垃圾收集器与内存分配策略
垃圾收集器与内存分配策略
@(Java虚拟机)[垃圾收集, GC]
[TOC]
对象已死吗
程序计数器,虚拟机栈,本地方法栈随线程回收而回收,而Java堆和方法区不会回收,对象也是动态创建。这部分区域是垃圾回收的主要区域。
引用计数算法
给对象添加一个引用计数器,每当引用时,计数器加1,引用失效计数器减1。为0的对象就是不在使用的。引用计数简单有效率,但是主要无法解决对象之间的相互循环引用问题。
eg:A.a=B.b; B.b=A.a
可达性分析算法
可达性分析以'GC Roots'对象作为起点,向下搜索,走过路径称为引用链。一个对象到GC Roots没有任何引用链即从GC Roots到对象不可达,则该对象不可用。
可达性分析算法判定对象是否可回收
在Java语言中可以作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
再谈引用
判断对象是否存活与“引用”有关。在JDK1.2前,对引用定义很简单,如果reference类型的数据存储的数值代表另外一块内存的起始地址,这块内存代表着一个引用。在JDK1.2以后Java对引用概念进行扩充,将引用分为4种:(强度依次减弱)
- 强引用(Strong Reference) eg:Object obj=new Object();强引用还存在,就不回收被引用的对象。
- 软引用(Soft Reference)有用但非必须对象,系统在发生内存溢出异常前,会将这些对象列进回收范围中。
- 弱引用(Weak Reference)非必须对象,下次垃圾收集回收。
- 虚引用(Phantom Reference)对象是否有虚引用不会对其生存时间构成影响,也无法通过虚引用取得对象实例,设置虚引用的作用在于这个对象被垃圾收集器回收时收到一个系统通知。
生存还是死亡
在可达性分析算法中不可达的对象,也并非“非死不可”,对象暂时处于“缓刑阶段”,真正死亡需要至少两次的标记过程。
对象不可达会被第一次标记并且筛选,筛选条件是对象是否需要执行finalize方法。(对象没有覆盖finalize方法,或者finalize已经被虚拟机触发过,虚拟机则认为是没有必要执行)
如果需要执行finalize方法的,对象会被放入F-Queue队列中,并且会被虚拟机创建的Finalizer(低优先级)线程去调用finalize方法。稍后GC会对F-Queue中的对象进行第二次小规模标记,如果在finalize过程中该对象又和可达对象建立了关联,则在第二次标记时会被移除“即将回收”集合。
注:
1.finalize方法只会被系统调用一次。如果对象被调用过此方法,而且面临下次回收,它的finalize不会被执行。
2.finalize不确定性大,实用性不大。回收工作不如try-finally等方式。建议忘记此方法的存在。
回收方法区
方法区(或者HotSpot的永久代)垃圾回收主要是两部分内容:废弃常量和无用的类。
废弃常量判定:字符串“abc”在常量池中,但系统没有一个String对象指向它。常量池中其他类(接口),方法,字段的符号引 用与此类似。
无用类判定:3个条件
- 该类实例已全部回收。Java堆中不存在该类实例了。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用。无法在任何地方通过反射访问该类的方法
满足上述条件后虚拟机可以对无用类回收了。但不一定必然会回收。是否对类回收HotSpot提供-Xnnclassgc参数控制。
垃圾收集算法
标记-清除算法
先标记,在清除。不足之处:1.效率问题,标记和清除效率都不高。2.空间问题,清除后产生不连续内存碎片。
标记-清除算法
复制算法
将内存分为两块,每次使用一块,一块内存用完时,就将还存活的对象复制到另外一块上。前面内存空间一次清理掉。
不足:浪费内存空间。存活对象多时,复制效率不高。
优化方案:
大部分对象死得快。划分比例改一改:HotSpot--> 8(Eden)+1(Survivor)+1(Survivor)。浪费最后一份Survivor即可。
缺点:极端情况,1份装不下存活的对象,就需要其他内存空间做担保。
复制算法
标记-整理算法
和标记-清除算法类似,但是后续操作不是清理而是让存活对象移动到一端。清理端边界以外的内存
标记-整理算法
分代收集算法
当前商业虚拟机都采用此算法。根据对象的存活周期不同将内存划分为几块。将Java堆分为新生代和老年代,在新生代中使用复制算法,在老年代中使用标记-清理或者标记-整理算法。
HotSpot的算法实现
枚举根节点
使用OopMap来记录那些位置是引用,不需要对整个全局性引用和栈帧的本地变量表遍历。节约时间。
GC时需要暂所有的Java执行线程使分析可靠。
安全点
在OopMap的协助下,HotSpot快速完成GC Roots枚举。为了节约空间不是所有的指令会产生OopMap。安全点位置才会产生,也是在安全点下才能GC。
安全点选取条件:是否具有让程序长时间执行的特征。
长时间执行的特征是指令序列复用,例如方法调用,循环跳转,异常跳转等。这些地方的指令才会产生SafePoint。
GC发生时让所有线程都跑到安全点停下来:
抢先式中断:GC发生所有线程中断,如果线程中断不在安全点上,则恢复线程跑到安全点上。基本没有虚拟机这样做
主动式中断:设置标志,GC发生时,线程轮询这个标志,中断标志为真则自己中断,轮询标志的地方和安全点重合。
安全区域
线程不执行时(处于Sleep和Blocked下),无法响应JVM的中断请求。这种情况需要安全区域配合。
安全区域:指这段代码片段中不会引起引用关系的变化。在这区域中GC都是安全的。
线程执行到Safe Region中时,首先标记自己是进入Safe Region状态。当要离开Safe Region时需要检查是否完成了GC,没有完成就必须等待可以离开信号为止。
垃圾收集器
图中展示7中收集器在不同分代中工作。连线代表搭配使用。
HotSpot虚拟机的垃圾收集器
Serial收集器
Serial收集器不仅仅是使用一个CPU或者一条线程去完成垃圾收集,更重要的是它收集垃圾时必须暂停其他工作线程。
Serial收集器在Client模式下是很好的选择
新生代采用复制算法,暂停所有用户线程,老年代采用标记-整理算法,暂停所有用户线程
Serial/Serial Old收集器运行示意图
ParNew收集器
ParNew收集器是Serial的多线程版本。和Serial收集器没有太多创新之处。但它是Server模式下的虚拟机的首选新生代收集器。ParNew是除了serial外能和CMS(Concurrent Mark Sweep)搭配使用的收集器。ParNew在单CPU下效果不会有Serial的好。
ParNew/Serial Old收集器运行示意图
Parallel Scavenge收集器
Parallel Scavenge是新生代收集器,使用复制算法,并行的多线程收集器。
Parallel Scaveng收集器关注点和其他收集器不一样,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。而此收集器关注吞吐量。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
Parallel Scavenge可以使用GC自适应的调节策略来自动动态调整参数。
Serial Old收集器
Serial Old是Serial的老年代版本,也是单线程收集器,使用标记-整理算法。主要给Client模式的虚拟机使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS收集器
CMS(Concurrent Mark Sweep)收集器以获取最短回收停顿时间为目标。主要应用于B/S系统上。CMS采用标记-清除算法实现。分4个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记和重新标记需要Stop the world,初始标记GC Roots能直接关联的对象,速度快。并发标记进行GC Roots Tracing过程。重新标记对在并发标记过程中有部分对象标记产生变动的一部分重新标记。时间比初始标记长,但比并发标记短得多。
CMS过程中耗时较长的部分是并发的,所以整体上能和用户线程一起并发执行。
CMS缺点:
- 对CPU资源非常敏感(并发导致),CMS默认启动回收线程数(CPU数量+3)/4,CPU数量少于4个时,影响很大。
- 无法处理浮动垃圾,可能导致Concurrent Mode Failure 失败导致另一次Full GC。
- 标记-清除算法产生大量碎片,内存空间连续的无法分配大对象时需要Full GC。
G1收集器
G1是面向服务端应用的收集器,G1具备的特点:
- 并行与并发:充分利用多CPU,多核硬件优势。
- 分代收集:可以独立管理整个GC堆,不需其他收集器配合。
- 空间整合:G1整体看来是基于标记——整理算法实现。从局部看是基于复制算法
- 可预测停顿:可指定在M毫秒中,垃圾收集不超过N毫秒
G1将内存分为多个大小相等的独立区域Region,还保留新生代和老年代的概念,但不再是物理隔离的,都是是一部分Region(不需连续)的集合。G1避免在全区域垃圾回收,可以对单个Region回收,后台维护一个优先列表,来决定回收那些Region。
Remembered Set步骤:保证不对全堆扫描也不会遗漏。记录在不同Region中的对象引用。
G1收集器步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
理解GC日志
GC日志33.3125和100.667代表GC发生时间,Java虚拟机启动以来的秒数。
Full GC代表stop the world
DefNew,Tenured,Perm代表GC发生区域。显示的区域名和GC收集器相关,DefNew代表Serial新生代,ParNew代表Parallel新生代,PSYoungGen代表Parallel Scavenge新生代。
3324K-》152K(3721K):GC前该区域内存区域已使用容量-》GC后改内存区域已使用容量(该内存区域总容量)
3324K-》152K(11904K):GC前Java堆已使用容量-》GC后Java堆已使用容量(Java堆总容量)
0.0025925secs表示该内存区域GC所占用的时间。单位秒
垃圾收集相关常用参数
1.png2.png
内存分配与回收策略
对象主要分配在新生代Eden区上,少数情况会分配在老年代中。规则不固定,细节由垃圾收集器和参数决定。
下面使用Serial/Serial Old收集器的内存分配和回收策略。
对象优先在Eden分配
private static final int _1MB=1024*1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M 限制Java堆大小为20M,不可扩展,10M新生代,10M老年代
* -XX:+PrintGCDetails
* -XX:SurvivorRatio=8决定新生代中Eden区与Survivor区=8:1
*/
public static void testAllocation(){
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1=new byte[2*_1MB];
allocation2=new byte[2*_1MB];
allocation3=new byte[2*_1MB];
allocation4=new byte[4*_1MB]; //出现一次Minor GC
}
eden space 8192K,from space 1024K,to space 1024K,新生代9216K。当分配allocation4时,内存不足,发生Minor GC,1,2,3被转到老年区,然后4被分配在Eden中。Survivor空闲。
新生代GC(Minor GC):发生频繁,速度快
老年代GC(Major/Full GC):速度慢,一般伴随Minor GC
大对象直接进入老年代
虚拟机提供-XX:pretenureSizeThreshold参数,大于这个值的对象直接分配到老年代,避免在Eden和两个Survivor发生复制。
注:pretenureSizeThreshold只对Serial和ParNew有效。
长期存活对象将进入老年代
给对象定义了对象年龄计数器,对象在Eden出生并在minor GC后任存活,并被Survivor容纳,将移动到Survivor中,对象年龄设为1,每过一次Minor GC,年龄加1。默认到15,就会转入老年代。参数值可用-XX:MaxTenuringThreshold设置。
动态对象年龄判定
如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于该年龄的对象进入老年代。就不需MaxTenuringThreshold中要求的年龄。
空间分配担保
在Minor GC前,虚拟机先检查老年代连续可用空间是否大于新生代对象总空间,如果成立,则Minor GC安全。不成立
虚拟机先查看HandlePromotionFailure设置值是否允许担保失败。允许则检查老年代连续空间是否大于历次转入老年代对象的平均值。大于则Minor GC(有风险)。小于或者HandlePromotionFailure不允许冒险,则Full GC。JDK6 Update14后HandlePromotionFailure参数不在使用。
风险是因为老年代需要做担保。