JVM 学习笔记——垃圾回收

为什么要垃圾回收

 

随着程序的运行,内存中存在的变量、对象等信息占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至造成大量内存泄漏(Memory Leak),使得系统出现可用内存不足导致不必要的异常


如何判断对象是否可以回收

引用计数法

引用计数算法通过判断对象的引用数量来决定对象是否可以回收,该方法是垃圾回收的早期策略,在这种方法中,堆中的每个对象实例都有一个引用计数,代表该对象被引用的数量,任何引用计数为 0 的对象可以被当作垃圾回收

这种方法存在一个重大缺陷,即循环引用问题

对象1引用对象2,而对象2引用对象1,二者的引用计数均为 1,无法归零,故永远无法回收,代码示例如下

public class Demo {
    private Object ref;

    public static void main(String[] args) {
        Demo demo1 = new Demo();
        Demo demo2 = new Demo();
        demo1.ref = demo2;
        demo2.ref = demo1;
        demo1 = null;
        demo2 = null;
    }
}

可达性分析算法

可达性分析算法是目前 JVM 所使用的算法,该算法通过一系列的名为 “GC Roots” 的对象作为起始点进行搜索,如果在 “GC Roots” 和一个对象之间没有可达路径,则称该对象是不可达的

在Java中,可作为 GC Root 的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 Native 方法引用的对象
  • 被同步锁持有的对象

在可达性分析算法中被判定为不可达的对象,还要经历再次标记过程才能判定为可回收对象

第一次标记并进行一次筛选:对象没有重写 finalize() 方法或者已经执行过 finalize() 方法,则判定为可回收对象(筛选的本质是判断此对象是否有必要执行 finalize() 方法)
第二次标记:对象重写了 finalize() 方法或者 finalize() 方法没有被执行,则将此对象放置在 F-Queue 队列中,并在稍后由一个虚拟机自动建立、低优先级(优先级为 8)的 Finalizer 线程去执行 finalize() 方法(如果一个对象的 finalize 方法运行缓慢,将会导致队列后的其他对象永远等待,严重时将会导致系统崩溃),执行完毕后,会再次判断该对象是否可达,若仍然不可达,则判定为可回收对象,否则对象 “复活”(移出即将回收对象的集合)


Java 四种引用与引用队列

在 JDK1.2 之后,Java 对引用的概念进行了扩充,将引用分为了如下四种

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)

强引用

Java 默认的引用即为强引用,垃圾回收器永远不会回收被引用的对象,当内存不足时,JVM 直接抛出 OutOfMemoryError

Object obj = new Object();

软引用

软引用用来描述一些非必需但仍有用的对象,在内存足够的时候,软引用对象不会被回收,当内存不足时,系统会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出 OutOfMemoryError

在 JDK1.2 之后,用 java.lang.ref.SoftReference 类来表示软引用

SoftReference<String> sr = new SoftReference<>(new String("CacheData"));

弱引用

弱引用的强度比软引用要更低一些,无论内存是否足够,只要 JVM 开始垃圾回收,那些被弱引用的对象都会被回收

在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用

WeakReference<String> wr = new WeakReference<>(new String("CacheData"));

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收

在 JDK1.2 之后,用 java.lang.ref.PhantomReference 类来表示虚引用,虚引用必须要和 ReferenceQueue 引用队列一起使用

ReferenceQueue<String> rq = new ReferenceQueue<>();
PhantomReference<String> pr = new PhantomReference<>(new String("Phantom"), rq);

引用队列(ReferenceQueue)

引用队列(ReferenceQueue)可以配合软引用、弱引用和虚引用使用,当引用的对象将要被 JVM 回收时,会将引用本身加入到引用队列中

当引用的对象被回收后,可能需要对引用本身进行处理,如将 ArrayList 中已经为空的引用删去,此时需要使用引用队列

ReferenceQueue<String> rq = new ReferenceQueue<>();
SoftReference<String> sr = new SoftReference<>(new String("Soft"), rq);
WeakReference<String> wr = new WeakReference<>(new String("Weak"), rq);
PhantomReference<String> pr = new PhantomReference<>(new String("Phantom"), rq);
Reference<? extends String> ref = rq.poll();
System.out.println(ref);	//对软/弱/虚引用本身进行处理

垃圾回收算法

JVM 首先通过可达性分析算法判断对象是否为垃圾,在确定了哪些垃圾可以回收后,JVM 通过垃圾回收算法进行高效的垃圾回收,JVM 规范中没有对如何实现垃圾收集器做出明确的规定,故不同的 JVM 可能对垃圾回收有着不同的实现,下面介绍几种常见的垃圾回收算法

标记-清除算法(Mark-Sweep

标记-清除算法分为标记清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象

它存在两个缺点:

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

复制算法(Copying)

复制算法将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上,然后再将之前的那一块的内存空间一次性全部清空,之后分配内存就在另一块上分配,直到内存再次用完,循环这个步骤。

优点

  • 每次都是对其中的一块进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效

缺点

  • 内存代价过大,每次都要浪费一半的内存

标记-整理算法(Mark-Compact)

标记-整理算法与标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收垃圾对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新相应的指针。

标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但却解决了内存碎片的问题


分代垃圾回收机制

JVM 将堆内存划分为新生代老年代永久代(JDK1.7后逐渐移除),根据不同代的特点选择最适合的回收算法

新生代(Young)

新生代(Young)分为 Eden 区和 Suvivor 区,Suvivor 区又分为 From 区和 To 区,默认比例为8:1:1。划分的目的是因为 JVM 采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在 Eden 区分配(大对象除外,大对象直接进入老年代),当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次垃圾回收,称作 Minor GC

GC 开始时,对象只会存在于 Eden 区和 From 区,To 区是空的(作为保留区域),GC 进行时, Eden 区中所有存活的对象都会被复制到 To 区,而在 From 区中,仍存活的对象会根据它们的年龄决定去向,年龄达到阀值(默认为 15,新生代中的对象每经历一轮垃圾回收,年龄加 1,GC 分代年龄存储在对象的 header 中)的对象会被移到老年代中,没有达到阀值的对象会被复制到 To 区,接着清空 Eden 区和 From 区,到此,新生代中存活的对象都在 To 区。

接着, From 区和 To 区会进行交换,也就是上次 GC 清空的 From 区成为新的 To 区,上次的 To 区成为新的 From 区,以保证新的一轮 GC 中,To 区是空的,当 To 区没有足够的空间存放新生代的存活对象时,通过分配担保机制将新生代对象放入老年代中

老年代(Old)

老年代中的对象生命周期较长,存活率比较高,在老年代中进行 GC 的频率相对而言较低

当老年代内存不足、或是显式的调用 System.gc() 时触发一次 Full GC,也称 Major GC,

Full GC 的速度一般会比 Minor GC 慢 10 倍以上,Full GC 会伴随着至少一次 Minor GC,并对整个堆的垃圾对象进行回收

老年代的垃圾回收算法采用的是标记-整理算法标记-清除算法(根据垃圾回收器的选择不同而不同)


STW 机制

STW(Stop-The-World),是指在垃圾回收时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集器),native 代码可以执行,但不能与 JVM 交互


垃圾回收器

垃圾回收算法服务于垃圾回收器,目前 HotSpot 虚拟机用到的垃圾回收器如下图所示,只有两个回收器之间有连线时才能配合使用

串行

串行垃圾回收器在进行垃圾回收时,会触发 STW 机制,挂起 Java 应用程序的其他所有线程

串行垃圾回收器是为单线程环境而设计的,只使用一个线程去回收

Serial

新生代串行回收器

Serial Old

老年代串行回收器

并行

并行指多条垃圾回收器线程并行工作,也会造成 STW,适合 Server 模式以及多核 CPU 环境

并行垃圾回收器有三种,分别为 ParNew 、Parallel Scavenge 、Parallel Old

ParNew

Serial 的多线程版本,新生代回收器

默认开启的回收器线程数和 CPU 核数一样,通过如下参数控制并行的垃圾回收线程数

-XX:ParallelGCThreads=n

Parallel Scavenge

Parallel Scavenge 回收器也是一个并行的多线程新生代回收器,其目标是达到一个可控制的吞吐量

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

Parallel Scavenge 回收器提供了一个参数

-XX:+UseAdaptiveSizePolicy

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

Parallel Old

Parallel Scavenge 的老年代版本

响应时间优先

CMS(Concurrent Mark Sweep)

CMS(Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,工作在老年代,使用标记-清除算法

CMS 回收器的工作流程分为以下 4 个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下 GC Roots 直接引用到的对象,速度很快,需要 Stop The World
  • 并发标记(CMS concurrent mark):进行 GC Roots Tracing 的过程,在初始标记的基础上继续向下追溯标记,该阶段 Java 应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿,该阶段在整个过程中耗时最长
  • 重新标记(CMS remark):并发标记期间用户线程仍然在工作,可能对先前的标记有干扰,故需要重新标记进行标记修正,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要 Stop The World
  • 并发清除(CMS concurrent sweep)清理垃圾对象,这个阶段回收器线程和 Java 应用程序线程并发执行

优点

  • 并发回收、低停顿

缺点

  • CMS 对 CPU 资源非常敏感,在并发阶段,虽然不会导致用户停顿,但是会占用了一部分线程而导致应用程序变慢,总吞吐量降低
  • CMS 在并发清除后会产生内存碎片,导致尽管老年代空间足够,但是找不到足够的连续空间分配大对象而提前触发 Full GC
  • CMS收集器无法处理浮动垃圾(并发清理阶段用户线程仍在运行,可能会产生垃圾),若产生垃圾过多,可能出现并发失败(Concurrent Mode Failure)而导致另一次 Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS 将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行 GC 时释放这些之前未被回收的内存空间

相关参数

-XX:CMSInitiatingOccupancyFraction:考虑到浮动垃圾的产生,CMS 必须在 GC 时预留足够空间,而不能等到内存空间被完全填满时才进行 GC,可以通过该参数(默认92%)的值来调整 CMS GC 的触发阈值,该值设置得过低则会因频繁进行 CMS GC 导致应用程序吞吐量下降,设置得过高则会因并发失败(Concurrent Mode Failure)而频繁触发 Full GC 导致应用程序停顿

-XX:+UseCMSCompactAtFullCollection:用于指定在执行完 Full GC 后对内存空间进行整理,以避免内存碎片的产生。不过由于内存整理过程无法并发执行,所带来的问题就是停顿时间变得更长

-XX:CMSFullGCsBeforeCompaction:设置在执行多少次 Full GC 后对内存空间进行整理

G1(Garbage First)

设计思想

G1 是当今垃圾回收技术最前沿的成果之一,JDK7 加入 JVM 的回收器大家庭中,成为 HotSpot 重点发展的垃圾回收技术,JDK9 后成为默认的垃圾回收器

G1 是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量,面向服务端应用

G1 采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为 2048 个分区

Humongous:当分配的一个对象超过一半区域的大小时,这个对象就会被放入这个区域,这个区域属于老年代区域

G1 会执行一个并发的全局标记的阶段来去确定整个堆当中对象的存活情况,优先回收垃圾多的 Region ,这就是为什么这种垃圾回收器的方式称为 Garbage-First 的原因

步骤

初始标记(Initial Marking)

初始标记阶段仅仅标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 的值,让下一个阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这一阶段需要停顿线程,但是耗时很短

要达到 GC 与用户线程并发运行,必须要解决回收过程中新对象的分配,所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start)的指针,从 Region 区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围

并发标记(Concurrent Marking)

并发标记阶段是从 GC Root 开始对堆中的对象进行可达性分析,找出存活的对象,这一阶段耗时较长,但可与用户程序并发执行

最终标记(Final Marking)

最终标记阶段修正在并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录

筛选回收(Live Data Counting and Evacuation)

筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划

优点

  • 并行于并发:G1 能充分利用 CPU 多核环境下的硬件优势,来缩短 Stop-The-World 停顿时间。部分其他回收器原本需要停顿 Java 程序执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行
  • 分代收集:虽然 G1 可以不需要其他回收器配合就能独立管理整个堆,但还是保留了分代的概念,它采用不同的方式去处理新老对象以获取更好的回收效果
  • 空间整合:与 CMS 的标记-清理算法不同,G1 从整体来看是基于标记-整理算法,从局部来看是基于复制算法
  • 可预测的停顿:这是 G1 相对于 CMS 的一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个毫秒级的时间片段内

CMS 和 G1 的区别

使用范围不同

  • CMS 收集器是老年代的收集器,可以配合新生代的 Serial 和 ParNew 收集器一起使用
  • G1 收集器收集范围是老年代和新生代。不需要结合其他收集器使用

STW 的时间不同

  • CMS 收集器以最小的停顿时间为目标的收集器。
  • G1 收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)

垃圾碎片不同

  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片

  • G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值