JVM学习总结
1.JVM简略图2.类加载器:类是模板,对象是具体的,每new处理一个对象,jvm会给新对象分配一个地址空间1.虚拟机自带的加载器2.启动类(根) 加载器3.扩展类加载器4.应用程序加载器类加载器的类别BootstrapClassLoader(启动类加载器)c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地
1.JVM简略图
2.类加载器:
类是模板,对象是具体的,每new处理一个对象,jvm会给新对象分配一个地址空间
1.虚拟机自带的加载器
2.启动类(根) 加载器
3.扩展类加载器
4.应用程序加载器
类加载器的类别
BootstrapClassLoader(启动类加载器)
c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
ExtClassLoader (标准扩展类加载器)
java编写,加载扩展库,如classpath中的jre ,javax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
AppClassLoader(系统类加载器,应用程序加载器)
java编写,加载程序所在的目录,如user.dir所在的位置的class
CustomClassLoader(用户自定义类加载器)
java编写,用户自定义的类加载器,可加载指定路径的class文件
双亲委派机制:安全
作用优势:## 标题
避免重复加载 + 避免核心类篡改
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java
API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
具体过程:
1.类加载器收到类加载的请求,判断此类是否已经加载过,如果已经加载过则不需要加载,
2.如果未加载过,将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载
3.启动加载检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则抛出异常,通知子加载器加载
4.重复步骤3
如果加载失败 抛出Class Not Found …
null;java调用不到 ,c,c++
双亲委派机制流程图:
3.沙箱安全机制
我们都知道,程序员编写一个Java程序,默认的情况下可以访问该机器的任意资源,比如读取,删除一些文件或者网络操作等。当你把程序部署到正式的服务器上,系统管理员要为服务器的安全承担责任,那么他可能不敢确定你的程序会不会访问不该访问的资源,为了消除潜在的安全隐患,他可能有两种办法:
让你的程序在一个限定权限的帐号下运行。
利用Java的沙箱机制来限定你的程序不能为非作歹。以下用于介绍该机制。
什么是沙箱?
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
java中的安全模型:
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示
JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示
JDK1.1安全模型
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示
JDK1.2安全模型
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示
最新的安全模型
以上提到的都是基本的 Java 安全模型概念,在应用开发中还有一些关于安全的复杂用法,其中最常用到的 API 就是 doPrivileged。doPrivileged 方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源。有时候这是非常必要的,可以应付一些特殊的应用场景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。
组成沙箱的基本组件:
1.字节码校验器(bytecode verifier):
确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
2.类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
3.存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
4.安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
5.安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
参考:点击访问
4.native 关键字
凡事带native关键字,说明java 的作用范围达不到了,回去调用C语言底层库
会进入本地 方法栈,调用本地方法本地接口 JNI
JNI的作用:扩展java 的使用,融合不同的编程语言为java所用, 最初想融合c和c++.
它在内存中专门开辟了一块标记区域:Native Method Stack,登记native方法
在最终执行的时候加载本地方法库中的方法通过JNI
目前该方法使用越来越少了,除非与硬件有关的应用
5.程序计数器
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码,(用来存储指针指向下一条指令的地址,也即将要执行的指令代码)
在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
6.方法区
Method Area 方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,该区域属于共享空间;
静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
Class对象是存放在堆区的,不是方法区!这点很多人容易犯错。类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的!
static ,final ,Class,常量池
常量池
jvm为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,
integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。
因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java程序的动态链接中起了核心的作用。
因为方法区是被所有线程共享的,所以必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类还没有被加载的情况下,只应该有一个线程去加载,而另一个线程等待。
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。同样方法区也不必是连续的。方法区可以在堆(甚至是虚拟机自己的堆)中分配。jvm可以允许用户和程序指定方法区的初始大小,最小和最大尺寸。
方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展java程序,一些类也会成为垃圾。jvm可以回收一个未被引用类所占的空间,以使方法区的空间最小。
7.运行时栈
栈内存:主管程序的运行,声明周期和线程同步
程序结束,栈内存也就释放了
对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就over
栈:8大基本类型 + 对象引用 + 实例的方法
1.局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。方法 Code 属性的 max_locals 数据项确定了该方法所需要分配的最大局部变量表的容量。
局部变量不存在“准备”阶段,如果一个局部变量定义了但没有赋初始值是没法使用的。
赋 null 值的操作在经过虚拟机 JIT 编译器优化之后会被消除掉。
2.操作数栈
操作数栈是一个后入先出(LIFO)栈,当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈和出栈操作。方法 Code 属性的 max_stacks 数据项设定了操作数栈的最大深度。
3.动态连接
Class 文件常量池中指向方法的符号引用中会有一部分在运行期间转化为直接引用,这部分称为动态连接。
与动态连接相对应的是静态连接。静态方法、私有方法、实例构造器、父类方法和 final 方法统称为虚方法,虚方法的调用没有其他版本,无须对方法接收者进行多态选择,因此它们在类加载的解析阶段就会把涉及到的符号引用全部转变为可确定的直接引用,不会延迟到运行期才去完成。而有些方法,比如重载和重写方法,具有多个版本,无法直接确定调用的是什么版本,这部分符号引用的转换就必须等到运行期来完成。
4. 方法返回地址
当一个方法开始执行以后,只有两种方法可以退出当前方法:
当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
当方法返回时,可能进行3个操作:
恢复上层方法的局部变量表和操作数栈
把返回值压入调用者调用者栈帧的操作数栈
调整 PC 计数器的值以指向方法调用指令后面的一条指令
三种JVM
- SUN公司的 HotSpot
Java HotSpot™ 64-Bit Server VM (build 25.261-b12, mixed mode)
(使用最多) - BEA JRockit
- IBM J9 VM
8.堆
Heap ,一个JVM只有一个堆内存,堆内存大小是可以调节的。
类加载器读取了这些文件后,一般会把什大学放到堆中?
类,方法,常量,变量,保存我是所引用类型的真实对象
堆内存中分为三个区域:
- 新生区(伊甸园 Eden space)
- 养老区
- 永久区
GC垃圾回收,主要在伊甸园区和养老区
假设内存满了,OOM,堆内存不够
JKD8以后, 永久存储区改了名字(元空间);
新生区:
类:诞生和成长的地方,甚至死亡;
-
伊甸园:所有的对象都是在伊甸园区 new出来
-
幸存区(0,1)
-
经过研究,99%的对象都是临时的
老年区:
永久区
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据, 存储的是Java运行时的一些环境或类信息~,这个区域不存在垃圾回收!关闭VM虚拟就会释放这个区域的内存
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现00M;
●jdk1.6之前:永久代,常量池是在方法区;
●jdk1.7 :永久代,但是慢慢的退化了,去永久代, 常量池在堆中
●jdk1.8之后:无永久代,常量池在元空间
元空间:逻辑上存在,物理上不存在
下面跑一个程序看看虚拟机的内存大小:
public class Test {
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
//返回jvm的初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max=" + max +"字节" + (max/(double)(1024*1024))+"MB");
System.out.println("total=" + max +"字节" + (total/(double)(1024*1024))+"MB");
//默认情况下:分配内的总内存大约是电脑内存的1/4,而初始化的内存为1/64
}
}
默认情况下:分配内的总内存大约是电脑内存的1/4,而初始化的内存为1/64
运行结果:(我的电脑内存16G)
可见 305664k + 699392 = 1029177344
所以元空间的物理内存并不在堆中
在Configuration中手动调节内存大小:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
OOM:
public class Hello {
public static void main(String[] args) {
// -Xms8m -Xmx8m -XX:+PrintGCDetails
String str = "longzx6666666666666";
while (true){
str += str + new Random().nextInt(999999999)+new Random().nextInt(999999999);
}
}
}
将内存调为8M,打印结果
-Xms8m -Xmx8m -XX:+PrintGCDetails
在一个项目中,突然出现了0OM故障,那么该如何排除~研究为什么出错
●能够看到代码第几行出错:内存快照分析工具,MAT, Jprofiler
●Dubug,一行行分析代码!
MAT, Jprofiler作用
●分析Dump内存文件,快速定位内存泄露;
●获得堆中的数据
●获得大的对象~
内存分析工具:JProfiler下载安装,已经IDEA安装插件
1.下载JProfiler(IDEA)插件
在IDEA上直接下载Settings–plugins,搜索JProfiler 点击 install 按钮安装,然后从启IDEA工具
方式2:
官网下载插件,手动安装
官方插件下载地址
在首页向下翻,中部位置能看到JProfiler最新版本,包括历史版本下载链接。点击DOWNLOAD进行下载。
然后在IDEA中引入插件
找到你压缩包的位置即可,然后重启
安装成功后会有如下标志
2.安装JProfiler监控软件
官网地址
3.配置IDEA运行环境
Settings–Tools–JProflier–JProflier executable选择JProfile安装可执行文件。(如果系统只装了一个版本,启动IDEA时会默认选择)保存
测试:
import java.util.ArrayList;
public class DumpDemo {
byte[] array = new byte[1*1024*1024]; //1m
public static void main(String[] args) {
ArrayList<DumpDemo> list = new ArrayList<>();
int count = 0;
try{
while (true){
list.add(new DumpDemo());
count = count + 1;
}
}catch (Exception e){
System.out.println(" count:"+count);
}
}
}
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
再次运行
打开文件
点击打开文件,使用JProfiler分析
分析线程:
也可以在程序中捕获Error,
9.JVM常用参数:
通过”java -X”可以输出非标准参数列表,如下所示:
堆设置
-Xms256M:初始堆大小256M,默认为物理内存的1/64
-Xmx1024M:最大堆大小1024M,默认为物理内存的1/4,等于与-XX:MaxHeapSize=64M
-Xmn64M:年轻代大小为64M(JDK1.4后支持),相当于同时设置NewSize和MaxNewSize为64M
-XX:NewSize=64M:初始年轻代大小
-XX:MaxNewSize=256M:最大年轻代大小(默认为堆最大值的1/3)
-XX:OldSize=64M:年老代大小64M(测试验证JDK1.8.191该参数设置无效,JDK11下设置成功)
-XX:NewRatio=4:年老代:年轻代=4:1,默认值2
-XX:SurvivorRatio=8:年轻代中,2个Survivor区与1个Eden区比例=2:8,Survivor占新生代内存比例为1/5,默认值8
-XX:MaxHeapFreeRatio=70:堆内存使用率大于70时扩张堆内存,xms=xmx时该参数无效,默认值70
-XX:MinHeapFreeRatio=40:堆内存使用率小于40时缩减堆内存,xms=xmx时该参数无效,默认值40
打印垃圾回收器信息和设置垃圾回收器(串行、并行、并发等行为的收集器)
-verbose:gc :记录GC运行以及运行时间,一般用来查看GC是否有瓶颈
-XX:+PrintGCDetails :记录GC运行时的详细数据信息,包括新生占用的内存大小及消耗时间
-XX:-PrintGCTimeStamps :打印收集的时间戳
-XX:+UseParallelGC :使用并行垃圾收集器
-XX:-UseConcMarkSweepGC :使用并发标志扫描收集器
-XX:-UseSerialGC :使用串行垃圾收集器
-Xloggc:filename :设置GC记录的文件
-XX:+UseGCLogFileRotation :启用GC日志文件的自动转储
-XX:GCLogFileSize=1M :控制GC日志文件的大小
类加载和跟踪类加载和卸载的信息
Xbootclasspath :指定需要加载,但不想通过校验类路径。
JVM会对所有的类在加载前进行校验并为每个类通过一个int数值来应用
-XX:+TraceClassLoading :跟踪类加载的信息(诊断内存泄露很有用)
-XX:+TraceClassUnloading :跟踪类卸载的信息(诊断内存泄露很有用)
10.GC:垃圾回收
JVM在进行GC时,并不是对这三个区域统-一回收。 大部分时候,回收都是新生代~
●新生代
●幸幸区(form, to)
●老年区
GC两种类:轻GC (普通的GC), 重GC (全局GC)
幸存区的from和to是动态变化的,谁空谁是to
GC常用算法
引用计数器算法:
原理:此对象有一个引用,则+1;删除一个引用,则-1。只用收集计数为0的对象。
缺点:无法处理循环引用的问题。如:对象A和B分别有字段b、a,令A.b=B和B.a=A,除此之外这2个对象再无任何引用,那实际上这2个对象已经不可能再被访问,但是引用计数算法却无法回收他们。
复制算法:
复制算法就是将内存空间按容量分成两块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后把已经使用过的这一块一次清理掉。这样使得每次都是对半块内存进行内存回收。内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。
Eden中存活的对象会被复制到To中
●好处:没有内存的碎片~
●坏处:浪费了内存空间~ :多了- -半空间永远是空to。 假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候;新生区~
标记清除算法:
该算法分为标记和清除两个阶段。标记就是把所有活动对象都做上标记的阶段;清除就是将没有做上标记的对象进行回收的阶段
●优点:不需要额外的空间!
●缺点:两次扫描,严重浪费时间,会产生内存碎片。
标记压缩:
标记-压缩算法与标记-清理算法类似,只是后续步骤是让所有存活的对象移动到一端,然后直接清除掉端边界以外的内存。
标记清除压缩:
先进行几次标记清除,然后再压缩
总结:
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
思考一一个问题:难道没有最优算法吗?
答案:没有,没有最好的算法,只有最合适的算法—>- GC :分代收集算法
年轻代:
●存活率低
●复制算法!
老年代:
●区域大:存活率
●标记清除(内存碎片不是太多) +标记压缩混合实现
GC题目:
●JVM的内存模型和分区~详细到每个区放什么?
●堆里面的分区有哪些? Eden, form, to,老年区,说说他们的特点!
●GC的算法有哪些?标记清除法,标记压缩,复制算法,引用计数器,怎么用的?
●轻GC和重GC分别在什么时候发生?
11.JMM
更多推荐
所有评论(0)