《深入理解Java虚拟机系列二》--- 垃圾回收算法(通俗易懂)
Posted 小样5411
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《深入理解Java虚拟机系列二》--- 垃圾回收算法(通俗易懂)相关的知识,希望对你有一定的参考价值。
目录
前言
本文对应《深入理解Java虚拟机》一书的第三章GC算法部分,这章主要讲的就是GC(Garbage Collection—垃圾收集),先讲GC算法,后面文章再讲各种垃圾收集器,垃圾收集就是回收内存,大部分对象创建后,很快就没用了,也形象称“朝生夕灭”,对象没作用了就要回收他,以便释放它占用的内存,这就是垃圾回收。下面我们来着重介绍它。
一、对象存活判定算法
首先回顾一下上一节讲过的,JVM内存区域一共分为五大部分:方法区,堆,JVM栈,本地方法栈,程序计数器,这五部分组成JVM内存区域(运行时数据区),其中虚拟机栈,本地方法栈,程序计数器是线程私有的,随着线程创建而创建,消亡而消亡,线程消亡,自然所占内存就释放了。而堆,方法区是线程共享的,垃圾收集器所关注的就是这部分内存该如何管理。
我们知道堆中存放着几乎所有的对象实例,对象创建后就在堆中分配内存,堆内存也是在内存区域中占比最多的,垃圾收集器回收堆中的对象是回收那些“死去”对象,死去指的是没有被其他任何对象引用,就是游离在堆内存中的对象,没有任何价值了。而判断对象是否为死去对象有两种算法:引用计数算法和可达性分析算法
1.1 引用计数法
算法描述:这个算法判断对象是否存活是在对象中添加一个引用计数器,每当有一个地方调用它时,计数器就+1,当引用失效时,计数器-1,若计数器为0,则表示不可能再被使用,也就是“死去对象”,那么就判定为需要回收。
存在的问题: 虽然这个算法再Python脚本语言,游戏脚本等领域都被采用,但是它存在一个致命的问题,那就是对象之间的循环引用问题,如果对象A引用了对象B,某时刻对象B又引用了对象A,这样的话,对象A和对象B的引用计数器永远不可能为0,那么这两个对象就永远无法被回收。所以我们目前常常用的算法是可达性分析算法
1.2 可达性分析算法
算法描述:该算法就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系,根据引用关系向下搜索,搜索过 程所走过的路径称为引用链(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 即从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
1.3 其他补充:回收方法区性价比低
上面说了垃圾回收是回收线程共享的堆内存和方法区内存,但其实几乎就是在堆内存回收,一是因为堆内存占绝大部分内存,里面创建了很多很多的对象实例,二是因为回收方法区垃圾 的“性价比”通常是十分低的:在Java堆中,尤其是在新生代中,对其进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收的判定条件过于苛刻,导致回收一次,往往回收不到什么东西,所以一般我们垃圾回收一般也都是针对堆。
二、垃圾收集算法(3种GC算法)
2.1 分代收集理论
讲具体算法之前要介绍一个非常经典、重要的理论—分带收集理论,这个理论建立在两个假说之上:
(1)弱分代假说:绝大多数对象都是朝生夕灭的(刚产生不久就会“死亡“,等待被回收)。
(2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消 亡。
根据这两条假说,设计者一般就把堆逻辑上分为两部分:新生代和永久代,这在上一节也说过,垃圾收集(后称GC)也是根据这两块区域进行收集,进而产生的常见收集类型有:
(1)新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
(2)老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。
(2)整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。一般如果整个堆内存都用完了,就会触发这种收集,这种收集会停止一切用户线程,完全就执行GC线程将堆和方法区完全充分收集一遍,这样的后果一般就是电脑卡死,什么都执行不了,什么都点不了,必须等它收集完毕,用户线程才恢复。
2.2 标记-清除算法
算法描述:该算法分标记和清除两步,首先标记出所有需要回收的对象,然后清除这些被标记的对象,这个算法是最基础的收集算法,因为后续大多GC算法都是改进其缺点得到的,那么它有哪些缺点呢?
缺点:
(1)执行效率不稳定
如果堆中有大量对象需要被回收,那么标记这些对象,再清除这些对象的执行效率都不高,执行效率随着对象数量的增长而降低。
(2)清除会产生大量空间碎片
空间碎片的意思就是会有很多不连续的空间,这样空间就是一片一片的,如果创建一个比较大的对象,需要分配比较大的连续内存时,那么可能找不到这种连续的大片内存,从而虚拟机以为内存满了,从而触发了Full GC(整堆收集),这样用户线程都会停下,体验是非常不好的。清除过程如下图
2.3 标记-复制算法
该算法常称为复制算法,它是为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。这种复制算法又称为”半区复制“,就是将内存分为大小相等的两块(假设为内存A和内存B),每次只使用其中一块,等其中一块内存满了,假设内存A满了,就标记出内存A中存活对象,然后将这块内存中的存活对象复制到另一块内存中(存活对象复制到内存B中),之后将这块已经用完的内存全部清理掉(内存A清理一遍),变为空。下面画图给大家再理解下,分为两个区实际上常常叫From区和To区
分为两个区后,因为From区不断创建对象,From区满了,于是会触发垃圾收集(采用标记-清除),用可达性分析算法判定存活对象,与GC Roots集有路径(引用链)表示存活对象,没有引用链的就是可回收的死去对象,将其标记(直观就是黄色的),标记后将这些存活对象复制到To区
复制过程如下,GC复制算法会将五个存活对象复制到To区,并且保证在To区内存空间上的连续性
然后GC将From区中垃圾对象清除,From区就为空了,复制算法基本就执行完毕
IBM公司做过一项专门研究,发现新生代对象有98%都熬不过第一轮垃圾收集,这也是为什么新生代对象”朝生夕灭“原因,因此商用Java虚拟机大多采用这种标记-复制算法去回收新生代,但它也存在问题,就是因为针对半区回收,那每次总有一个半区是空着的,这样内存空间利用率就直接减半。
优点:不会产生内存碎片,运行高效
缺点:一半空间永远是空的
问题:那能不能解决这个空间问题呢?
1989年,有人做了一个优化,现在的标记-复制一般都是指这个优化后的半区复制分代策略,做法是将新生代分为一块较大的Eden区(伊甸园区)和两块较小的Survivor区(幸存区),这两块常常称为幸存区From和幸存区To,默认Eden区和Survivor区为8:1,这样可用空间就是整个新生代的90%,比浪费一半空间好多了,每次分配内存只使用Eden和一块Survivor(一块叫Survivor From,另一块叫Survivor To),Eden和Survivor From满了后就会触发垃圾收集,会先标记存活对象,然后将这两部分的存活对象再复制到剩下的一块Survivor To中,然后清理原先的Eden+Survivor From中可回收对象,存活对象一般2%不到,所以剩10%空间是肯定够的,极端情况存活对象超过10%就不考虑了。
我们现在说的标记-复制算法一般都指上面这个过程,可以画图再理解一次
最后,From空间与To空间互换,保证To空间为空,之后From继续接收伊甸园区来的存活对象,重复上述过程,记住最后互换这个过程,目的是保证新对象一直会在From区分配,下次存新对象实例还是From区
复制算法最佳使用场景:对象存活度较低时,垃圾收集后存活少情况,即新生区
2.4 标记-整理算法
该算法就是先标记存活对象,然后让所有存活对象都向内存空间的一端移动,即整理存活对象到一端,再清除掉边界以外的内存。
标记-清除算法和标记-整理算法关键区别就在于一个是非移动式算法,一个是移动式算法,标记清除会有空间碎片,而移动式相当于压缩了一下,就不会产生空间碎片,图解如下
1、将存活的对象标记
2、整理(向一段移动存活的对象,清除边界外的内存)
还可结合标记清除算法,几次清除再压缩效果好,需要调,比如内存碎片到达一个量级再统一整理
但标记-整理算法在移动存活对象并更新引用这些对象的地方是极为负重费事的操作,并且这中移动操作必须暂停用户应用程序才能进行,这种停顿正常停几十或者几百毫秒,但长了可非常影响用户体验。移动虽然存在弊端,但是不移动又会有碎片,导致内存分配可能明明还剩很多空间(不连续),但是一个对象要求的连续空间都没有,从而认为没有空间了,就触发Full GC,暂停一切用户线程强制清理,所以就要选择,有些垃圾回收器就选择用标记整理算法,如关注吞吐量的Paraller Scavenge收集器,虽然会有停顿(移动造成),但可获得的吞吐量比较大,吞吐量就是处理资源的效率,也反应后台运算任务执行快不快。
下一文将讲解垃圾回收器(7种经典垃圾回收器+2种最新发展的垃圾回收器)
以上是关于《深入理解Java虚拟机系列二》--- 垃圾回收算法(通俗易懂)的主要内容,如果未能解决你的问题,请参考以下文章