深入理解Java虚拟机笔记——垃圾收集器与内存分配策略
判断对象是否死亡
引用计数器算法
给对象添加一个引用计数器,每当有地方引用它,计数器值就加1;当引用失效时,计数器值减1;计数器为0的对象就不可能再被使用。
缺点是很难解决对象之间相互循环引用的问题。
可达性分析算法
通过一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象不可用。
可作为 GC Roots 的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
各种引用
引用分为强引用、软引用、弱引用和虚引用四种。
-
强引用类似于“Object obj = new Object()”这类引用,垃圾收集器永远不会回收被强引用的对象。
-
软引用描述一些还有用但非必须的对象。在系统将要发生内存溢出异常前,将会把这些软引用关联着的对象列入回收范围中进行第二次回收。使用SoftReference类实现软引用。
-
弱引用描述非必须对象。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。WeakReference类实现弱引用。
-
虚引用的唯一目的是能在这个被虚引用关联的对象被回收时收到一个系统通知。PhantomReference类实现虚引用。
回收方法区
永久代的垃圾收集主要回收两部分:废弃常量和无用的类。
无用的类的判定:
- 该类的所有实例已经都被回收。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该方法。
垃圾收集算法
标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。
两个缺点:一是效率不高;二是空间问题,标记清除后会产生大量不连续的内存碎片,导致以后程序需要分配大对象的时候,因无法找到足够连续内存而需要触发另一次垃圾收集动作。
复制算法
算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
代价是将内存缩小为原来一半。如图分为左右两边。
改进:这种算法一般用来回收新生代,因为新生代中的对象98%是存活时间很短的,所以并不按照1:1的比例来划分内存空间。将*内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survior。当回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survior空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1.
当Survivor空间不够,需要依赖其他内存(这里指老年代)进行分配担保。
标记-整理算法
算法的标记过程和“标记-清除”算法一样,但是后续步骤是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
一般把Java堆分为新生代和老年代,新生代使用复制算法,老年代使用“标记——清理”或者“标记——整理”算法。
HotSpot算法实现
枚举根节点
可作为GC Roots的结点主要在全局性的引用(常量或类静态属性)与执行上下文(栈帧中的本地变量表)中。
GC停顿(Stop The World)
可达性分析期间整个执行系统看起来像被冻结在某个时间点上,不可出现分析过程中对象引用关系还在变化的情况,否则分析结果准确性无法得到保障。
安全点
程序执行时并非所有地方都能停顿下来GC,只有到达安全点才能暂停。
分为抢先式中断和主动式中断。
垃圾收集器
Serial 收集器
是单线程的收集器。在垃圾收集的时候,必须暂停其他所有工作线程,直到收集结束。
新生代使用复制算法,老年代使用标记——整理算法。
虚拟机运行在Client模式下的默认新生代收集器(简单高效)。
ParNew 收集器
Serial收集器的多线程版本。
虚拟机运行在Server模式下首选的新生代收集器。可以与 CMS收集器 配合。
Parallel Scanvenge 收集器
目标是达到一个可控制的吞吐量。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
CMS 收集器
以获取最短回收停顿时间 为目标的收集器。
基于 标记——清除 算法实现。
并发和并行概念:
- 并发(Concurrent):用户线程与垃圾收集线程同时执行。用户程序和垃圾收集程序运行于不同CPU。
- 并行(Parallel):多条垃圾收集线程并行工作,用户线程处于等待状态。
步骤:
- 初始标记。初始标记仅仅标记GC Roots直接关联到的对象,速度很快。
- 并发标记。进行GC Roots Tracing的过程。
- 重新标记。修正并发标记期间因用户程序继续运作导致标记产生变动的那部分对象的标记记录。
- 并发清除。
初始标记、重新标记需要“Stop the World”。
G1 收集器
面向服务端应用 的垃圾收集器。
特点:
- 并行与并发。
- 分代收集:不需要其他收集器配合。
- 空间整合:整体上基于 标记——整理算法,局部上基于 复制算法。不会产生内存空间碎片。
- 可预测的停顿。
步骤:
- 初始标记。标记GC Roots直接关联对象,且修改 TAMS 的值。
- 并发标记。GC Roots 可达性分析(Tracing)。
- 最终标记。几乎和CMS的重新标记一样。
- 筛选回收。
新生代、老年代 和 永久代(元空间)
新生代
新生代分为三个部分:一个Eden区和两个Survior区(分别叫做 from 和 to)。Eden区和Survior区的默认大小比例为8:1,两个Survivor区所以占2。
存放于Java堆中。
老年代
老年代为 OldGen。和新生代一样存放于Java堆中。分代原因是利于分代GC。
永久代(元空间)
方法区和永久代的关系:方法区是JVM的规范,永久代是JVM规范的一种实现。
永久代在 动态生成类 情况比较多的情况下容易内存溢出。
永久代(Permanent Generation)在 JDK1.8 后被移除了,对于方法区的实现采用 元空间(Metaspace) 来代替。
JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
个人认为的更换原因:
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。
Minor GC、Major GC 和 Full GC
- 新生代GC(Minor GC):发生在新生代的垃圾收集动作。Minor GC比较频繁,且回收速度比较快。
- 老年代GC(Major GC / Full GC):发生在老年代的GC,一般出现 Major GC 经常会伴随着 Minor GC(因此是 Full GC)。一般速度比Minor GC慢十倍以上。
何时执行 Full GC
1. System.gc() 的调用。
2. 老年代空间不足
老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
3. 永久区(Permanent Generation)空间不足
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanent Generation中存放的为一些class的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,Permanent Generation 可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
4. CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;
concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
应对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
5. 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象。在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000
来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC
来禁止RMI调用System.gc。
6、堆中分配很大的对象
所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即 -XX:+UseCMSCompactAtFullCollection
开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。
内存分配和回收策略
1. 对象优先分配在 Eden
对象主要分配在新生代的 Eden 区上。Eden 区没有足够空间分配的话,虚拟机会发起一次 Minor GC。
2. 大对象直接进入老年代。
大对象指的是 很长的字符串以及数组。
原因:放入新生代会导致内存还有不少空间就提前触发垃圾收集以获取足够的连续空间来安置。
可以通过 -XX:PretenureSizeThreshhold
参数来设置 大于这个设置值的对象直接在老年代分配。
3. 长期存活的对象将进入老年代。
每个对象有个对象年龄计数器,对象每熬过一次 Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认15岁),将晋升到老年代。这个年龄阈值可以通过 -XX:MaxTenuringThreshold
设置。
4. 动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等待 MaxTenuringThreshold 中要求的年龄。
5. 空间分配担保
因为新生代中使用复制算法,所以Survivor区如果比较小或者GC后存活下来的对象比较多则可能无法复制过去。因此需要老年代进行空间分配担保,把Survivor容纳不下的对象进入老年代。
在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure
是否允许担保失败。如果允许,会继续检查 老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行Minor GC,如果小于或不允许担保,则会进行一次Full GC。