Java垃圾回收机制

栏目: Java · 发布时间: 6年前

内容简介:Java垃圾回收机制

顾名思义,标记-清除算法分为两个阶段,标记(mark)和清除(sweep).

在标记阶段,collector从mutator根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。

而在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。

[图片上传失败...(image-8c300b-1510574775013)]

从上图我们可以看到,在Mark阶段,从根对象1可以访问到B对象,从B对象又可以访问到E对象,所以B,E对象都是可达的。同理,F,G,J,K也都是可达对象。到了Sweep阶段,所有非可达对象都会被collector回收。同时,Collector在进行标记和清除阶段时会将整个应用程序暂停(mutator),等待标记清除结束后才会恢复应用程序的运行。

缺点:

​ 标记-清除算法的比较大的缺点就是垃圾收集后有可能会造成大量的内存碎片,像上面的图片所示,垃圾收集后内存中存在三个内存碎片,假设一个方格代表1个单位的内存,如果有一个对象需要占用3个内存单位的话,那么就会导致Mutator一直处于暂停状态,而Collector一直在尝试进行垃圾收集,直到Out of Memory。

2. 标记-压缩算法(mark-compact)

​ 顾名思义,标记-压缩算法分为两个阶段,标记(mark)和压缩(compact).

​ 其中标记阶段跟标记-清除算法中的标记阶段是一样的,而对于压缩阶段,它的工作就是移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有 非可达对象释放出来的空闲内存 都集中在一起,通过这样的方式来达到减少内存碎片的目的。

Java垃圾回收机制

3. 复制算法(copying)

堆内存对半分为两个半区,只用其中一个半区来进行对象内存的分配,如果在这个半区内存不够给新的对象分配了,那么就开始进行垃圾收集,将这个半区中的所有可达对象都拷贝到另外一个半区中去,然后继续在另外那个半区进行新对象的内存分配。

Java垃圾回收机制

Java垃圾回收机制

缺点:

​ 存压缩为原来的一半,利用率比较低,典型的空间换时间

4. 引用计数算法(reference counting)

​ 通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。

​ 采用引用计数的垃圾收集机制跟前面三种垃圾收集机制最大的不同在于,垃圾收集的开销被分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。

注意:

  • 当某个对象的引用计数减为0时,collector需要递归遍历它所指向的所有域,将它所有域所指向的对象的引用计数都减一,然后才能回收当前对象。

  • 但是这种引用计数算法有一个比较大的问题,那就是它不能处理环形数据 - 即如果有两个对象相互引用,那么这两个对象就不能被回收,因为它们的引用计数始终为1。这也就是我们常说的“内存泄漏”问题。如下图:

    [图片上传失败...(image-56fb94-1510574775013)]

5. 分代收集算法

​ 当前的商业虚拟机都采用的是”分代收集“算法,一般是把 java 堆分成新生代和老生代,这样就可以根据各个年代的特点采用最适当的垃圾收集算法,新生代中,对象大多是”朝生夕死“可以采用复制算法,而老年代的对象存活率比较高,而且没有担保空间进行内存分配,就要采用”标记-清除算法“或者”标记-整理“算法。

## 二、Java垃圾回收

1. Java的内存分布

[图片上传失败...(image-5469f-1510574775013)]

其中,堆内存分为年轻代和年老代,非堆内存主要是Permanent区域,主要用于存储一些类的元数据,常量池等信息。而年轻代又分为两种,一种是Eden区域,另外一种是两个大小对等的Survivor区域。

2. Java年轻代垃圾回收机制

[图片上传失败...(image-3bdb3-1510574775013)]

​ 部分的新创建对象分配在新生代。因为大部分对象很快就会变得不可达,所以它们被分配在新生代,然后消失不再。当对象从新生代移除时,我们称之为"Minor GC"。 新生代使用的是复制收集算法

​ 新生代划分为三个部分:分别为Eden、Survivor from、Survivor to,大小比例为8:1:1(为了防止复制收集算法的浪费内存过大)。每次只使用Eden和其中的一块Survivor,回收时将存活的对象复制到另一块Survivor中,这样就只有10%的内存被浪费,但是如果存活的对象总大小超过了Survivor的大小,那么就把多出的对象放入老年代中。

在三个区域中有两个是Survivor区。对象在三个区域中的存活过程如下:

  1. 大多数新生对象都被分配在Eden区。
  2. 第一次GC过后Eden中还存活的对象被移到其中一个Survivor区。
  3. 再次GC过程中,Eden中还存活的对象会被移到之前已移入对象的Survivor区。
  4. 一旦该Survivor区域无空间可用时,还存活的对象会从当前Survivor区移到另一个空的Survivor区。而当前Survivor区就会再次置为空状态。
  5. 经过数次(默认是15次)在两个Survivor区域移动后还存活的对象最后会被移动到老年代。

如上所述,两个Survivor区域在任何时候必定有一个保持空白。如果同时有数据存在于两个Survivor区或者两个区域的的使用量都是0,则意味着你的系统可能出现了运行错误。

3. Java老年代垃圾回收机制

​ 存活在新生代中但未变为不可达的对象会被复制到老年代。一般来说老年代的内存空间比新生代大,所以在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,我们称之为 "Major GC"(或者Full GC)。 老年代使用标记-清理或标记-整理算法

空间分配担保

在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

  1. 如果大于,那么Minor GC可以确保是安全的。

  2. 如果小于,虚拟机会查看HandlePromotionFailure设置值是否允许担任失败。

    • 如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小
      • 如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的
      • 如果小于,进行一次Full GC
    • 如果不允许,也要改为进行一次Full GC

    ​ 前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时(最极端就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来,在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

    ​ 取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

2.Java垃圾收集器

Java垃圾回收机制
  • Serial收集器(Serial/Serial Old)

    Serial是一个单线程的收集器,但它的“单线程”意义并不仅仅说明它只会使用一个CPU或一条手机此案成去完成垃圾和收集工作,更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

    Java垃圾回收机制
  • ParNew收集器

    ParNew收集器其实就是Serial收集器的多线程版本。

    它是运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

    Java垃圾回收机制
  • Parallel Scavenge收集器

    ​ 该收集器也是一个新生代的垃圾收集器,他也是使用复制算法的收集器,又是一个并行的垃圾收集器。该收集器的特点是他的关注点与其他的收集器不同,CMS等收集器的关注点是尽可能缩短垃圾回收时用户线程的停顿时间,而parallel Scavenge收集器的目标是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间),比如虚拟机总共运行100分钟,垃圾回收占用了1分钟,那么吞吐量就是99%。

  • Parallel Old收集器

    Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

    Java垃圾回收机制
  • CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

    • 初始标记(CMS initial mark)
    • 并发标记(CMS concurrent mark)
    • 重新标记(CMS remark)
    • 并发清除(CMS concurrent sweep)

    其中,初始标记、重新标记这两个步骤仍然需要”Stop The world”。初始标记仅仅只是标记一下GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

    由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

    Java垃圾回收机制

    **CMS的优势:**并发收集、低停顿。

    CMS的缺点:

    • 对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量 + 3)/4,并发回收时垃圾收集线程所占CPU资源随着CPU数量的增加而下降,而且在CPU不足4个时,CMS对用户程序的影响就可能变得很大,导致执行速度降低。
    • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
    • CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片太多的时候,将会给大对象分配带来很大麻烦。
  • G1收集器

    G1是一款面向服务端应用的垃圾收集器。HOtSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。

    G1具备如下特点:

    • **并行与并发:**G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
    • **分代收集:**虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的就对象以获取更好的收集效果。
    • 空间整合 :G1从整体上来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。
    • 可预测的停顿 :这是G1相对于CMS的另一大优势。

    G1垃圾收集器和CMS垃圾收集器有几点不同。首先,最大的不同是内存的组织方式变了。Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region - 每个region从1M到32M不等。

    [图片上传失败...(image-f2affc-1510574775013)]

    一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。

    在G1垃圾收集器中,年轻代的垃圾回收过程跟PS垃圾收集器和CMS垃圾收集器差不多。

    [图片上传失败...(image-5a0154-1510574775013)]

    对于年老代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:

    1. Initial Mark阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了。

    2. Concurrent Mark阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,那就是,如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。

    3. Remark阶段 - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,能够在Remark阶段更快的标记可达对象。

    4. Clean up/Copy阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的,如下图所示:

      [图片上传失败...(image-ed332d-1510574775013)]

    从上可以看到,由于Initial Mark阶段和Clean up/Copy阶段都是跟minor gc同时发生的,相比于CMS,G1暂停应用程序的时间更少,从而提高了垃圾回收的效率。


以上所述就是小编给大家介绍的《Java垃圾回收机制》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏编程算法与技巧

游戏编程算法与技巧

【美】Sanjay Madhav / 刘瀚阳 / 电子工业出版社 / 2016-10 / 89

《游戏编程算法与技巧》介绍了大量今天在游戏行业中用到的算法与技术。《游戏编程算法与技巧》是为广大熟悉面向对象编程以及基础数据结构的游戏开发者所设计的。作者采用了一种独立于平台框架的方法来展示开发,包括2D 和3D 图形学、物理、人工智能、摄像机等多个方面的技术。《游戏编程算法与技巧》中内容几乎兼容所有游戏,无论这些游戏采用何种风格、开发语言和框架。 《游戏编程算法与技巧》的每个概念都是用C#......一起来看看 《游戏编程算法与技巧》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

html转js在线工具
html转js在线工具

html转js在线工具