垃圾收集器与内存分配策略
一、垃圾收集器回收区域
程序计数器、虚拟机栈、本地方法栈这3个区域随线程而生、随线程而灭。栈中栈帧随着方法的进入和退出有条不紊地执行入栈和出栈操作。每一个栈帧中分配多少内存在类结构确定时就已经知道(编译器可知),于是这些区域的内存分配与回收具备确定性,在这几个区域就不需要过多考虑回收的问题。
Java堆与方法区内存的分配和回收是动态的,垃圾收集器关注的是这部分内存。
二、对象存活判定算法
1.引用计数算法
给对象添加一个引用计数器,每当一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象不可能再被使用。——这种算法判定效率高,大部分情况下是不错的算法(微软的COM(Component Object Model)
技术、Python
语言等),但缺点是这种算法很难解决对象之间相互循环引用的问题。
/**
* testGC()方法执行后,objA和objB会不会被GC呢?
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB; //相互引用
objB.instance = objA;
objA = null; //变量objA、objB不指向这两个对象了,且对象不会再被访问
objB = null;
// 假设在这行发生GC,objA和objB是否能被回收?
System.gc(); //引用计数器法不会通知GC回收对象,可达性分析算法会通知GC回收对象。
}
}
2.可达性分析(Reachability Analysis
)算法
思路:通过一系列被称为是GC Roots
的对象作为起始点,从这些节点开始向下搜索,搜索走到过的路径称为引用链(Reference Chain
),当一个对象到GC Roots
没有任何引用链相连(从GC Roots
到这个对象不可达),就说这个对象不可用。
其中对象5、6、7不可达,判定为可回收的对象。
在Java语言中,可做为GC Roots
的对象有:(1) 虚拟机栈(栈帧中的本地变量表)中引用的对象; (2)本地方法栈中JNI(Java Native Interface)
引用的对象; (3)方法区中常量引用的对象; (4)方法区中类静态属性引用的对象.
三 、引用说明
- 强引用(
Object obj = new Object();
)
只要强引用存在,垃圾收集器不会回收被引用的对象。生存时间:JVM
停止运行时终止。 - 软引用
软引用描述一些还有用但并非必须的对象。在系统将要发生内存溢出异常时,将会把这些对象列进回收对象范围内,进行第二次回收(第一次回收无引用、弱引用对象)。如果这次回收还没有足够的内存,才会抛出OOM
异常。生存时间:内存不足时终止。 - 弱引用
描述非必需对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC
发生之前。当垃圾收集器GC
时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。 生存时间: 下一次GC
之前。 - 虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,但也无法通过虚引用来取得一个对象实例。唯一目的: 能在这个对象被回收时,收到一个系统通知。
四、对象存活判定之后的事宜(对象自我拯救,两次标记)
可达性算法判定对象是否存活之后,对象并非“非死不可”,对象真正死亡经历两个标记过程。当对象在进行可达性分析之后没有GC Roots
相连接的引用链,该对象会被第一次标记并进行筛选,判定是否覆盖了finalize()
方法。如果对象没有覆盖finalize()
方法或者finalize()
已经被虚拟机调用过(只能调用一次), 则直接回收该对象。
如果对象覆盖了finalize()
方法且没有被调用过,则将其加入到F-Queue
队列之中,由虚拟机创建的一个低优先级Finalizer
线程去执行调用队列里的这个方法。
finalize()
方法是对象逃脱死亡的最后一次机会,执行之后,GC
将对F-Queue
中的对象进行第二次小规模标记,如果对象重新 与引用链上的对象进行了关联,则第二次标记时该对象被移除出”即将回收”的集合,否则该对象就被真正回收了。
结论: 1. 对象可以在GC
时自我拯救(finalize()方法
);2.finalize()
方法最多被系统自动执行一次。
五、回收方法区
永久代回收主要是废弃常量与无用的类。
1. 废弃常量
判断方法:可达性分析
2. 无用的类(-Xnoclassgc
控制)
- 该类所有的实例都已经被回收,即
Java
堆中不存在该类的任何实例。- 加载该类的
ClassLoader
已经被回收- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的对象。
六、垃圾收集算法
1. 标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记过程与对象自我拯救中标记过程一样。
缺点:
(1) 效率问题
标记和清除涉及两次堆的扫描,耗时严重。
(2) 空间问题
标记清楚之后会产生大量不连续的内存碎片,空间碎片过多会导致以后在程序运行过程中需要分配大对象时,无法找到足够的连续内存(Free List(空闲列表)
)而必须触发一次GC
动作。
2. 复制算法
原理:
将可用内存按照容量分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上去,然后把已使用过的内存空间一次清理掉。这样子,每次都是对整个半区进行内存回收,内存分配时不用考虑内存碎片的情况,只要移动堆顶指针按顺序分配内存即可,比较简单高效。
优点:
1. 清除过程效率高
2. 无内存碎片,利用bump the pointer(指针碰撞)
实现内存快速分配。
缺点:
需要双倍空间,适用于存活率比较低的对象(复制对象不多)。
现代虚拟机实现
1.将新生代分为内存较大的
Eden
区域和两块内存较小的Survivor
区域(8:1:1),每次使用Eden
和其中的一块Survivor
区域。当回收时,将Eden
和Survivor
中还存活的对象复制到另外一块Survivor
中,最后清理掉Eden
与刚才用过的Survivor
区域。
2.这个实现需要老年代进行内存分配担保,如果另外一块Survivor
空间没有足够空间存放上一次新生代收集下来存活的对象,这些对象直接通过分配担保机制进入老年代。
3.标记-整理算法
过程:标记过程与”标记-清除”算法一致,但不是直接对可回收对象进行清理,而是让存活的对象都想一端移动,然后直接清理掉端边界之外的内存。
优点:
无内存碎片,可利用bum the pointer(指针碰撞)
进行快速内存分配
缺点:
需要移动对象的成本,适用于老年代
4.分代收集算法
根据对象存活周期不同,将内存划分为几块,即新生代与老年代。新生代一般采用复制算法收集垃圾,老年代采用”标记-整理“算法回收垃圾。
七、垃圾收集器
并行与并发的区别
1. 并行: 多条垃圾收集线程并行工作,用户线程任然处于等待状态。
2. 并发:用户线程与垃圾收集线程同时运行。
Young Generation:
- Serial
- ParNew
- Parallel Scavenge
- G1
Tenured Generation:
- CMS
- Serial Old(MSC)
- Parallel Old
- G1
1. Serial GC
特性
单线程收集器,只会使用一个CPU
或者一条线程去完成垃圾收集工作,同时在它进行垃圾回收时,必须暂停其他的工作线程,知道垃圾收集结束。(Stop the World
)工作原理
Serial
新生代采用复制算法,暂停所有用户线程,Serial Old
采用标记-整理算法,暂停所有线程。使用场景
虚拟机运行在Client
模式下默认的新生代收集器,简单而高效。
2. ParNew GC
特性
使用多线程进行垃圾回收。运行原理
ParNew
新时代采用复制算法。使用场景
Server
模式下首选的新生代收集器。ParNew GC
使用-XX:+UseConcMarkSweepGC
选项后(ParNew + CMS);-XX:+UseParNewGC
(ParNew + Serial Old)
-XX:ParallelGCThread
限制垃圾回收线程数。
3. Parallel Scavenge
收集器(吞吐量优先)
特性
新生代收集器,复制算法,并行多线程收集器。CMS
等收集器关注的是尽可能缩短垃圾收集的用户线程停顿时间,而Parallel Scavenge
收集器为了达到一个可控制的吞吐量。1.吞吐量是
CPU
用于运行用户代码的时间与CPU
总消耗时间的比值(Throughput
).
2.停顿时间越短,越适合于与用户交互的程序,响应速度快。高吞吐量则可以高效利用CPU
时间,经快完成程序的运算任务,适用于后台运算,不需要较多的交互任务。参数控制
-XX:MaxGCPauseMillis
:控制最大垃圾收集停顿时间
-XXX:GCTimeRatio
: 直接设置吞吐量大小
-XX:+UseAdaptiveSizePolicy
打开后,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整-XX:SurvivorRario
等参数以提供最合适的停顿时间或者最大吞吐量。(自适应调节策略)
4. Serial Old
特性
单线程,标记-整理算法。原理
使用场景
(1)Client
模式下的虚拟机使用。
(2) 如果在Server
模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge
收集器搭配使用,另一种用途就是作为CMS
收集器的后备预案,在并发收集发生Concurrent Mode Failure
时使用。
5. Parallel Old GC
特性
Parallel Old
是Parallel Scavenge
收集器的老年代版本,使用多线程和“标记-整理”算法。原理
使用场景
在注重吞吐量以及CPU
资源敏感的场合,都可以优先考虑Parallel Scavenge
加Parallel Old
收集器。
6. CMS GC
特性
CMS(Concurrent Mark Sweep)
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。运行原理
CMS
收集器是基于“标记—清除”算法实现的,它的运作过程如下:
- 初始标记
初始标记只是标记一下GC Roots
能直接关联到的对象,速度很快,需要“Stop The World
”. - 并发标记
并发标记阶段就是进行GC Roots Tracing
的过程,遍历其他的对象进行可达性分析。 - 重新标记
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World
”。 - 并发清除
并发清除阶段清除对象。
3.优点与缺点
优点: 并发收集、低停顿。
缺点:
1. CMS收集器对CPU资源非常敏感;
2. CMS收集器无法处理浮动垃圾(CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生);
3. CMS收集器会产生大量空间碎片.
7. G1
收集器
特性
G1(Garbage-First)
是一款面向服务端应用的垃圾收集器。- 并行并发
- 分代收集
- 空间整合
- 可预测的停顿
运行原理
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。执行过程
- 初始标记(Initial Marking)
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。 - 并发标记(Concurrent Marking)
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。 - 最终标记(Final Marking)
最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。 - 筛选回收(Live Data Counting and Evacuation)
筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
- 初始标记(Initial Marking)
8. GC
总结
八、内存分配与回收策略
1.对象首先在Eden
区域分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够内存的时候,虚拟机发生一次Minor GC(新生代).
2. 大对象直接进入老年代
大对象指的是需要大量连续内存空间的对象。
-XX:PretenureSizeThreshold 使得大于设定值的对象直接进入老年代。
3.长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
4. 动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
5. 空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁,参见代码清单3-9,请读者在JDK 6 Update 24之前的版本中运行测试。