温馨提示:这篇文章已超过442天没有更新,请注意相关的内容是否还可用!
JVM垃圾回收(GC)是一种自动管理内存的机制,旨在自动识别和清理不再使用的对象,以释放内存空间。GC通过识别无引用或不可达对象,进行内存回收,避免内存泄漏和性能下降。它是Java语言的重要特性之一,有助于提高应用程序的效率和稳定性。
目录
0垃圾的定义
1.什么是垃圾
2.如何定位垃圾对象
引用计数法:
根可达算法:
什么对象是根对象?
线程变量:
静态变量:
常量池:
JNI指针 :
1.GC 简介
1.1. 引言
1.2. 何为 GC
1.2.1. 手动 GC
1.2.2. 自动 GC
引用计数法
可达性分析
2.GC入门分析
2.1.碎片整理
1)对象创建时,执行写入操作越来越耗时
2)内存分配错误
2.2. 分代设想
2.3. 对象分配
对象内存分配过程
2.4. GC 模式分析
3.GC 算法基础
3.1. 标记可达对象
3.2. 移除不可达对象
标记-清除(Mark-Sweep)
标记-清除-整理(Mark-Sweep-Compact)
标记-复制 (Mark and Copy)
4.GC 算法实现
4.1. GC 算法实现简介
4.2. GC 收集器应用分析
4.2.1. Serial 收集器应用分析
4.2.2. Parallel 收集器应用分析
4.2.3. CMS 收集器应用分析
4.2.4. G1 收集器应用分析
0垃圾的定义
1.什么是垃圾
没有任何引用指向的一个对象或者多个对象
2.如何定位垃圾对象
引用计数法:
对于某个对象而言,只要应用程序中持有对该对象的引用,就说明该对象不是垃圾,否则,如果一个对象都没有引用该对象,认为该对象就是垃圾。
弊端:如果A,B两个对象相互持有对方的引用,而没有其他对象来持有A,B的引用,也就是说这来那个对象本质上是垃圾,但是永远不能被回收。
根可达算法:
通过GC的root对象,开始时乡下寻找,看某个对象是否可达,遍历到的对象,就不是垃圾,否则就是垃圾。
什么对象是根对象?
Java 程序运行时会启动一个线程栈,栈里会有main方法里边的马上调用执行的这些开对象,被称为根对象。从类型上分,主要有 线程栈变量,静态变量,常量池,JNI指针。
是不是根对象,首先得看是不是main栈帧里边的对象。
线程变量:
main线程中用到的线程变量
静态变量:
class load到内存够厚哦,马上会对静态变量赋默认值,初始化,所以main线程中的静态变量算根对象。
常量池:
main方法中用到的其他类变量,这些个对象的创建要去常量池中看他的类结构,所以这些个对象也算一个。
JNI指针 :
main方法 中如果用到C++\C写的本地方法,这些本地方法用到的类或者对象,也是根对象
1.GC 简介
1.1. 引言
在理解 GC 之前,先回顾一下 JVM 体系结构,如下图所示:
1.2. 何为 GC
基于正在使用的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是 垃圾对象,然后基于特定算法进行回收,这个过程称之为 GC ( Garbage Collection ). 所有的 GC 系统可从如下几个方面进行实现: 1.GC 判断策略(例如引用计数,对象可达性分析) 2.GC 收集算法(标记 - 清除,标记 - 清除 - 整理,标记 - 复制 - 清除) 3.GC 收集器 ( 例如 Serial,Parallel,CMS,G1) 为何要学习 GC 呢? 深入理解 GC 的工作机制,可以帮你写出更好的 Java 应用,同时也是进军大规模应用开发 的一个前提。1.2.1. 手动 GC
手动 GC 即显式地进行内存分配 (allocate) 和内存释放 (free) 。如果忘记释放, 则对应的那块内存不能再次使用。内存一直被占着 , 却不能使用,这种情况就称 为内存泄漏 (memory leak) 。所谓站着茅坑bls。 这是一个使用手动内存管理用 C 编写的简单示例:int send_request() { size_t n = read_size(); int *elements = malloc(n * sizeof(int)); if(read_elements(n, elements) 手动 GC 时忘记释放内存是相当容易的。这样会直接导致内存泄漏。1.2.2. 自动 GC
自动 GC 一般是在 JVM 系统内存不足时,由 JVM 系统启动 GC 过程,自动对内存 进行垃圾回收。引用计数法
其中: 1) 绿色云朵是内存中的根对象,表示程序中正在使用的对象。 2) 蓝色圆圈是内存中的活动对象,其中的数字表示其引用计数。 3) 灰色圆圈是内存中没有活动对象引用的对象,表示非活动对象。 对于引用计数法,有一个很大的缺陷就是循环引用,例如:
其中红色对象实际上是应用程序不使用的垃圾。但由于引用计数的限制,仍然存 在内存泄漏。当然也有一些办法来应对这种情况 , 例如 “弱引用” ( ‘ weak’ references) 或者使用其它的算法来排查循环引用等。
可达性分析
可达性分析就是对可达对象进行标记,不可达对象即认为是垃圾,然后进行清除。 除通常有两个步骤: 1) 标记所有可到达的对象 ,此时内存中所有的对象都会被扫描一遍,标记处存活对象,从而能确定出垃圾。比较耗时 2) 清除不可到达对象占用的内存地址。此方法解决了循环依赖问题,但存在短时间的线程暂停 ,我们一般称这种现象为STW 停顿 (Stop The World pause, 全线暂停 ) 。 说明: JVM 中包含了多种 GC 算法收集器 , 他们虽在实现上略有不同 , 但理论上都采用了以上两个步骤。
2.GC入门分析
2.1.碎片整理
系统 GC 时每次执行清除 (sweeping) 操作, JVM 都必须保证“不可达对象“占 用的内存能被回收然后重用。内存是被回收了,但这有可能会产生大量的内存碎 片 ( 类似于磁盘碎片 ), 进而引发两个问题 :1)对象创建时,执行写入操作越来越耗时
因为寻找一块足够大的空闲内存会 变得更加麻烦。2)内存分配错误
对象创建时 , JVM 需要在连续的内存块中为对象分配内存。如果碎片问题很 严重 , 直至没有空闲片段能存放新创建的对象 ,就会发生内存分配错误 (allocation error) 。 为了解决碎片问题, JVM 在启动 GC 执行垃圾收集的过程中 , 不仅仅是标记和清 除 , 还需要执行 “内存碎片整理”。这个过程会让所有可达对象 (reachable objects) 进行依次移动 , 进而可以消除 ( 或减少 )内存碎片,并为新对象提供更大 并且连续的内存空间。示意图如下说明:内存整理时会将对象移动到靠近内存地址的起始位置。
2.2. 分代设想
我们知道垃圾收集要停止整个应用程序的运行,那么假如这个收集过程需要的时 间很长,就会对应用程序产生很大性能问题,如何解决这个问题呢?通过实验发 现内存中的对象通常可以将其分为两大类: 1) 存活时间较短 ( 这样的对象比较多 ) 2) 存活时间较长 ( 这样的对象比较少 ) 基于对如上问题的分析,科学家提出了分代回收思路,将 VM 中内存分为年轻代 (Young Generation) 和老年代 (Old Generation-老年代有时候也称为年老区 (Tenured)分代设想将内存拆分为两个可单独清理的区域,允许采用不同的算法来大幅提高 GC 的性能。但这种方法也不是没有问题。例如,在不同分代中的对象可能会互 相引用 , 这样的对象就难以回收。
2.3. 对象分配
JAVA 中堆内存的内存结构如下图所示:
对象内存分配过程
基于此内存架构,对象内存分配过程如下: 1 )编译器通过逃逸分析( JDK8 已默认开启),确定对象是在栈上分配还是在堆上分配。 2 )如果是在堆上分配,则首先检测是否可在 TLAB ( Thread Local Allocation Buffer ) 上直接分配。 3 )如果 TLAB 上无法直接分配则在 Eden 加锁区进行分配 ( 线程共享区 ) 。 4 )如果 Eden 区无法存储对象,则执行 Yong GC ( Minor Collection )。 5 )如果 Yong GC 之后 Eden 区仍然不足以存储对象,则直接分配在老年代。 说明:在对象创建时可能会触发 Yong GC ,此 GC 过程的简易原理图分析如下 :其中: 1) 新生代由 Eden 区和两个幸存区构成 ( 假定为 s1,s2), 任意时刻至少有一个 幸存区是空的 (empty) ,用于存放下次 GC 时未被收集的对象。 2)GC 触发时 Eden 区所有 ” 可达对象 ” 会被复制到一个幸存区,假设为 s1,当幸 存区 s1 无法存储这些对象时会直接复制到老年代。 3)GC 再次触发时 Eden 区和 s1 幸存区中的 ” 可达对象 ”会被复制到另一个幸存区 s2 ,同时清空 eden 区和 s1 幸存区 . 4)GC 再次触发时 Eden 区和 s2 幸存区中的 ” 可达对象 ”会被复制到另一个幸存区 s1 ,同时清空 eden 区和 s2 幸存区 . 依次类推。 5) 当多次 GC 过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参 数 -XX:+MaxTenuringThreshold 来指定上限,默认 15),会被看成是“年 老”的对象然后直接移动到老年代。
备注: 什么样的对象在栈上分配? 满足: 1.线程私有的小对象 2.没有逃逸: 线程私有,其他线程没有调用该对象 3.支持标量替换:用对象中的普通属性代替整个对象,如类 T中就两个成员变量 int a ;int b ; 那么完全可以用这两个属性代替T这个类;没必要在构建T这个对象; 4.无需调整 什么对应会进行线程本地分配TLAB(Thread local allocation buffer)? 设置TLAB的背景: 因为eden是多线程进程的场所,所以就会存在多线程间的同步问题,效率就会相对降低,为了提高效率,产生了TLAB. TLAB: 1.每个线程默认可以在 eden中取1%的空间,这个空间是该线程独有的 2.每个线程再分配对象时,优先在该线程独有的这块空间分配,这里不会和其他线程产生争抢,所以提高了效率 3.一般无需调整这块的大小
2.4. GC 模式分析
垃圾收集事件 (Garbage Collection events) 通常分为 : 1) Minor GC (小型 GC ):年轻代 GC 事件, ( 新对象 ) 分配频率越高, Minor GC 的频率就越高 2) Major GC ( 大型 GC): 老年代 GC 事件 3) Full GC (完全 GC ) : 整个堆的 GC 事件 说明:一般情况下可以将 Major GC 与 Full GC 看成是同一种 GC 。3.GC 算法基础
3.1. 标记可达对象
现在的 GC 算法,基本都是要从标记 ” 可达对象 ” 开始(Marking Reachable Objects),这些标记为可达的对象即为存活对象。同时我们可以将查找可达对象 时的起始位置对象,认为是根对象( Garbage Collection Roots )。 基于根对象标记可访问或可达对象,对于不可达对象, GC 会认为是垃圾对象。这个过程要遍历所有的对象,比较耗时。 例如:在下面图示 中的绿色云朵为根对象,蓝色圆圈为可达对象,灰色圆圈为垃圾对象。首先,GC 遍历(traverses)内存中整体的对象关系图(object graph)确定根对象,那什么样的对象可作为根对象呢?GC 规范中指出根对象可以是:
1) 栈中变量直接引用的对象 2) 常量池中引用的对象 3) … 其次,确定了根对象以后,进而从根对象开始进行依赖查找,所有可访问到的对 象都认为是存活对象,然后进行标记( mark )。 说明:标记可达对象需要暂停所有应用线程 , 以确定对象的引用关系。其暂停的 时间 , 与堆内存大小、对象的总数没有直接关系 , 而是由存活对象 (aliveobjects) 的数量来决定。3.2. 移除不可达对象
移除不可达对象(Removing Unused Objects)时会因 GC 算法的不同而不同, 但是大部分的 GC 操作一般都可大致分为三类:
标记清除(Mark-Sweep),标记清除整理(Mark-Sweep-Compact),标记复制(Mark-Copy).
标记-清除(Mark-Sweep)
优点:算法相对简单 缺点: 1.算放上要进行扫描两遍,效率偏低。 2.容易产生内存碎片 适应场景:存活对象比较多,即垃圾较少的对象下效率较高,因为第二次遍历(清理垃圾)时,就快一些。比如Eden中垃圾较多,就不适合这种算法。 两次扫描:第一遍找出所有存活的对象,第二遍清理垃圾对象 对于标记清除算法(Mark and Sweep algorithms)应用相对简单,但内存会产生大量的碎片,这样再创建大对象时,假如内存没有足够连续的内存空间可能会 出现 OutOfMemoryError 。
标记-清除-整理(Mark-Sweep-Compact)
标记清除整理算法中在清除垃圾对象以后会移动可用对象,对碎片进行压缩,这 样会在内存中构建相对比较大的连续空间便于大对象的直接存储,但是会增加 GC 暂停时间。 优点: 清理了垃圾,不产生内存碎片,不造成内存的浪费 缺点: 1.算法设计上要进行两次扫描,效率偏低 2.在压缩(整理)的过程中需要调整对象的引用,效率偏低, 适应场景: 对GC停顿时间要求不太严格的场景
标记-复制 (Mark and Copy)
将堆内存划分为相等的两块区域,每次只使用其中的一块,当其中的一块内存使用完了。就将还存活的对象复制到另一块上面,,然后把已经使用过得内存一次性清除掉。 优点: 1.只扫描1次堆内存,提高了效率 2.没有产生内存碎片 缺点: 1.空间浪费严重 2.移动复制对象的过程中需要调整对象的引用 适用场景: 因为它是一次性清除垃圾,所以更适应于 存活对象较少,即垃圾较多的场景,因为一次性就能删掉很多的垃圾,比如Eden的垃圾较多时,就可以使用标记复制算法。
4.GC 算法实现
4.1. GC 算法实现简介
我们知道, JVM 系统在运行时,因新对象的创建,可能会触发 GC 事件。无论哪 种 GC 都可能会暂停应用程序的执行,但如何将暂停时间降到最小,这要看我们 使用的 GC 算法。现在对于 JVM 中的 GC 算法无非两大类:一类负责收集年轻代, 一类负责收集老年代。假如没有显式指定垃圾回收算法,一般会采用系统平台默 认算法,当然也可以自己指定,例如 JDK8 中基于特定垃圾回收算法的垃圾收集 器应用组合如下:
Young | Tenured | JVM options |
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | -XX:+UseG1GC |
4.2. GC 收集器应用分析
4.2.0 垃圾回收器的诞生历史:
JDK的诞生时serial垃圾回收器就跟随而生,它是单线的,能够满足当时几十兆的内存情形。一开始很好用。伴随着JVM对内存的需求越来越大,诞生了多线程的parallel scavenge (ps)和 parallel old (po) 垃圾回收器。一开始的时候内存也就几G,用的挺好。后来内存需求扩展到几十个G,ps,po的stw问题越来越突出。 为了降低STW,诞生了CMS,为了配合CMS的使用,出现了paralle new ;当然CMS时代也就是针对基本上小于32G的内存。
CMS是里程碑式的GC,它开启了并发回收的先河,但是CMS的问题也比较多,因此目前没有一个JDK的默认垃圾回收器是CMS。目前G1逐渐成熟起来,JDK9默认就是G1。
4.2.1. Serial 收集器应用分析
Serial GC 是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5 和 JAVA SE6 中客户端虚拟机采用的默认配置。 Serial GC (串行收集器)应用特点: 1) 内部只使用一个线程执行垃圾回收 ( 不能充分利用 CPU 的多核特性 ),无法并行化。 2) GC 时所有 正在执行的用户线程暂停 并且可能会产生较长时间的停顿(Stop the world) Serial GC(串行收集器)场景应用: 1) 一般可工作在 JVM 的客户端模式。 2) 适用于 CPU 个数或核数较少且内存空间较小(越大可能停顿时间越长)的场景 Serial GC(串行收集器)算法应用 1) 新生代使用 mark-copy( 标记 - 复制 ) 算法 ( 新生代存活对象较少 ) 2) 老年代使用 mark-sweep-compact( 标记 - 清除 - 整理 ) 算法 Serial GC(串行收集器)实践应用
4.2.2. Parallel 收集器应用分析
Parallel 收集器为并行收集器,它可利用多个或多核 CPU 优势实现多线程并行 GC 操作,其目标是减少停顿时间,实现更高的吞吐量( Throughput )。 Parallel GC (并行收集器)应用特点: 1) 可利用 CPU 的多核特性执行多线程下的并行化 GC 操作。 2) GC 期间 , 所有 CPU 内核都在并行清理垃圾 , 所以暂停时间较短。 3) 最大优势是可实现可控的吞吐量与停顿时间。 Parallel GC (并行收集器)场景应用: 1) GC 操作仍需暂停应用程序(也有可能暂停时间比较长,因为 GC 阶段不能被打断),所以不适合要求低延迟的场景。 2) 因其高吞吐 GC(throughput GC)量的特性,适用于后台计算、后台处理的 弱交互场景而不是 web 交互场景 Parallel GC(并行收集器)算法应用: 1) 在年轻代使用标记 - 复制 (mark-copy) 算法,对应的是 Parallel Scavenge 收集器 2) 在老年代使用标记 - 清除 - 整理 (mark-sweep-compact)算法,对应的是 Parallel Old 收集器。 Parallel GC(并行收集器)实践应用:
4.2.3. CMS 收集器应用分析
CMS产生的原因是无法忍受STW了。
CMS 的官方名称为 “ Mostly Concurrent Mark and Sweep Garbage Collector” ,其设计目标是追求更快的响应时间。 CMS (并发收集器)应用特点: 1) 使用空闲列表 (free-lists)管理内存空间的回收,不对老年代进行碎片整理,减少用户线程暂停时间。 2) 在标记 - 清除阶段的大部分工作和用户线程一起并发执行。 3) 最大优点是可减少停顿时间(可提高服务的响应速度) ,最大缺陷是老年代的内存碎片 CMS (并发收集器)场景应用: 1) 应用于多个或多核处理器,目标降低延迟,缩短停顿时间 , 响应时间优先 . 2) CPU 受限场景下,因与用户线程竞争 CPU ,吞吐量会减少。 CMS (并发收集器)算法应用: 1) 年轻代采用并行方式的 mark-copy ( 标记 - 复制 ) 算法。 2) 老年代主要使用并发 mark-sweep ( 标记 - 清除 ) 算法。 CMS (并发收集器)关键步骤分析: 1) 初始标记( initial mark )此阶段标记一下 GC Roots 能直接关联到的对象,速度很快,产生stw的时间很小。 2) 并发标记(concurrent mark)此阶段就是进行 GC Roots Tracing 的过程,从直接关联对象遍历所有可达对象,然后进行标记。 (IBM以前做过统计,80%的GC时间都用在并发标记上,而CMS的并发设计,让用后线程和标记线程同时执行,客户会感觉慢了点,但是至少程序没有暂停,较好的解决了STW问题) 3) 重新标记( final remark)此阶段要修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 (由于并发标记过程中用用线程也不会太多,标记时间也不会用的太久,产生的新的对象和变动了标记的对象也不会太多,重新标记时又是多线程并行标记,所以这个过程很快,产生的stw也比较少) 4) 并发清除( concurrent sweep )此阶段与应用程序并发执行 , 不需要 STW 停顿。目的是删除未使用的对象 , 并收回他们占用的空间。 5) 并发重置 (concurrent Reset) 此阶段与应用程序并发执行 , 重置 CMS 算法相关的内部数据,同时 GC 线程切换到用户线程。 (并发清理的过程中产生的垃圾,没办法 ,只能下一次再清理,这些垃圾被称为浮动垃圾) CMS (并发收集器)实践应用:
优点:并发收集,低停顿
缺点:
1.CMS设计的初衷是为了减少STW,但是由于本身算法的限制(标记清除算法),天然的会产生内存碎片,一旦碎片一多,yong区的大对象进入old区时,一旦找不到足够的空间,就会导致promotionFaild(升级失败),这个时候CMS的解决办法就是调用serialOld垃圾回收器来做标记清除整理回收。这个老太太是单线程的也是针对几十M内存设计的回收器。所以面对大内存的CMS 时代,回收效率会特别慢,可能会产生几个小时到1天的STW。
2.并发清理会产生浮动垃圾
解决办法:
可以降低CMS FGC的阈值,默认是92%才FGC,可以调低一些,比如 60%时就进行FGC,这样old区的垃圾就及时清理了。留出来的空间就多了一些,能够很好的容纳Yong区的垃圾了。–XX:CMSInitiatingOccupancyFraction 92%
4.2.4. parNew垃圾器应用分析
parNew垃圾收集器是配合CMS的产生的新生代的垃圾回收器,是parallel scavege 的 一种新版本。 优点:在多核CPU时,比serial 效率高 缺点:收集过程暂停所有的应用线程,单CPU时比serial 效率差一些 算法:标记复制 适用范围:新生代 应用:运行在服务器上 PS 和 PN区别的延伸阅读: https://docs.oracle.com/en/java/javase/13/gctuning/ergonomics.html#GUID-3D0BB91E-9BFF-4EBB-B523-14493A860E73
4.2.5. G1 收集器应用分析
G1(Garbage-First )收集器是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器。 G1 在实现高吞吐的同时,也最大限度满足了 GC 停顿时间可控的目标。在 Oracle JDK7 update 4 后续的版本中已全面支持 G1 回收器功能。 G1 收集器主要为有如下需求的程序设计: 1) 可以像 CMS 收集器一样能同时和应用线程一起并发的执行; 2) 减少整理内存空间时的停顿时间; 3) 要满足可预测的 GC 停顿时间需求; 4) 不能牺牲太多的吞吐性能; 未来 G1 计划要全面取代 CMS 。 G1 相比 CMS 有更多的优势, G1 是压缩型收集器,可以实现更有效的空间压缩,消除大部分潜在的内存碎片问题。 G1 提供了更精准的可预测的垃圾停顿时间设置,可满足用户在指定垃圾回收时间上的需求。 在 G1 中,堆不再分成连续的年轻代和老年代空间,而是划分为多个 ( 通常是 2048 个 ) 可以存放对象的小堆区 (smaller heap regions)。每个小堆区都可能是 Eden 区 , Survivor 区或者 Old 区 . 在逻辑上 , 所有的 Eden 区和 Survivor区合起来就是年轻代 , 所有的 Old 区拼在一起那就是老年代,如下图所示:


G1好的参考资料百度安全验证
https://baijiahao.baidu.com/s?id=1782119405007668815&wfr=spider&for=pc
5.常见垃圾回收器组合参数设定:(1.8)
-XX:+UseSerialGC = Serial New (DefNew) + Serial Old
小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
-XX:+UseParNewGC = ParNew + SerialOld
这个组合已经很少用(在某些版本中已经废弃)
java - Why Remove support for ParNew+SerialOld andDefNew+CMS in the future? - Stack Overflow
-XX:+UseConcMarkSweepGC = ParNew + CMS + Serial Old(CMS一旦产生promotionFailed后才使用)
-XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
-XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
-XX:+UseG1GC = G1
Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
java +XX:+PrintCommandLineFlags -version
通过GC的日志来分辨
Linux下1.8版本默认的垃圾回收器到底是什么?
1.8.0_181 默认(看不出来)Copy MarkCompact
1.8.0_222 默认 PS + PO
6.JVM常用命令行参数
JVM的命令行参数参考:百度安全验证
https://baijiahao.baidu.com/s?id=1782119405007668815&wfr=spider&for=pc
HotSpot参数分类
标准: - 开头,所有的HotSpot都支持
非标准:-X 开头,特定版本HotSpot支持特定命令
不稳定:-XX 开头,下个版本可能取消
java -version
java -X
-
import java.util.List; import java.util.LinkedList; public class HelloGC { public static void main(String[] args) { System.out.println("HelloGC!"); List list = new LinkedList(); for(;;) { byte[] b = new byte[1024*1024];// 每次1M list.add(b); } } }
还没有评论,来说两句吧...