JVM 原理之 GC 原理

本贴最后更新于 2201 天前,其中的信息可能已经水流花落

什么是 GC

GC(Garbage Collection)垃圾收集机制,这也是 Java 和 C/C++ 之间的主要区别之一。对于一个 Java 开发者,一般不需要专门编写内存回收和垃圾清理代码,对于内存泄漏和溢出的问题,不需要那么特别注意。
总的来说:GC 机制对于 JVM 中的内存进行标记,确定哪一些内存需要回收。再根据一些回收策略,自动进行回收内存,永不停息的保证 JVM 中的内存空间。这也就防止了内存泄漏和溢出问题了。

JVM 的内存分布

对于了解 GC 的原理,那么 JVM 的内存分布肯定是需要了解的。在 Java 运行的时候,JVM 管理的内存区域有以下几块:imagepng

私有内存区:

区域名称 特性
程序计数器 指示当前程序执行到了哪一行,执行 JAVA 方法时记录正在执行的虚拟机字节码指令地址;执行本地方法时,计数器值为 undefined
虚拟机栈 用于执行 JAVA 方法。栈帧存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。程序执行时栈帧入栈;执行完成后栈帧出栈
本地方法栈 用于执行本地方法,其它和虚拟机栈类似

共享内存区:

区域名称 特性
Java 堆 堆区是理解 Java GC 机制最重要的区域,没有之一。JVM 管理内存中,最大的一块。 堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例。
方法区 方法区是各个线程共享的区域,用于存储已经被虚拟机加载的 类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量静态变量、编译器即时编译的代码等。

Java 堆的分布

GC 主要针对堆内存,所以将堆内存进行详细阐述。
堆内存主要分为三块:新生代(Youn Generation)、老年代(Old Generation)、持久代(Permanent Generation)

三代的特点不同,造就了他们使用的 GC 算法不同:

  1. 新生代适合生命周期较短,快速创建和销毁的对象;
  2. 老年代适合生命周期较长的对象;
  3. 持久代在 Sun Hotpot 虚拟机中就是指方法区(有些 JVM 根本就没有持久代这一说法)。

imagepng

新生代

新生代(Youn Generation):大致分为 Eden 区和 Survivor 区,Survivor 区又分为大小相同的两部分:FromSpace 和 ToSpace。新建的对象都是从新生代分配内存,Eden 区不足的时候,会把存活的对象转移到 Survivor 区。当新生代进行垃圾回收时会出发 Minor GC(也称作 Youn GC)。Eden 占比 80%,两块 Survivor 占比 20%。

新生代的回收,引用 Copy 算法。回收过程大致如下:

  1. Eden 区第一次内存满之后,执行 MinorGC,清理消亡对象。之后将剩余存活对象复制到 From Survivor 区中(此时 To 区空白,两个 Survivor 总有一个空白)
  2. 第二次 Eden 区满之后,再次执行 MinorGC,清除 Eden 中消亡对象。并且将 From 中的消亡对象清除,将 Eden 和 From Survivor 中存活的对象 copy 到 To 区。之后 Eden 再满,From 和 To 区相互交换。
  3. 直到 To 区填满了,就将所有存活的对象移动到老年代。

注意: 若没有填满,每次 MinorGC 的时候,给存活对象标记 +1,根据--Xx:MaxTenuringThreshold(默认 15)。标记大于 1 的时候,同样移进老年代。

老年代

老年代进行垃圾回收的时候,成为 MajorGC/FullGC。
发生 Minor GC 时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次 Full GC。
否则,如果小于的话,JVM 就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败)。如果允许,则只会进行 MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行 Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发 MinorGC 就会同时触发 Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)

注意: FullGC 比 MinorGC 的速度慢 10 被以上。因为 FullGC 的时候,用户线程暂停,降低系统性能、吞吐量。

永久代(方法区)

永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。

对于无用的类进行回收,必须保证 3 点:

  1. 类的所有实例都已经被回收
  2. 加载类的 ClassLoader 已经被回收
  3. 类对象的 Class 对象没有被引用(即没有通过反射引用该类的地方)

JDK1.8 之前,这些数据保存于此。JDK8,将永久代从堆中取出,数据存在本地内存地区(堆外空间)。

回收算法概述

追踪回收算法(tracing collector)可达性分析算法

通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的。

如图:对象 5、6、7 就是不可达的,需要被回收
imagepng

按代回收算法(Generational Collector)

当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代老年代永久代. 这样就可以根据各年代特点分别采用最适当的 GC 算法:

  • 在新生代: 每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集.
  • 在老年代: 因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用**“标记—清理”“标记—整理”**算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.

注意: 除去按代回收,还有按区回收算法。

  • 分区收集
    上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分, 而分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间.
    在相同条件下, 堆空间越大, 一次 GC 耗时就越长, 从而产生的停顿也越长. 为了更好地控制 GC 产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿.

复制回收算法(Coping Collector) (新生代)

把堆均分成两个大小相同的区域,只使用其中的一个区域,直到该区域消耗完。此时垃圾回收器终端程序的执行,通过遍历把所有活动的对象复制到另一个区域,复制过程中它们是紧挨着布置的,这样也可以达到消除内存碎片的目的。复制结束后程序会继续运行,直到该区域被用完。
但是,这种方法有两个缺陷:

  1. 对于指定大小的堆,需要两倍大小的内存空间,
  2. 需要中断正在执行的程序,降低了执行效率

标记-清理算法 (老年代)

该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象。

该算法会有以下两个问题:
1. 效率问题: 标记和清除过程的效率都不高;
2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集.

标记-整理算法 (老年代)

标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

空间分配担保(Handle Promotion Failure)

在执行 Minor GC 前, VM 会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个 Survivor 作为轮换备份, 因此当出现大量对象在 Minor GC 后仍然存活的情况时, 就需要老年代进行分配担保, 让 Survivor 无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象.
但存活对象的大小在实际完成 GC 前是无法明确知道的。

因此 Minor GC 前, VM 会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行 Minor GC, 否则进行 Full GC(让老年代腾出更多空间)。

然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次 Minor GC 存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次 Full GC(让老年代腾出更多空间).

转载

此文章是站在各位大佬的肩膀上进行总结的,感谢。
http://www.importnew.com/23035.html
https://blog.csdn.net/antony9118/article/details/51375662
https://blog.csdn.net/anjoyandroid/article/details/78609971
http://baijiahao.baidu.com/s?id=1604308216748480477&wfr=spider&for=pc

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖
  • GC
    17 引用 • 45 回帖
  • 垃圾回收
    5 引用 • 1 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...