美文网首页
Java 垃圾回收

Java 垃圾回收

作者: 淡季的风 | 来源:发表于2021-04-07 22:40 被阅读0次

GC: 程序运行的过程种, 需要在内存中为对象、变量等分配内存,当一个对象、变量不再被使用时候,就需要及时回收这部分占用的内存, 否则会发生内存泄漏, 这个内存回收的过程就是GC。

Java GC: 垃圾回收主要针对于堆和方法区进行,虚拟机栈、本地方法栈、程序计数器是线程私有, 随着线程的生命周期结束而结束, 因此不需要对这三个区域垃圾回收。

1、 判断对象是否可以被回收

1.1、 引用计数法

给对象添加一个引用计数, 当对象被引用一次, 引用计数+1, 引用失效时引用计数-1。 引用计数为0时对象可被回收。python语言主要采用引用计数法来实现垃圾标记。

优点:

  • 方式简单, 回收速度快

缺点:

  • 当两个对象出现循环引用的情况下,计数器永远不为0, 导致无法对他们无法回收。
  • 需要额外空间存储引用计数
  • 频繁更新引用计数降低了性能
public class ReferenceCountingGC {

   public Object instance = null;

   public static void main(String[] args) {
       ReferenceCountingGC objectA = new ReferenceCountingGC();
       ReferenceCountingGC objectB = new ReferenceCountingGC();
       objectA.instance = objectB;
       objectB.instance = objectA;
   }
}

1.2、 可达性分析算法

通过GC Roots作为搜索起始点, 能够达到的对像都是存活的, 不可达的对象可被回收。

image.png

优点

  • 解决了循环引用问题

Java虚拟机使用该算法来判断对象是否可被回收。Java中GC Roots一般包含以下内容:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

1.3、 方法区的回收

因为方法区主要存储永久代的对象, 而永久代对象的回收率比新生代低很多, 因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。

*在大量使用反射、动态代理、CGLib等ByteCode框架, 以及动态生成JSP等频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证内存不会溢出。*

类的卸载条件很多, 需要满足以下三个条件, 并且即时满足了也并一定会被卸载。

  • 该类所有的实例都已经被回收, 也就时堆中不存在任何改类的实例。
  • 加载该类的ClassLoader已被回收。
  • 该类的Class对象没有在任何地方被引用, 也就无法在任何地方通过反射访问该类的方法。

1.4、 finalize()

finalize()函数类似C++的析构函数, 用来做关闭外部资源等工作。但是try-finally等方式可以做的更好, 并且该方法运行代价及其高昂, 不确定性大,无法保证各个对象的调用顺序, 因此最好不要使用。

当一个对象被回收是,如果需要执行该对象的finalize() 方法, 那么就可以通过该方法让对象重新被引用, 从而实现自救。 自救只能进行一次, 如果回收的对象之前调用了finalize()方法自救, 后面回收时不会再调用finalize()方法。

2、引用类型

无论时通过引用计数方法判断对象的引用数量, 还是通过可达性分析方法判断对象是否可达, 判断对象是否可被回收都与引用有关。Java具有四种强度不同的引用类型。

2.1、强引用

被强引用关联的对象不会被回收。
使用new一个新对象的方式来创建强应用。

Object obj = new Object();

2.2、软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。

Object obj = new Object(); 
SoftReference<Object> sf = new SoftReference<Object>(obj); 
obj = null; // 使对象只被软引用关联

2.3、弱引用

被弱引用关联的对象一定会被回收, 也就是它无法存货到下一次垃圾回收前。
使用 WeakReference 类来实现弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

2.4、 虚引用

又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。

使用 PhantomReference 来实现虚引用。

Object obj = new Object(); 
PhantomReference<Object> pf = new PhantomReference<Object>(obj); 
obj = null;

3、 垃圾回收算法

3.1、 标记-清除

image.png

将存活的对象进行标记,然后将未标记的对象清除掉。

标记-清楚算法分为2个阶段标记清除2个阶段:

  • 标记阶段标记出所有需要清楚的对象
  • 清除阶段在标记完成后,统一回收所有需要回收的对象

适用场合

  • 存活对象比较多的情况下比较高效
  • 适用于老年代

不足

  • 标记和清除效率都不高
  • 会产生大量不连续的内存碎片, 导致无法给大对象分配内存。

适用场景:

3.2、 标记-整理

image.png

让存活的对象都向一端移动, 然后清理掉边界以外的内存区域。

标记-整理算法是在标记-清除算法的基础上做了一些优化。
首先也需要从GC Roots出发对所有对象进行依次标记,但之后,它并不是简单的将需要回收的对象回收, 而是将所有存活的对象压缩到内存的一端。 之后清理掉边界以外的内存空间。这样既避免了内存碎片, 又不需要2块相同的内存, 性价比比较高。

不足
适用场景

  • 适用于老年代垃圾回收
  • 避免内存碎片产生
  • 不需要将内存划分为2块

3.3、 复制

image.png

将内存划分为两块, 每次只使用其中一块, 当一块内存用完了就将存活的对象复制到另外一块内存, 然后再把使用过的内存区域清理一遍。

现代的商业虚拟机都使用这种收集算法来回收年轻代

适用场景

  • 存活对象比较少的情况
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 适用于年轻代(即新生代):基本大多数对象都是朝生夕死, 存活下来的比较少

不足

  • 需要一块空的内存空间
  • 只使用了内存的一半
  • 需要复制移动对象

3.4、 分代收集

现代的商业虚拟机普遍采用分代收集算法。它根据对象存活周期将内存划分为几块, 每块都采用不用的垃圾回收算法来回收。

一般情况下将堆区域划分为新生代老年代, 还有一块位于堆区域以外的永久代

  • 新生代存活率低, 采用复制算法。
  • 老年代存活率高, 采用标记-清除或者标记-整理算法。
image.png

4、垃圾回收机制

image.png

如上图所示, JVM 堆区域划分为Eden区、Survivor区、Tenured(Old)区。

4.1、Minor GC、Major GC、 Full GC

部分收集: 单独收集年轻代或者老年代。

  • Minor GC: 收集年轻代(Eden和Survisor区域)
  • Major GC: 收集老年代(Tenured区域)

整堆收集

  • Full GC: 同时收集年轻代和老年代

4.2、 内存分配策略

4.2.1、对象优先分配到Eden 区
大多数情况下, 对象分配到Eden 区, Eden区空间不够时, 执行Minor GC。

4.2.2、大对象直接进入老年代
大对象是指需要连续内存空间的对象, 比如很长的字符串和数组。
经常出现大对象会提起出发垃圾回收以获取足够的内存空间分配给大对象。

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

4.2.3、长期存活的对象进入老年代
为对象定义年龄计数器, 对象每移入Survivor区一次, 年龄增加一岁, 年龄增加到一定岁数移入老年代。

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

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

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

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

4.3、Full GC的触发条件

对于Minor GC触发条件非常简单, 当Eden区域满时,就会触发一次Minor GC。 而Full GC出发条件相对复杂, 有以下条件:

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

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

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

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

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

5、垃圾回收器

image.png

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

  • 单线程与多线程: 单线程指的时垃圾收集器只使用一个线程收集, 而多线程表示使用多个线程。
  • 串行与并行: 串行指的是垃圾收集器与用户程序交替执行, 这时用户程序会停顿;并行指的是垃圾收集器与用户程序同时执行。除了CMS和G1外, 其他的垃圾收集器都是串行执行。

5.1、Serial收集器

Serial翻译为串行, 表示它是串行执行的收集器。
Serial收集器是单线程执行,只会使用一个线程进行垃圾收集工作。

它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。

它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。

5.2、 ParNew 收集器

image.png

它是Serial收集器的多线程版本。
是Server模式下的首选新生代收集器, 除了性能原因外, 主要是因为它是除了Serial外唯一一个可以与CMS收集器配合的垃圾收集器。
默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

5.3、 Parallel Scavenge 收集器

与 ParNew 一样是多线程收集器。

其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。

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

5.4、Serial Old 收集器

是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:

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

5.5、 Parallel Old 收集器

image.png

是 Parallel Scavenge 收集器的老年代版本。

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

5.6、 CMS收集器

image.png

CMS(Concurrent Mark Sweep), Mark Sweep是指的标记-清除算法。分为以下四个流程:

  • 初始标记: 仅仅只是标记一下GC Roots可以直接关联到的对像, 速度很快, 需要停顿。
  • 并发标记:根据GC Roots进行可达性追踪, 它在整个回收过程耗时最长, 不需要停顿。
  • 重新标记:修正并发标记期间因为用户进程继续运作而导致标记产生变动的那一部分对象, 需要停顿。
  • 并发清除:不需要停顿。

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

优点:并发收集、低停顿

不足

  • 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的, CPU利用率不高。
  • 无法处理浮动垃圾, 可能出现Concurrent Mode Failure:
    1)浮动垃圾是指由于并发标记阶段由于用户进程继续工作而产生的垃圾, 这部分垃圾只能到下一次GC时才能回收。
    2) 由于浮动垃圾的存在, 不得不预留出来一部分内存, 意味着CMS收集器不能像其他收集器一样等到内存满的时候再回收。
    3) 如果预留的垃圾不够存放浮动垃圾, 就会发生Concurrent Mode Failure, 这时虚拟机将临时启用Serial Old收集器进行垃圾回收。
  • 标记-清除算法导致的空间碎片:由于标记-清除算法会导致空间碎片, 往往会出现老年代空间剩余, 但是无法找到足够连续空间来分配当前对象, 不得不提前一次触发Full GC。

5.7、 G1收集器

G1收集器时一款面向服务端应用的垃圾收集器, 在多CPU和大内存的场景下有很好的性能。 HotSpot 团队赋予它的使命是未来可以替换掉CSM收集器。

相比于CMS收集器, 它有以下特点:

  1. 空间整合: G1收集器采用并发-整理算法,不会产生内存碎片。分配大对象时不会因为找不到连续内存空间而触发下一次GC。
  2. 可预测停顿: 这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的各种垃圾收集器,收集的范围是整个新生代或者老年代,而G1不再是这样。使用G1收集器, Java堆的内存布局和其他收集器有很大区别, 它将整个Java堆划分为多个大小相等的内存区间(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代已经不物理隔离了, 他们都是一部分Region的集合。
其他

image.png
G1:
image.png

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

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

image.png

5.8、常用的收集器组合

新生代GC策略 老年老代GC策略 说明
组合1 Serial Serial Old Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
组合2 Serial CMS+Serial Old CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。
组合3 ParNew CMS 使用 -XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项 -XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
组合4 ParNew Serial Old 使用 -XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
组合5 Parallel Scavenge Serial Old Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
组合6 Parallel Scavenge Parallel Old Parallel Old是Serial Old的并行版本
组合7 G1GC G1GC -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启; -XX:MaxGCPauseMillis=50 #暂停时间目标; -XX:GCPauseIntervalMillis=200 #暂停间隔目标; -XX:+G1YoungGenSize=512m #年轻代大小; -XX:SurvivorRatio=6 #幸存区比例

6、参考文献

相关文章

网友评论

      本文标题:Java 垃圾回收

      本文链接:https://www.haomeiwen.com/subject/xnilektx.html