Jvm中垃圾回收算法(结合深入理解jvm第二版,第三版整理)
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)PDF分享
链接:https://pan.baidu.com/s/1R1kyn4bfQfQEVvOwS7UHeg
提取码:txdv
下面我们来详细说说这几种算法
1.标记-清除算法
它是最基础的收集算法, 是因为后续的收集算法大多都是以标记-清除算法为基础, 对其缺点进行改进而得到的。
过程:首先从GCROOT开始,向下扫描标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
即 扫描—标记----清除
缺点:第一个是标记和清除两个过程的效率不稳定,堆中包含大量对象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过程的执行效率都随对象数量增长而降低;
第二个是内存空间的碎片化问题
还有就是标记清除之后会产生大量不连续的内存碎片,这样在分配大对象的时候,无法找到足够的连续内存提前触发一次GC
2.复制算法
复制算法是为了解决标记-清除算法面对大量可回收对象时执行效率低
的问题
过程:复制算法将内存分为两个区域,一部分用来为新对象分配内存,另一部分用来在GC的时候将还在存活的对象复制过去,将对象复制过去后,直接将另一部分的区域清空;复制算法主要适用于存活对象少,大量短期对象的情况。(因为复制大量存活的对象很花费时间),主要使用于新生代的垃圾回收。
现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代, IBM公司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。 因此并不需要按照1∶ 1的比例来划分新生代的内存空间。
每次垃圾收集都发现有大批对象死去,只有少量存活,则使用复制算法;如果对堆内存有一定的了解的话,我们知道新生代内存被分为一个较大的Eden区和两个较小的Survivor区,每次只使用Eden区和一个Survivor区,当回收时将Eden区和Survivor还存活着的对象一次性的拷贝到另一个Survivor区上,最后清理掉Eden区和刚才使用过的Survivor区,Eden和Survivor的默认比例是8:1:1。当Survivor空间不足以容纳一次Minor GC之后存活的对象时, 就需要依赖其他内存区域(实际上大多就是老年代) 进行分配担保(Handle Promotion) 。
内存的分配担保也一样, 如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象, 这些对象便将通过分配担保机制直接进入老年代, 这对虚拟机来说就是安全的。
标记-复制算法在对象存活率较高时就要进行较多的复制操作, 效率将会降低。 更关键的是, 如果
不想浪费50%的空间, 就需要有额外的空间进行分配担保, 以应对被使用的内存中所有对象都100%存
活的极端情况, 所以在老年代一般不能直接选用这种算法。
3.标记—整理算法
标记—整理算法其实只是在优化后的标记清除算法,解决了标记—整理算法所带来的内存不规整,无法利用空闲区域来存放大对象,提前引起GC的问题。其实只是在标记—清除算法的基础上,多了一个整理内存的过程。
主要过程与标记-清除算法相同,都是先标记从GC Roots向下查询到的对象,然后对未标记的对象进行回收,但是不同的是标记-整理算法最后会将所有存活对象向到一端移动,减少了清除的过程带来的内存碎片。
4.可达性分析理论(算法)
在深入理解Jvm的第三版中对于可达性分析从算法上升到了理论层次,引入了三种假说
1) 弱分代假说(Weak Generational Hypothesis) : 绝大多数对象都是朝生夕灭的。
2) 强分代假说(Strong Generational Hypothesis) : 熬过越多次垃圾收集过程的对象就越难以消
亡。
其实对于1和2 来说,我们常用的垃圾收集器,在设计原则中收集器都将Jvm根据对象的生命周期将堆内存按"代"分为新生代和老年代,所有jvm可以在对象的不同生命周期可以根据实际情况使用不同的回收算法,这样有利于提高回收效率和成本
-在新生代区域中,每次垃圾收集都发现有大批对象死去,只有少量存活,则使用复制算法;如果对堆内存有一定的了解的话,我们知道新生代内存被分为一个较大的Eden区和两个较小的Survivor区,每次只使用Eden区和一个Survivor区,当回收时将Eden区和Survivor还存活着的对象一次性的拷贝到另一个Survivor区上,最后清理掉Eden区和刚才使用过的Survivor区,Eden和Survivor的默认比例是8:1:1。
- 老年代中因为对象存活率高、没有足够大的额外空间对存活的对象进行复制,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
3) 跨代引用假说( Intergenerational Reference Hypothesis) : 跨代引用相对于同代引用来说仅占极少数。
但是如果现在只进行一次新生代区域内的收集(Minor GC) , 但新生代中的对象是完全有可
能被老年代所引用的, 为了确认在老年代区域的引用对象是否存活, 就不得不在固定的GC Roots之外, 再额外遍历整个老年代中所有对象来,来确保可达性分析结果的正确性( 反过来也是一样) 遍历整个老年代所有对象
的方案虽然理论上可行, 但无疑会为内存回收带来很大的性能负担。
其实根据前两条假说我们可以逻辑推理一个推论: 存在互相引用关系的两个对象, 是应该同时生存或者同时消亡的。
如果某个新生代对象存在跨代引用, 由于老年代对象难以消亡, 该引用会使得新生代对象在收集时同样得以存活, 进而在年龄增长之后晋升到老年代中, 这时跨代引用也随即被消除了。
依据这条假说, 我们就不应再为了少量的跨代引用去扫描整个老年代, 也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用, 只需在新生代上建立一个全局的数据结构( 该结构被称为“记忆集”, Remembered Set) , 这个结构把老年代划分成若干小块, 标识出老年代的哪一块内存会存在跨代引用。 此后当发生Minor GC时, 只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。 虽然这种方法需要在对象改变引用关系( 如将自己或者某个属性赋值) 时维护记录数据的正确性, 会增加一些运行时的开销, 但比起收集时扫描整个老年代来说仍然是划算的。