一、可达性与引用
GCRoots:
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
强引用:最常见的引用
软引用:系统内存要溢出了回收
弱引用:对象未被引用就收集了
虚引用:被找到就直接收集掉
finalize方法:
宣告一个对象死亡至少有经历两次标记
第一次标记,判断他是否有必要finalize(),若对象未被覆盖finalize()方法或被虚拟机执行过了,则认为没必要执行;若有必要执行会被加入F-Queue队列中,GC稍后再标记一次。
每个对象finalize方法只能执行一次,因为自救只能救一次
二、垃圾收集算法与收集器
标记-清除
复制-清除
标记-整理
新生代空间大,GC后存活量少,所以多数用复制;老年代空间小,存活率高,没有分配担保,用标记
GC需要再某个一致性的快照(仿佛系统被冻结的时间点)进行分析,因此必须要做Stop The World。HotSpot中通过OopMap数据结构记录(待补完)
虚拟机在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程(Stop The World 所以叫STW),暂停后再找到“GC Roots”进行关系的组建,进而执行标记和清除。
这些特定的指令位置主要在:
1、循环的末尾
2、方法临返回前 / 调用方法的call指令后
3、可能抛异常的位置
收集器
年轻代:
a.串行 Serial,串行执行,Stop The World,JDK 1.3.1前唯一选择,单CPU甚至双CPU下比并行要好,
b. 并行 ParNew,并行执行,Stop The World
c. Parallel Scavenge,并行执行,通过参数 -XX:MaxGCPauseMillis控制最大停顿时间,-XX:GCTimeRatio控制吞吐量。但他并不能和CMS配合
老年代
d. Serial Old,作为CMS的后备,当CMS浮动垃圾太多而Concurrent Mode Failure时
e. Parallel Old
f. CMS :标记——清除,将垃圾回收的过程分为4个步骤
初始标记:Stop The World,仅标记可关联对象
1. 标记老年代中所有的GC Roots对象
2. 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)
ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数;
并发标记:耗时长,与用户线程一起跑
从“初始标记”阶段标记的对象开始找出所有存活的对象;
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理
这个阶段因为是并发的容易导致concurrent mode failure
预清理
通过参数CMSPrecleaningEnabled
选择关闭该阶段,默认启用,主要做两件事情:
处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(其实这里并非使用CardTable,而是一个类似的数据结构,叫ModUnionTalble),通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象(晋升到老年代的对象、原本就在老年代的对象)
可中断的预清理 AbortablePreclean
该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold
默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段
因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
在该阶段,主要循环的做两件事:
- 处理 From 和 To 区的对象,标记可达的老年代对象
- 和上一个阶段一样,扫描处理Dirty Card中的对象
重新标记:Stop The World
这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,并将对象放入幸存带或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间
并发清除:开始清除,耗时长,与用户线程一起跑,产生浮动垃圾
CMS问题:
对CPU敏感:CMS默认线程数是(CPU数 + 3)/4 ,就是4个以上时收集器有25%以上的cpu资源,当CPU不足四个时就会影响用户程序。有种增量式并发收集器i-CMS,通过抢占式来处理,使得垃圾收集时间更长,但对用户程序影响小点
浮动垃圾:并发清理时用户线程在跑,就可能有浮动垃圾,使得CMS不能像其他收集器那样老年代几乎满了再收集。1.5默认用了68%就会激活,1.6是92%,通过-XX:CMSInitiatingOccupancyFraction的值控制百分比。CMS运行期预留内存无法满足程序需要,就会Concurrent Mode Failure,就是启动预备的Serial Old,再不行就Full GC
内存碎片:因为标记清除导致内存碎片。CMS提供一个-XX:+UseCMSCompactAtFullConllection开关,决定FullGC前做不做压缩(STW),还有一个-XX:CMSFullGCsBeforeCompaction 决定多少次不压缩的Full GC后来一次压缩
G1 收集器
新的Garbage-First(G1)GC就回到了以copying为基础的算法上,把整个GC堆划分为许多小区域(region),通过每次GC只选择收集很少量region来控制移动对象带来的暂停时间。这样既能实现低延迟也不会受碎片化的影响
(待补充)
三、内存分配与回收策略
主要分配在Eden,如果启动了本地线程分配缓冲,按线程优先在TLAB上。
Eden没有足够空间使进行一次Minor GC
大对象直接进入老年代,长期存活的对象进入老年代。每个对象有一个Age计数器,Eden中经过一次GC仍然存活并能被Survivor容纳,移动到Survivor,年龄记为1,然后每次Minor GC对象加一,默认15次到老年代(参数 -XX:MaxTenuringThreshold)
注:年龄判断不是绝对的,Survivor空间中相同年龄所有对象大小大于Survivor的一半时,年龄大于等于这个的对象就直接进入老年代
空间分配担保
Minor GC前检查老年代最大可用连续空间十分大于新生代所有对象总空间,成立则绝对安全;
否则,查看虚拟机参数HandlePromotionFailure是否允许担保失败,如果允许,查询老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,大于则试一把Minor GC;若小于且不允许担保,则Full GC
四、问题总结
1、为什么有两个Survivor
设置两个Survivor区最大的好处就是解决了碎片化
为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
碎片化带来的风险是极大的,严重影响JAVA程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存。。。画面太美不敢看。。。这就好比我们爬山的时候,背包里所有东西紧挨着放,最后就可能省出一块完整的空间放相机。如果每件行李之间隔一点空隙乱放,很可能最后就要一路把相机挂在脖子上了。
那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。
2、Full GC的触发条件与优化
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
避免Full GC : https://blog.csdn.net/endlu/article/details/51144918
3、jdk 1.8 JVM的更新
https://www.cnblogs.com/sxdcgaq8080/p/7156227.html