初识JVM
1、JVM架构图
2、类加载器
1、启动类加载器(根类加载器Bootstrap Class Loader)
用来加载Java的核心类库(jre/lib/rt.jar)
2、扩展类加载器(Extension Class Loader)
用来加载Java/lib/ext
3、系统类加载器(应用程序加载器 System Class Loader)
加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径
4、自定义类加载器(略)
3、双亲委派机制
简单的说就是一个类加载器在接到加载类请求时,会向上委托给自己的父类加载器加载一直到启动类加载器;如果父类可以完成加载任务,就成功返回;父类无法加载才会自己去加载;如果都无法完成加载时就会抛出ClassNotFound的错误;
4、Native关键字 - (修饰的方法都存在本地方法区)
凡是native修饰的,说明Java的作用范围达不到了,会去调用底层C/C++的库;
调用本地库接口(JNI)加载本地方法库中的方法;
JNI作用:扩展Java的使用,融合不同的编程语言为java所用;
5、程序寄存器
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计;
6、方法区
所有线程共享,静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法中,但是实例变量存在堆内存中,和方法区无关;简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间;
static修饰、final修饰、Class、常量池;
7、栈
栈内存,主管程序的运行,生命周期和线程同步;线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题;会存在栈溢出问题(StackOverFlowError),如递归调用。。。
8、堆
这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组,堆内存的大小是可以调节的;
堆内存中细分为三个区域:
所有对象时在伊甸园区创建的;
#测试机8G内存
// JVM试图使用的最大内存 long l = Runtime.getRuntime().maxMemory(); // JVM的总内存 long l1 = Runtime.getRuntime().totalMemory(); System.out.println("最大内存:"+l/(double)1024/1024);//1796M System.out.println("JVM的总内存:"+l1/(double)1024/1024);//240M
默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存是1/64;
扩展1:遇到OOM(Out Of Memory)该怎么处理?
1、可以尝试调大VM分配内存:
#VM OPTIONS命令 -Xms1024m -Xmx1024m -XX:+PrintGCDetails -Xms:设置初始总内存分配大小,默认1/64 -Xmx:设置最大分配内存,默认1/4 XX:+PrintGCDetails:打印GC信息
控制台查看配置后的结果:1.8之后,元空间(之前叫永久代)逻辑上存在,物理上不存在,由下图可以看出;
2、调大内存后还是出错,就分析内存快照,看一下哪个地方出现了问题(需要借助专业工具)
测试工具有JProfiler、MAT等,这边用JProfiler分析Dump内存文件;
首先,本机安装JProfiler;然后IDEA也要安装扩展,IDEA安装完重启下工具栏就会显示图标了;
然后,本机安装好后,IDEA还要配置下安装路径;
也是需要配置下VM参数;
#VM OPTIONS 为了方便测试,将虚拟机内存调小了,然后指定Dump内存溢出错误时文件
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
运行之后会在项目根目录生成一个.hprof文件,双加开发即可:
9、GC分代回收算法
参考链接:很详细清晰;
GC的作用区域在堆和方法区;
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代:新生代 -》幸存区(分from,to两块,动态变换,空的变为to区)-》老年代
默认配置当一个对象经历了15次GC,还没有死,就可以进入老年代了;以下参数可以调整进入老年代的次数
#VM OPTIONS
-XX:+MaxTenuringThreshold=16
1、两种GC
轻GC(普通的GC):
1)在不断创建对象的过程中,当Eden区域被占满,此时会开始做轻GC;
2)此时不能回收的被放入幸存To区
3)如果幸存From区也是空的,那么From区变为To区,To区变为From区;如果幸存From区已存放有对象就复制到To区,此时From区清空再互换(From、To)身份(From和To是逻辑层次的,反正就是幸存区总会有一个是空的状态,称之为To区);
4)依次类推,始终保证幸存区有一个空的,用来存储临时对象,用于交换空间的目的。反反复复多次没有被淘汰的对象,将会被放入Old区域中,默认15次(由参数--XX:MaxTenuringThreshold=15 决定);
重GC(全局GC/Full GC):
JVM会安全的暂停所有正在执行的线程,来回收内存空间,在这个时间内,所有除了回收垃圾的线程外,其他有关JAVA的程序,代码都会静止,反映到系统上,就会出现系统响应大幅度变慢,卡机等状态。
触发Full GC的情况:
1)System.gc()方法的调用:
此方法的调用是建议JVM进行Full GC,尽管仅仅是建议而非一定,但非常多情况下它会触发 Full GC,从而添加Full GC的频率,也即添加了间歇性停顿的次数。
强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+DisableExplicitGC来禁止RMI调用System.gc。
2)老年代空间不足
一种是分配一个对象,空间真的不足。另一种是由于内存碎片,导致没有连续内存空间来分配给对象。
3)metaspace空间不足也会造成Full GC
metaspace中存放的为一些class的信息、常量、静态变量等数据,当系统中要载入的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满。在未配置为採用CMS GC的情况下也会执行Full GC。
4)之前历次minorGC晋升到老年代的对象平均大小如果大于此时老年代的剩余空间,也会导致一次Full GC。
2、常用算法
引用计数器:每有一个引用就+1,基本不用;
复制算法:
优点:没有内存的碎片;
缺点:浪费了空间,幸存区永远有一个是空的;假设百分百存活的话,复制起来效率很差;
标记清除算法:
优点:不需要额外的空间;
缺点:两次扫描(标记,清除),严重浪费时间,会产生内存碎片;
标记压缩:在标记清除两次扫描,再一次扫描,整理内存空间
优点:防止内存碎片产生;
缺点:又多一次扫描;
总结:
内存效率(时间复杂度) | 复制算法 > 标记清除算法 > 标记压缩算法 |
内存整齐度 | 复制算法 == 标记压缩算符 > 标记清除算法 |
内存利用率 | 标记压缩算符 == 标记清除算法 > 复制算法 |
年轻代 | 存活率低,用复制算法 |
老年代 | 区域大,存活率高,标记清除算法 + 标记压缩(混合);调优:设定几次标记清除后再压缩 |
GC中相关问题
问题1:怎么定义活着的对象?
从根引用开始,对象的内部属性可能也是引用,只要能级联到的都被认为是活着的对象。
问题2:什么是根?
本地变量引用,操作数栈引用,PC寄存器,本地方法栈引用等这些都是根。
问题3:对象进入Old区域有什么坏处?
Old区域一般称为老年代,老年代与新生代不一样。新生代,我们可以认为存活下来的对象很少,而老年代则相反,存活下来的对象很多,所以JVM的堆内存,才是我们通常关注的主战场,因为这里面活着的对象非常多,所以发生一次FULL GC,来找出来所有存活的对象是非常耗时的,因此,我们应该避免FULL GC的发生。
问题4:S0和S1一般多大,靠什么参数来控制,有什么变化?
一般来说很小,我们大概知道它与Young差不多相差一倍的比例,设置的参数主要有两个:
-XX:SurvivorRatio=8 -XX:InitialSurvivorRatio=8
第一个参数(-XX:SurvivorRatio)是Eden和Survivous区域比重(注意Survivous一般包含两个区域S0和S1,这里是一个Survivous的大小)。如果将-XX:SurvivorRatio=8设置为8,则说明Eden区域是一个Survivous区的8倍,换句话说S0或S1空间是整个Young空间的1/10,剩余的8/10由Eden区域来使用。
第二个参数(-XX:InitialSurvivorRatio)是Young/S0的比值,当其设置为8时,表示S0或S1占整个Young空间的1/8(或12.5%)。
问题5:一个对象每次Minor GC时,活着的对象都会在S0和S1区域转移,讲过MInor GC多少次后,会进入Old区域呢?
默认是15次,参数设置
--XX:MaxTenuringThreshold=15
,计数器会在对象的头部记录它的交换次数