一、如何判断对象已死
1、引用计数算法
给每个对象添加一个引用计数器,每当一个地方引用他时,引用计数器加一,引用失效时,计数减一。这种方法存在问题,即循环引用的问题。
2、可达性算法分析
通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象到GC Roots没有任何引用链时,则证明对象不可用。
GC Roots的对象包括以下几种:
(1)虚拟机栈中引用的对象
(2)方法区中静态属性引用的对象
(3)方法区中的常量
(4)本地方法区中Native方法引用的对象
二、垃圾收集算法
1、标记-清除算法
2、复制算法
3、标记-整理算法
4、分代收集算法
三、垃圾收集器
新生代:Serial ParNew Parallel Scavenge
老年代:CMS Serial Old Parallel Old
包含所有的收集器:G1
1、Serial收集器
单线程收集器 新生代采取复制算法,老年代使用的是标记-整理法
2、ParaNew收集器
实际上是Serial的多线程版本。只有他能与CMS配合工作
3、parallel Scavenge收集器
关注点与其他垃圾收集器不同,其他垃圾收集器尽可能缩短垃圾收集时用户的暂停时间,Parallel Scavenge收集器关注的是可控制的吞吐量。即:
运行时用户代码时间/(用户代码时间+垃圾收集时间)提供两个参数用于精确控制吞吐量:-XX:MaxGCPauseMills(控制最大垃圾收集停顿时间)和-XX:GCTimeRatio(设置吞吐量大小)
4、Serial Old收集器
Serial收集器的老年代版本,使用标记整理法。主要是两大用途,一是配合Parallel Scavenge收集器使用,另一种用途是作为CMS的备选法案,这一部分在CMS中介绍。
5、Parallel Old收集器
Parallel Scavenge的老年代版本
6、CMS收集器(Concurrent Mark Sweep)
运作过程分为四个步骤
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
其中初始标记和重新标记两个步骤依然需要”stop the world“,初始标记仅仅是标记一下GC Roots能关联到的对象,速度很快。并发标记阶段就是进行GC Root Tracing的过程,重新标记是为了修正并发标记期间用户程序继续运作导致的标记产生变动的那一部分对象的标记记录。
缺点:
a:CMS对CPU资源非常敏感,CMS在并发阶段,虽然不会停止用户程序,但是也要占用CPU资源,占用的数量为(CPU数量+3)/4
b:CMS无法处理浮动垃圾。并发清除阶段用户程序还在运行,导致这时产生的垃圾无法被回收只能等下次回收。这也导致另外一个问题:CMS进行垃圾收集的时候需要预留空间,提供给在并发清理过程中用户程序使用。若预留的空间不够,会发生“Concurrent Mode Failure”,这时会用Srevial Old 收集器进行垃圾收集。
c:CMS是基于标记-清除算法的收集器,会产生内存碎片。解决方法,两个参数 -XX:+UseCMSCompactAtFullCollection:FullGC时开启内存碎片整理
-XX:CMSFullGCsBeforeCollection:设置执行多少次不压缩的Full GC后,跟着来一次带压缩的Full GC
7、G1收集器
特点:
(1)并行与并发:缩短用户程序停顿的时间
(2)分代收集:G1不需要其他的垃圾收集器就能管理整个GC堆
(3)空间整合:基于“标记-整理”算法,不会产生内存碎片
(4)可预测的停顿:可以建立可预测的停顿时间模型,能让使用者明确在一个长度为M毫秒的时间段内,消耗在垃圾收集的时间不超过N毫秒。
G1收集器Java堆的内存布局与其他垃圾收集器有很大不同,G1收集器将整个Java对划分为多个大小相等的独立区域(Region),虽然还有新生代和老年代的区分,但是它们都是Region的一部分。
G1能够建立起可预测的停顿模型就与Region有关。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region.
如何解决垃圾收集判断对象是否还有引用,会导致全表扫描的问题:使用Remember Set,每个Region里面都会有一个与之对应的remember Set,虚拟机发现程序有对R二分册对象类型的数据进行写操作时,会产生一个Write barrier暂时中断写操作,检查reference引用的对象是否处于不同的Region之中(在分代的例子中就检查老年代的对象是否引用了新生代的对象),如果是,就将相关信息记录到被引用对象所属Region的Remember Set中。当进行内存回收时,在GC Roots的枚举范围值中增加Remember set,即可保证不对全堆扫描也不会有遗漏。
G1收集器大概分为以下几个步骤:
(1)初始标记
(2)并发标记
(3)最终标记
(4)筛选回收
G1与CMS有很多相同之处:初始标记阶段只是标记一下GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start),使让下一阶段用户程序运行时,能够在正确可用的Region中创建对象。这个阶段需要停顿线程。并发标记是从GC Roots开始对堆中的对象进行可达性分析,找出存存活对象,耗时较长,但是可以并发执行。最终标记是为了修正上一阶段用户线程运行时导致标记变化的一部分记录。将这部分记录在线程Remember Set Logs里面。然后将Remember Set Logs这部分数据合并到Remember Set中。这段需要停顿线程,但是标记是并行执行的。最后筛选回收,首先对各个Region的回收价值和成本做排序,然后在根据用户期望的停顿时间进行垃圾回收。这个阶段其实是停顿用户线程的。
四、内存分配与回收策略
Java技术体系中所提倡的自动内存管理其实包括两个部分,如何对对象进行内存分配以及如何回收分配给对象的内存,以上讲了如何回收分配给对象的内存,下面讲讲如何给对象进行内存分配。
对象的内存分配,大方向上就是在堆上进行分配,对象主要是分配在新生代的Eden区中,如果启动了本地线程分配缓冲,将优先在TLAB上进行分配。少数情况直接分配在老年代中。
(1)对象优先在Eden区中进行分配,如果Eden区中空间不够,将触发一次young GC
(2)大对象直接分配在老年代中.Java虚拟机提供了一个-XX:PretenureSizeThreshold,令大于这个设置值的对象直接在老年代分配。
(3)长期存活的对象分配在老年代中。可通过-XX:maxTenuringThreshold设置,默认是15
(4)动态对象年龄的判定。如果在survivor空间中相同年龄所有对象的大小的总和大于survivor区的一半,年龄大于等于这个年龄的对象直接进入老年代。
参考文献:《深入理解Java虚拟机》 周志明 著