JVM 垃圾收集器与内存分配策略

JVM 垃圾收集器与内存分配策略

由JVM内存区域可知Java运行时内存的各个区域。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,当方法结束或者线程结束时,内存就会跟着被回收了。

而只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,所以Java堆方法区这两个区域内存的分配和回收时动态的,垃圾收集器也只关注这部分内存的管理。


一.对象存活判断

1.引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时候计数器值为零的对象就是不可能再被使用的。

  • 优点:实现简单,效率高
  • 缺点:无法解决对象相互循环引用的问题——会导致对象的引用虽然存在,但是已经不可能再被使用,却无法被回收。

2.可达性分析算法

通过一系列的称为”GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots不可达(也就是不存在引用链)的时候, 证明对象是不可用的。如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

image.png

在Java, 可作为GC Roots的对象包括:

  • 在虚拟机栈中引用的对象
  • 在方法区中类静态属性引用的对象
  • 在方法区中常量引用的对象
  • 在本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用
  • 所有被同步锁(synchronized关键字)持有的对象

二.如何回收(垃圾收集算法)

分代收集理论:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过多次垃圾收集的对象就越难以消亡。

Java堆划分为新生代和老生代两个区域。在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

1.标记-清除算法

分为标记和清除两个阶段先标记出需要回收的对象(可达性分析算法或者引用计数算法),在标记完成后统一回收所有被标记的对象。

缺点:

  • 效率问题,标记和清除效率都不高。
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。


    image.png

2.标记-复制算法

将可用内存划分为大小相等的两块,每次只使用其中的一块。当这块用完了,就将还存活的复制到另一块上,然后将已使用过的另一半内存一次性清除。

新生代中的对象98%都是朝生夕死的。将内存分为较大Eden和两个较小的survivor空间。每次使用Eden和其中一个survivor,回收时将存活的对象一次性地复制到另一块survivor中,再清理掉Eden和已用过的survivor。

HotSpot虚拟机Eden与Survivor默认的大小比例为8:1:1。即只让10%的新生代被浪费的,survivor空间不够时,需要依赖其他内存(老年代)进行分配担保,即让对象进入老年代。

优点:

  • 对整个半区进行回收,不会出现空间碎片。
  • 如果内存中多数对象都是可回收,就只需复制少数的存活对象。

缺点:

  • 如果内存中多数对象都是存活的,将产生大量的内存间复制的开销。
  • 可用内存缩减了一半,造成空间浪费。
image.png

3.标记-整理算法

复制在对象存活率较高时效率很低。根据老年代的特点提出该算法。标记过程同标记清除一样,但不是直接对可回收对象进行清理,而是让存活对象朝着一端移动,然后直接清理掉边界以外的内存。

标记-整理与标记-清除的差异在于整理是移动式的回收算法,清除是非移动的。

image.png

根据各年代特点分别采用最适当的GC算法。

在新生代中每次垃圾收集都能发现大批对象已死, 只有少量存活,因此选用标记-复制算法, 只需要复制少量存活对象就可以完成收集。

在老年代因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用标记—清理标记—整理算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。即:

  • 新生代:存活率低,使用复制算法
  • 老年代:存活率高,使用“标记-整理”或“标记-清除”算法

三.垃圾收集器

  • 新生代:Serial收集器  ParNew收集器  Parallel Scavenge收集器
  • 老年代:Serial Old收集器  Parallel Old收集器  CMS收集器
image.png

新生代:

1.Serial收集器

标记复制。单线程收集器,只会使用一个处理器或一条收集线程进行垃圾收集,而且在垃圾收集时,必须暂停其他所有工作线程,直到收集结束。优点:简单高效。

image.png

2.ParNew收集器

标记复制。是Serial收集器的多线程并行版本,同时使用多条线程进行垃圾收集,其余与Serial一样。目前唯一能与CMS收集器配合工作。

image.png

3.Parallel Scavenge收集器

标记复制。并行收集的多线程收集器。CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而PS收集器的目的则是达到一个可控制的吞吐量。吞吐量即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间))。

image.png

老年代:

4.Serial Old收集器

标记整理。Serial 收集器的老年代版本,单线程收集器。

5.Parallel Old收集器

标记整理。Parallel Old是Parallel Scavenge的老年代版本,多线程收集器。

6.CMS收集器(Concurrent Mark Sweep并发标记清除)

标记清除。

并发收集,回收停顿时间短。

步骤:

  1. 初始标记:停掉用户其他线程,仅标记GCRoots能直接关联到的对象,速度很快。
  2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,耗时长但不需要停顿用户线程,与垃圾收集线程一起并发执行。
  3. 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。耗时比初始标记长一点,但远低于并发标记。
  4. 并发清除:清理删除掉那些标记阶段判断为死亡的对象,因为标记清楚算法不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

总体而言,CMS收集器的内存回收过程是与用户线程一起并发执行的。

image.png

缺点:

  • CMS对处理器资源非常敏感。
  • CMS无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。
  • CMS是标记清除,会产生大量碎片空间,对大对象内存分配带来麻烦。

7.G1收集器(Garbage First)

与其他收集器不同,G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新胜达的Eden空间、Survivor空间,或者老年代空间。收集器对不同角色的Region采用不同的策略去处理。优先处理回收价值收益最大的那些Region,也就是Garbage First的由来。

  • 从整体来看:“标记-整理” 算法
  • 从局部(两个Region之间)来看:“复制”算法

四.内存分配与回收策略

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:

image.png

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的。

  • 新生代GC(Minor GC):发生在新生代的垃圾收集动作,非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动作,一般会伴随Minor GC 速度一般比Minor GC慢上10倍以上。

对象的内存分配从大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况下也有可能直接分配在老年代中,分配的规则并非百分之百固定的,细节取决于当前使用的是哪一种垃圾收集器的组合,还有虚拟机中与内存相关的参数设置。

1.对象优先分配在Eden

大多数情况下,对象在新生代的Eden区分配,当Eden区没有足够空间时,虚拟机将发起一次Minor GC。

2.大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来“安置”它们。

3.长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳,将被移动到Survivor空间中,并且对象年龄为1.对象在Survivor区每“熬过”一次Minor GC,年龄就增加一岁,当年龄增加到一定程度(默认为15岁),就将被晋升到老年代中。对象晋升老年代年龄的阈值,可以通过参数设置。

4.动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到要求的年龄

5.空间分配担保

发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果成立,那么Minor GC确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管是有风险的,如果小于或者设置不允许冒险,那这时也要进行一次Full GC。

你可能感兴趣的:(JVM 垃圾收集器与内存分配策略)