Java程序在运行过程中会产生大量的对象,但是内存大小是有限的,如果光用而不释放,那内存迟早被耗尽。如C、C++程序,需要程序员手动释放内存,Java则不需要,是由垃圾回收器去自动回收。
垃圾回收器回收内存至少需要做两件事情:标记垃圾、回收垃圾。于是诞生了很多算法及垃圾回收器。
即判断JVM中的所有对象,哪些对象是存活的,哪些对象可回收的算法。
在对象中添加一个属性用于标记对象被引用的次数,每多一个其他对象引用,计数+1,当引用失效时,计数-1,如果计数=0,表示没有其他对象引用,就可以被回收。
这个算法无法解决循环依赖的问题。
通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系链向下搜索,如果某个对象无法被搜索到,则说明该对象无引用执行,可回收。相反,则对象处于存活状态,不可回收。
JVM中的实现是找到存活对象,未打标记的就是无用对象,GC时会回收。
哪些对象可以作为GC Root呢:
概念:
顾名思义,标记-清除算法分为两个阶段,标记(mark)和清除(sweep)。
标记:遍历所有的GC Roots,然后将所有的GC Roots可达的对象标记为存活的对象。
清除:清除的过程将遍历所有堆中的对象,将没有标记的对象全部清除。
图解:
对上图中的黄色部分进行垃圾回收,回收后的截图如下所示:
从图中可知,进行标记清理后,可用内存增加,但是清除垃圾后的内存地址不连接,出现垃圾碎片。
缺点:
1、执行效率不稳定,如果Java堆中包含大量对象,而且大部分是需要被回收的,这时必须记性大量标记及清除动作,导致标记和清除两个过程执行效率都随对象数量增长而降低。
2、内存空间碎片化的问题,标记、清除后会产生大量的不连续内存碎片,空间碎片太可能会导致当以后需要分配大对象时无法找到足够的连续内存二不得不提前触发另一次垃圾收集动作。
概念:
复制算法将内存分为两个区间,这两个区间是动态的,在任意一个时间点,所有分配的对象内存只能在其中一个区间(活动区间),另外一个区间就是空闲区间。
当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址一次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。这个时候空闲内存已经变成了活动区间,垃圾对象全部在原来的活动区间,清理掉垃圾对象,原活动区间就变成了空闲区间。
这种方式内存的代价太高,每次基本上都要浪费一半的内存。于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存是Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)。
图解:
优点:
1、很好地解决了“标记-清除”算法,内存布局混乱的缺点。
缺点:
1、浪费一半的内存。
2、假设对象存活率为100%,那么“标记-复制”算法的GC过程就是重复的把对象复制一遍,而且将所有的引用地址重置一遍。可以预见的复制所消耗的时间随着对象存活率达到一定程度将会变成灾难。所以“标记-复制”算法使用的场景是可以忍受只是用50%内存,对象存活率非常低
概念:
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
图解:
优点:
1、弥补了“标记-清除”算法,内存区域分散的缺点
2、弥补了“标记-复制”算法内存减半的代价
缺点:
1、效率不高,对于“标记-清除”而言多了整理工作。
当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下。具体如下:
根据各个年代的特点采取最适当的收集算法
1、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
2、老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
以下测试采用的是Serial加Serial Old收集器组合。
查看当前jdk默认额收集器使用以下语句。
java -XX:+PrintCommandLineFlags -version
测试代码
原文:https://www.cnblogs.com/gengaixue/p/13602322.html