1. 一些概念

1.1 垃圾&垃圾收集

  • 垃圾:在 JVM 语境下,“垃圾”指的是死亡的对象所占据的堆空间。
  • 垃圾收集:所谓“垃圾收集”,就是将已分配出去、但不再使用的内存回收回来,以便能再次分配。

1.2 对象是否死亡

如何判断一个对象是否死亡(即不可能再被任何途径使用)?通常有以下两种方法:

1.2.1 引用计数法

引用计数法(Reference Counting):为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。当有地方引用它时,计数器加一;引用失效时减一。当某个对象的引用计数为零时,说明该对象已死亡,便可以被回收了。

  • 主要优点:原理简单,判定效率高。
  • 主要缺点:无法解决循环依赖的问题(对象之间相互循环引用)。

1.2.2 可达性分析算法

可达性分析(Reachability Analysis)的基本思路如下:

通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),若某个对象到 GC Roots 间没有任何引用链相连(或者用图论的话来说就是从 GC Roots 到这个对象不可达时)则证明此对象是不可能再被使用的。

示意图如下:

GC Roots 可理解为「堆外指向堆内的引用」。在 Java 技术体系中,固定可作为 GC Roots 的对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象(比如引用类型的静态变量)
  3. 方法区中常量引用的对象(比如字符串常量池的引用)
  4. 本地方法栈中 Native 方法引用的对象
  5. JVM 内部的引用(例如:基本数据类型的 Class 对象、常驻异常、系统类加载器)
  6. 同步锁(synchronized 关键字)持有的对象
  7. 反应 JVM 内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等

1.3 引用的分类

无论是引用计数法还是可达性分析算法,二者都离不开「引用」。JDK 1.2 之后,Java 中「引用」的概念得以扩充,主要分为以下四种:

  • 强引用(Strongly Reference)

    • 例如:Object obj = new Object()
    • 特点:无论任何情况,只要强引用存在,垃圾收集器永不回收被引用的对象
  • 软引用(Soft Reference)
    • 场景:用于一些还有用、但非必须的对象
    • 时机:被软引用关联的对象,在系统将发生 OOM 前,回收这些内存
    • 实现:java.lang.ref.SoftReference
  • 弱引用(Weak Reference)
    • 场景:非必须对象,比软引用更弱
    • 时机:被弱引用关联的对象只能生存到下一次垃圾收集发生(无论内存是否充足都会回收)
    • 实现:java.lang.ref.WeakReference
  • 虚引用(Phantom Reference)
    • 又称“幽灵引用”或“幻影引用”
    • 特点:最弱的引用,是否存在完全不会影响其生存时间,无法通过它获取对象实例
    • 唯一目的:该对象被回收时收到一个系统通知
    • 实现:java.lang.ref.PhantomReference

1.4 回收方法区

《Java 虚拟机规范》并未要求虚拟机在方法区实现垃圾收集。方法区垃圾收集“性价比”较低。

但在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 及 OSGi 这类频繁自定义类加载器的场景中,通常都需要 JVM 具备类型卸载的能力,以避免方法区内存压力过大。

方法区垃圾回收的主要内容包括:废弃的常量和不再使用的类型。它们的判定条件如下:

  • 废弃的常量

    • 无任何对象引用常量池中的常量
    • 虚拟机中无任何其他地方引用该字面量
  • 不再使用的类型
    • 该类所有的实例(包括子类)都已被回收
    • 该类的类加载器已被回收
    • 该类的 Class 对象任何地方未被引用,任何地方无法通过反射访问该类的方法

虚拟机参数:

# 是否对类型回收
-Xnoclassgc # 查看类加载和卸载
-verbose:class -XX:+TraceClassLoading -XX:+TraceClassUnLoading

2. 垃圾收集算法

从如何判定对象消亡的角度出发,垃圾收集器可分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,也称“直接垃圾收集”和“间接垃圾收集”。

这里的垃圾回收算法都属于“追踪式垃圾收集”的范畴。

2.0 分代收集理论

当代商业虚拟机的垃圾收集器,多数都遵循了“分代收集”(Generational Collection)的理论。

分代收集理论,实质是一套符合大多数程序运行实际情况的经验法则,有如下三条:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

根据以上几条规则,可以推测:

  • 若一个区域中大多数对象都是朝生夕灭,把它们集中在一起,每次回收只需关注如何保留少量存活的对象;
  • 若一个区域剩下的都是难以消亡的对象,把它们集中在一起,便可以较低的频率回收该区域。

如何解决跨代引用问题?

依据跨代引用假说,为了解决极少数跨代引用,只需在新生代建立一个“记忆集(Remembered Set)”,把老年代划分为若干小块,标识出哪一块内存会存在跨代引用,此后发生 Minor GC 时,只有包含跨代引用的小块内存中的对象才会被加入到 GC Roots 进行扫描(避免扫描整个老年代)。

一些 GC 概念:

  • 部分收集(Partial GC):目标不是完整收集整个 Java 堆的垃圾收集

    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。
    • 混合收集(Mixed GC):是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。
  • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

下面介绍常见的垃圾收集算法。

2.1 标记-清除算法

标记-清除(Mark-Sweep)算法:最早、最基础的算法,分为“标记”和“清除”两个阶段:

  1. 标记出所有需要回收的对象(或反之);
  2. 在标记完成后统一回收所有被标记的对象(或反之)。

后续的收集算法大多都是以“标记-清除”算法为基础,对其缺点进行改进而得。

该算法的示意图如下:

优缺点分析:

  • 主要优点:实现简单;
  • 主要缺点:
    • 执行效率不稳定,标记和清除过程效率都不高(标记对象较多时);
    • 内存空间碎片化问题(碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作)。

图片来自:http://www.cnblogs.com/dolphin0520/p/3783345.html

2.2 标记-复制算法

简称复制(Copying)算法。现在的商用 Java 虚拟机大都优先采用了这种收集算法回收新生代。

“半区复制”(Semispace Copying)算法将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当这一块用完时,就将还存活的对象复制到另外一块上,然后再把已使用的内存空间一次清理掉。

该算法主要为了解决“标记-清除”算法面对大量可回收对象时执行效率低的问题。

示意图如下:

“半区复制”算法的优缺点:

  • 优点:实现简单,运行高效且不易产生内存碎片;
  • 缺点:可用内存缩小为原来的一半,空间浪费太多。

一般不需要按照 1:1 的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 (即“浪费”了 10% 的新生代空间)。

由于无法保证每次每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(大多指老年代)进行分配担保(Handle Promotion),也就是直接进入老年代(相当于“兜底方案”)。

2.3 标记-整理算法

复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。

标记-整理(Mark-Compact)算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

标记-清除算法与标记-整理算法区别:前者是一种非移动式的回收算法,后者是移动式的。

示意图:

主要问题:

  • 移动存活对象:回收内存时会更复杂(Stop The World);
  • 不移动存活对象:分配内存时会更复杂(空间碎片问题)。

2.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法(不是一种具体的算法实现,可以理解为「组合模式」):根据对象存活周期的不同,将内存划分为几块。

一般把 Java 堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。

  • 新生代

每次垃圾收集时,都有大批对象死去,只有少量存活,采用复制算法(只需复制少量存活对象)。

  • 老年代

对象存活率较高,没有额外空间对它进行分配担保,必须使用“标记-清除”或“标记-整理”算法进行回收。

3. 垃圾收集器

前面的收集算法只是内存回收的方法论,而垃圾收集器才是内存回收的具体实现(可理解为“接口”与“实现类”的关系)。

3.1 Serial 收集器

Serial 收集器是最基础、历史最悠久的收集器。特点:

  1. 单线程收集,且垃圾收集时,必须暂停其他所有的工作线程(Stop The World, STW),直到它收集结束。
  2. HotSpot 虚拟机运行在 Client 模式下默认新生代收集器。
  3. 优于其他收集器的地方:简单而高效(与其他收集器的单线程比)。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

运行示意图如下:

3.2 ParNew 收集器

ParNew 收集器实质上是 Serial 收集器的多线程并行版本。

除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数(-XX:SurvivorRatio, -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World、对象分配原则、回收策略等都与 Serial 收集器完全一致。

运行示意图:

  • 使用/禁用该收集器的 VM 参数
# 注:JDK 9 取消了 -XX:+UseParNewGC 参数
-XX:+/-UseParNewGC

3.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器是新生代收集器,也是使用标记-复制算法实现的、并行收集的多线程收集器,也称“吞吐量优先收集器”。

与 ParNew 类似,但关注点不同:

  • CMS 等收集器:尽可能地缩短垃圾收集时用户线程的停顿时间;
  • Parallel Scavenge 收集器:达到一个可控的吞吐量(Throughput)。

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),即运行用户代码时间所占比重。

响应速度快能提升用户体验;而吞吐量高则能更高效地利用 CPU 资源,尽快完成程序的计算任务(主要适合在后台运算而不需要太多交互的任务)。

运行示意图如下:

  • 虚拟机参数
# 最大垃圾收集停顿时间(毫秒)
-XX:MaxGCPauseMillis # 设置吞吐量(0~100)
-XX:GCTimeRatio # 自适应调节策略
-XX:UseAdaptiveSizePolicy

3.4 Serial Old 收集器

Serial 收集器的老年代版本,单线程,使用“标记-整理”算法。主要用于客户端模式下的 HotSpot 虚拟机。

运行示意图如下:

3.5 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,使用多线程和“标记-整理”算法实现。

运行示意图如下:

在注重吞吐量或者处理器资源较为稀缺的场合,都可以考虑 Parallel Scavenge + Parallel Old 收集器的组合。

3.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以「获取最短回收停顿时间」为目标的收集器。

它基于“标记-清除”算法实现,运作过程分为四步:

  1. 初步标记(CMS initial mark):只标记 GC Roots 能直接关联到的对象,速度很快;
  2. 并发标记(CMS concurrent mark):从 GC Roots 遍历整个对象图,耗时较长,但无需停顿用户线程(可与用户线程并发执行);
  3. 重新标记(CMS remark):修正并发标记期间,因用户线程导致标记产生变动的标记记录;
  4. 并发清除(CMS concurrent sweep):清理删除标记阶段判断的已经死亡的对象,可与用户线程并发执行。

运行示意图如下:

目前很大一部分 Java 应用集中在互联网网站或者 B/S 系统的服务上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器非常符合这类应用的需求。

CMS 的主要优缺点

  • 主要优点:并发收集、低停顿
  • 主要缺点
    • 对处理器资源非常敏感,降低吞吐量。
    • 无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”失败而导致另一次完全 Stop The World 的 Full GC 的产生。
    • 内存空间碎片问题

浮动垃圾(Floating Garbage):在 CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程以后,CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这部分垃圾就是“浮动垃圾”。

  • 虚拟机参数
# 使用 CMS 收集器
-XX:+UseConcMarkSweepGC # 老年代使用空间的比例(需根据实际情况权衡)
-XX:CMSInitiatingOccupancyFraction=80 # Full GC 时开启内存碎片的合并整理
-XX:+UseCMSCompactAtFullCollection

3.7 Garbage First 收集器

Garbage First(G1) 收集器是垃圾收集器发展史上里程碑式的成果,开创了「面向局部收集」的设计思路和「基于 Region」的内存布局形式。

G1 收集器的定位是「CMS 收集器的替代者和继承人」。由于实现较复杂,后文另行分析。这里只做简单描述:

  • JDK 7 Update 40 时,Oracle 认为它达到了足够成熟的商用程度;
  • JDK 8 Update 40 时;G1 收集器提供了并发的类卸载支持,被 Oracle 称为“全功能的垃圾收集器(Fully-Featured Garbage Collector)”。

此外,还有一些更为先进的低延迟收集器,比如 OracleJDK 11 加入的 ZGC,RedHat 公司的 Shenandoah 收集器。另外,还有一个有点“奇葩”的 Epsilon 收集器,等等。

衡量垃圾收集器优劣的指标主要有三个:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)。此三者构成了一个「三元悖论」(类似分布式系统中的 CAP 原则),难以同时满足。

4. 小结

本文简要介绍了一些垃圾收集的相关概念,常用的垃圾收集算法以及经典的垃圾收集器。由于 G1 收集器实现稍复杂,因此后面单独分析。本文主要内容概括如下图:

JVM笔记-垃圾收集算法与垃圾收集器的更多相关文章

  1. JVM笔记3:Java垃圾收集算法与垃圾收集器

    当前商业虚拟机的垃圾收集都采用"分代收集"算法,即根据对象生命周期的不同,将内存划分几块,一般为新生代和老年代,不同的代根据其特点使用最合适的垃圾收集算法 一,标记-清除算法: 该 ...

  2. 深入理解Java虚拟机 - 垃圾收集算法与垃圾收集器

    1. 垃圾收集算法       JVM的垃圾收集算法在不同的JVM实现中有所不同,且在平时工作中一般不会深入到收集算法,因此只对算法做较为简单的介绍.       1.1 标记-清除算法        ...

  3. JVM之GC算法、垃圾收集算法——标记-清除算法、复制算法、标记-整理算法、分代收集算法

    标记-清除算法 此垃圾收集算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记对象,它的标记过程前面已经说过——如何判断对象是否存活/死去 死去的对象就会 ...

  4. JAVA 垃圾收集算法,垃圾收集器与内存分配策略(内容全面,解析简单易懂)

    垃圾收集器需要解决的三个问题: 1)哪些内存需要回收 2)什么时候回收 3)如何回收 背景:程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内不需要过多的考虑回收的问题,因 ...

  5. JVM垃圾回收算法 及 垃圾收集器

    摘自<深入理解Java虚拟机> 一.什么是: GC算法是 方法论,那么垃圾收集器就是具体的 实现. 二.四种 垃圾回收算法 1.标记-清除算法:最基础的收集算法:不足有两点:1标记和清除两 ...

  6. JVM 的GC算法和垃圾收集器

    1.标记清除算法 黑色部分代表可回收对象,灰色部分代表存活对象,绿色部分代表未使用的.最基础的收集算法就是标记清除算法如同他名字一样,算法分为"标记"和"清除" ...

  7. JVM探秘:垃圾收集算法

    本系列笔记主要基于<深入理解Java虚拟机:JVM高级特性与最佳实践 第2版>,是这本书的读书笔记. 垃圾收集算法 垃圾收集算法主要有标记-清除算法.复制算法.标记-整理算法.分代收集算法 ...

  8. jvm特性(3)( 收集算法和收集器的概念)

    java内存模型和线程规范 JVM高级特性与实践(三):垃圾收集算法 与 垃圾收集器实现 大致知识点如下: 4种垃圾收集算法概念的学习 7种垃圾收集器特征的学习 一. 垃圾收集算法 1. 标记-清除算 ...

  9. Java基础:JVM垃圾回收算法

    众所周知,Java的垃圾回收是不需要程序员去手动操控的,而是由JVM去完成.本文介绍JVM进行垃圾回收的各种算法. 1. 如何确定某个对象是垃圾 1.1. 引用计数法 1.2. 可达性分析 2. 典型 ...

随机推荐

  1. 基于ci框架 修改出来了一个带农历的万年历。

    1这里没有写model:代码一看就懂,没什么负杂地方,就是麻烦一点. 直接control模块的代码: <?php if ( ! defined('BASEPATH')) exit('No dir ...

  2. FPGA大疆考试准备内容

    1.寄存器与锁存器 锁存器:电平触发的存储单元,在有效电平时间里可以多次改变数据. 优点:    占触发器资源少,缺点是容易产生毛刺.(附上去毛刺的方法:格雷码计数器(*https://blog.cs ...

  3. elasticsearch用法

    基本原理 搜索引擎的索引 倒排序 由value查找key 数据库的索引 由key查找value 用于解决分库分表后的排序分页 like查找 性能问题 日志库的全文搜索 spring集成时使用的不是re ...

  4. 关闭”xx程序已停止工作”提示窗口

    运行注册表编辑器,依次定位到HKEY_CURRENT_USER\Software\Microsoft\Windows\WindowsError Reporting,在右侧窗口中找到并双击打开Donts ...

  5. openssl编译时!遇见的问题

    openssl编译: 1.编译静态库 ./config --prefix=/root/openssl/soft make 2.编译动态库 ./config --prefix=/root/openssl ...

  6. 编辑距离算法-DP问题

    Levenshtein Distance The Levenshtein distance is a string metric for measuring the difference betwee ...

  7. Leetcode 412.FizzBuzz

    题目描述 写一个程序,输出从 1 到 n 数字的字符串表示. 1. 如果 n 是3的倍数,输出"Fizz": 2. 如果 n 是5的倍数,输出"Buzz": 3 ...

  8. spring入门(14)

    AOP是一个新的专题,基础部分主要是入门 后续的五.六.七都属于AOP专题: 所以有必要对这三章要学什么有个全局的认识. 1 概要 1 什么是AOP及实现方式 介绍了AOP的用途,以及大致的实现方案 ...

  9. 2000字谏言,给那些想学Python的人,建议收藏后细看!

    1. 这几天陆续收到很多读者.球友的留言.私信,说要怎么学Python?有没有基础的,偏小白的学习方法?我的回答是:等我统一答复. 小胖从不食言,今天就来说说我觉得一个零基础.想转行.一直不得法的人应 ...

  10. 通过ELK快速搭建集中化日志平台

    ELK就是ElasticSearch + LogStash + Kibana 1.准备工作 ELK下载:https://www.elastic.co/downloads/ jdk version:1. ...