深入理解Java虚拟机 - 第3章 垃圾收集器与内存分配策略

三、垃圾收集

程序计数器,虚拟机栈,本地方法栈不需要回收
Java堆,方法区需要回收内存

判断对象是否可以回收

1. 引用计数法

给对象添加一个引用计数器,每当一个地方引用他们,计数器就加一,引用失效就减一,任何时刻,计数器为0的对象是不能被使用的。Java 虚拟机不使用引用计数算法。

  • 优点:实现简单,判定效率高
  • 缺点: 解决不了相互循环引用的问题。
2. 可达性分析算法

通过一系列的GC Roots 的对象作为起点,从这些节点向下搜索,走过的路径称为引用链,当一个对象到GC Roots 对象没有引用链的相连时候就代表该对象需要被回收。(就是GC Roots 到这个对象不可达)这样就解决了相互引用的问题。
可作为GC Roots 的对象为:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(native方法)引用的对象
    在这里插入图片描述
3.引用类型

我们希望当内存足够的时候,有些对象可不不被回收继续保存,又对对象进行了分类:

  • 强引用:Object obj = new Object() 类似这样的,虚拟机不会回收
  • 软引用:描述一些有用但是还非必须的对象,在系统即将发生内存溢出的时候才会回收这部分对象,如果回收后还是没有足够的内存,会报内存溢出异常。JDK1.2后提供SoftReference类实现软引用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
  • 弱引用:与当前内存状况没关系,只能生存到下一次垃圾回收之前。WeakReference类实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
  • 虚引用:又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。PhantomReference类实现。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
4. finalize()

这是垃圾回收方法时调用的第一个方法。垃圾回收时会经历两次标记过程,如果垃圾回收的时候发现没有与GC Roots 相连接的引用链,会被第一次标记并筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有被覆盖到finalize() 或者finalize()没有已经被虚拟机执行过,那么虚拟机都会认为没有必要执行。如果有必要执行,那么会被放到一个F-queue 的队列中,会有一个低优先级的线程去执行finalize() ,执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

5.方法区的回收

方法区(虚拟机的永久代)回收两部分内容:废弃的常量,无用的类。
废弃的常量比如常量池一个“abc” 但是没有String 对象引用它,那么就会被回收。
无用的类需要满足三种条件才会被回收

  • 堆内没有该类的实例
  • 加载该类的ClassLoador 已经被回收
  • 该类的Class对象没有任何引用,无法通过反射访问该类的方法
    大量使用了反射,动态代理等框架需要虚拟机具备卸载类的功能

垃圾回收算法

1.标记-清除算法

在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。

在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。

在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。

缺点:

  • 标记清除阶段效率慢
  • 清除后造成大量不连续的空间,造成浪费
    在这里插入图片描述
2.复制算法

现代虚拟机采用这种算法,把内存分为一个 Eden 两个 Survivor ,Eden:Survivor = 8 : 1 每次回收时,把Eden 和一个Survivor 中存活的对象放入另一个Survivor 中,再清除原先的对象。
保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

在这里插入图片描述

1.标记-整理算法

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:

  • 不会产生内存碎片

缺点:

  • 需要移动大量对象,处理效率比较低
    在这里插入图片描述
1.分代收集算法

就是根据不同的内存区的特点采用不同的收集算法

在新生代,

  • 因为每次回收大量的对象,剩余很少,那么直接采用复制算法

老年代

  • 回收的对象比较少,剩余的比较多,采用标记-清除或者标记-整理算法。

HotSpot 算法实现

略…

垃圾收集器

在这里插入图片描述

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

  • 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
  • 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

新生代垃圾收集器

1. Serial 收集器

在这里插入图片描述

一个单线程的收集器,运行在Client模式下,回收算法是复制算法。
缺点是需要停顿用户线程。
优点是简单,高效(与其他单线程工作的收集器比较)。最多停顿100+ms

2. ParNew 收集器

在这里插入图片描述

这个是Serial 的多线程版本,主要用在Server 模式下,回收算法是复制算法。
优点是在新生代收集器中只有它能与 CMS 收集器配合使用。

3. Parallel Scavenge 收集器

新生代收集器,使用复制算法,也是多线程的收集器。

特点是主要关注的是吞吐量(用户代码运行时间 / 用户代码运行时间 + 垃圾回收时间),其他比如CMS关注的是停顿时间,吞吐量是提高CPU处理效率,高效率利用CPU时间,尽快完成运算任务,停顿时间短适用于那些于用户交互的程序,良好的反应速度提升用户体验。

缩短停顿时间是以牺牲吞吐量和新生代空间来换取的,如果设置过小,那么停顿的次数就会大大增加。导致吞吐量会减小。

可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

老年代垃圾收集器

1. Serial Old 收集器

是 Serial 收集器的老年代版本(单线程), 使用标记-整理算法,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
    在这里插入图片描述
2. Parallel Old 收集器

在这里插入图片描述

是 Parallel Scavenge 收集器的老年代版本。多线程,标记-整理算法,JDK1.6之后提供使用。

在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

3. CMS 收集器

在这里插入图片描述

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。目的是获取最短的停顿时间。
整个过程分为四个过程:

  • 初始标记 :仅仅是标记一下,标记GC Roots 可以到达的对象 需要 停止用户线程
  • 并发标记 :进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记 : 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。需要停顿。
  • 并发清除 : 不需要停顿。

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

缺点:

  • 对CPU资源比较敏感。每次申请默认启动的数量是(CPU数量+3)/ 4,当CPU在4个以上的时候,CPU占用率在25%以上,随着CPU增加而降低,当CPU为2个,占用50%。
  • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
4. G1 收集器

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。JDK 1.7出现

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
在这里插入图片描述

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记 :为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  • 筛选回收 :首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

具备如下特点:

  • 并行与并发
  • 分代收集
  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

四、内存分配

Minor GC 和 Full GC

  • Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存分配策略

1. 对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

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

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

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

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,对象在Survivor 每熬过一次Minor GC,年龄就加1,增加到一定年龄(默认15)则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

4. 动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5. 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

4. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

对象分配到死亡

在这里插入图片描述

参考:

《深入理解Java虚拟机》

https://cyc2018.github.io/CS-Notes/#/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA?id=minor-gc-%e5%92%8c-full-gc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值