JVM垃圾回收机制
但是对于Java开发⼈员⽽⾔,⾃动内存管理就像是⼀个⿊匣⼦,如果过度依赖于"⾃动",那么这将会是 ⼀场灾难,最严重的就会弱化Java开发⼈员在程序出现内存溢出时定位问题和解决问题的能⼒。此时, 了解JVM的⾃动内存分配和内存回收原理就显得⾮常重要, 只有在真正了解JVM是如何管理内存 后, 我们才能够在遇⻅OOM时, 快速地根据错误异常⽇志定位问题和解决问题。
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更⾼并发量的瓶颈时,我们就必须对这些“⾃动化”的技术实施必要的监控和调节。

垃圾回收器是对堆和⽅法区的回收。其中, Java堆是垃圾收集器的⼯作重点。
从回收频率上讲:
- 频繁收集Young区
- 较少收集old区
- 基本不动⽅法区
1. 垃圾回收相关算法
垃圾标记阶段:对象存活判断
在堆⾥存放着⼏乎所有的Java对象实例, 在GC执⾏垃圾回收之前, ⾸先需要区分出内存中哪些是存活 对象,哪些是已经死亡的对象。只有被标记为⼰经死亡的对象,GC才会在执⾏垃圾回收时,释放掉其所占⽤的内存空间,因此这个过程我们可以称为垃圾标记阶段。 那么在JVM中究竟是如何标记⼀个死亡对象呢? 简单来说,当⼀个对象已经不再被任何的存活对象继续引⽤时,就可以宣判为已经死亡。 判断对象存活⼀般有两种⽅式:引⽤计数算法和可达性分析算法。
标记阶段:引⽤计数算法
引⽤计数算法(Reference Counting) ⽐较简单, 对每个对象保存⼀个整型的引⽤计数器属性。⽤于记录 对象被引⽤的情况。 对于⼀个对象A,只要有任何⼀个对象引⽤了A,则A的引⽤计数器就加1;当引⽤失效时,引⽤计数器就 减1。只要对象A的引⽤计数器的值为0,即表示对象A不可能再被使⽤,可进⾏回收。 优点:实现简单,垃圾对象便于辨识;判定效率⾼,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引⽤计数器有⼀个严重的问题,即⽆法处理循环引⽤的情况。这是⼀条致命缺陷,导致在Java的垃 圾回收器中没有使⽤这类算法。
循环引⽤

标记阶段:可达性分析算法
可达性分析(或根搜索算法、追踪性垃圾收集)
相对于引⽤计数算法⽽⾔,可达性分析算法不仅同样具备实现简单和执⾏⾼效等特点,更᯿要的是该算 法可以有效地解决在引⽤计数算法中循环引⽤的问题,防⽌内存泄漏的发⽣。
相较于引⽤计数算法,这⾥的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性 垃圾收集(Tracing Garbage Collection)
所谓"GC Roots"根集合就是⼀组必须活跃的引⽤。
基本思路:
- 可达性分析算法是以根对象集合(GC Roots) 为起始点, 按照从上⾄下的⽅式搜索被根对象集合所连接的⽬标对象是否可达。
- 使⽤可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着, 搜索所⾛过的路径称为引⽤链(Reference Chain)
- 如果⽬标对象没有任何引⽤链相连,则是不可达的,就意味着该对象⼰经死亡,可以标记为垃圾对 象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots
在Java语⾔中,GC Roots包括以下⼏类元素
- 虚拟机栈中引⽤的对象 ⽐如:各个线程被调⽤的⽅法中使⽤到的参数、局部变量等。
- 本地⽅法栈内JNI(通常说的本地⽅法) 引⽤的对象
- ⽅法区中类静态属性引⽤的对象 ⽐如:Java类的引⽤类型静态变量
- ⽅法区中常量引⽤的对象 ⽐如:字符串常量池(String Table) ⾥的引⽤
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引⽤。 基本数据类型对应的Class对象, ⼀些常驻的异常对象(如: NullPointerException、OutOfMemoryError) , 系统类加载器。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
如果要使⽤可达性分析算法来判断内存是否可回收,那么分析⼯作必须在⼀个能保障⼀致性的快照中进⾏。这点不满⾜的话分析结果的准确性就⽆法保证。 这点也是导致GC进⾏时必须"Stop The World"的⼀个重要原因。即使是号称(⼏乎) 不会发⽣停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
对象的finalization机制
Java语⾔提供了对象终⽌(finalization) 机制来允许开发⼈员提供对象被销毁之前的⾃定义处理逻辑。
当垃圾回收器发现没有引⽤指向⼀个对象,即:垃圾回收此对象之前,总会先调⽤这个对象的finalize() ⽅法。
finalize() ⽅法允许在⼦类中被᯿写, ⽤于在对象被回收时进⾏资源释放。通常在这个⽅法中进⾏⼀些资 源释放和清理的⼯作,⽐如关闭⽂件、套接字和数据库连接等。
永远不要主动调⽤某个对象的finalize() ⽅法, 应该交给垃圾回收机制调⽤。理由包括下⾯三点:
1. 在finalize()时可能会导致对象复活。
2. finalize()⽅法的执⾏时间是没有保障的,它完全由GC线程决定,极端情况下,若不发⽣GC, 则 finalize() ⽅法将没有执⾏机会。
3. ⼀个糟糕的finalize() 会严重影响GC的性能。
从功能上来说, finalize() ⽅法与C++中的析构函数⽐较相似, 但是Java采⽤的是基于垃圾回收器的⾃动 内存管理机制, 所以finalize() ⽅法在本质上不同于C++中的析构函数。
由于finalize() ⽅法的存在, 虚拟机中的对象⼀般处于三种可能的状态。
如果从所有的根节点都⽆法访问到某个对象,说明对象⼰经不再使⽤了。⼀般来说,此对象需要被回 收。但事实上,也并⾮是“⾮死不可”的,这时候它们暂时处于“缓刑”阶段。⼀个⽆法触及的对象有可能在 某⼀个条件下“复活”⾃⼰,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能 的三种状态。如下:
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引⽤都被释放, 但是对象有可能在finalize() 中复活。
- 不可触及的:对象的finalize() 被调⽤, 并且没有复活, 那么就会进⼊不可触及状态。不可触及的 对象不可能被复活, 因为finalize() 只会被调⽤⼀次。
以上3种状态中, 是由于finalize() ⽅法的存在, 进⾏的区分。只有在对象不可触及时才可以被回收。
具体过程
判定⼀个对象obj A是否可回收, ⾄少要经历两次标记过程:
1.如果对象objA到GC Roots没有引⽤链, 则进⾏第⼀次标记。
2.进⾏筛选,判断此对象是否有必要执⾏finalize() ⽅法
如果对象objA没有重写finalize() ⽅法, 或者finalize() ⽅法已经被虚拟机调⽤过,则虚拟机视为“没有必要执⾏”, objA被判定为不可触及的。
如果对象objA重写了finalize() ⽅法, 且还未执⾏过, 那么objA会被插⼊到F-Queue队列中, 由⼀个虚拟机⾃动创建的、低优先级的Finalizer线程触发其finalize() ⽅法执⾏。
finalize() ⽅法是对象逃脱死亡的最后机会, 稍后GC会对F-Queue队列中的对象进⾏第⼆次标记。 如果objA在finalize() ⽅法中与引⽤链上的任何⼀个对象建⽴了联系,那么在第⼆次标记时, objA 会被移出“即将回收”集合。之后, 对象会再次出现没有引⽤存在的情况。在这个情况下, finalize ⽅法不会被再次调⽤, 对象会直接变成不可触及的状态, 也就是说, ⼀个对象的finalize⽅法只会被调⽤⼀次。
垃圾清除阶段
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执⾏垃圾回收,释放掉⽆⽤对象所占⽤的内存空间,以便有⾜够的可⽤内存空间为新对象分配内存。
⽬前在JVM中⽐较常⻅的三种垃圾收集算法是标记-清除算法(Mark-Sweep) 、复制算法(Copying) 、标记-压缩算法(Mark-Compact)
1. 标记-清除算法
当堆中的有效内存空间(available memory) 被耗尽的时候, 就会停⽌整个程序(也被称为stop the world) ,然后进⾏两项⼯作,第⼀项则是标记,第⼆项则是清除。
标记:Collector从引⽤根节点开始遍历, 标记所有被引⽤的对象。⼀般是在对象的Header中记录为可达对象。
清除:Collector对堆内存从头到尾进⾏线性的遍历, 如果发现某个对象在其Header中没有标记为可达对象, 则将其回收。

缺点
- 效率不算⾼
- 在进⾏GC的时候,需要停⽌整个应⽤程序,导致⽤户体验差
- 这种⽅式清理出来的空闲内存是不连续的,产⽣内存碎⽚。需要维护⼀个空闲列表
注意: 这⾥所谓的清除并不是真的置空,⽽是把需要清除的对象地址保存在空闲的地址列表⾥。下次有新对象 需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
2. 复制算法
为了解决标记-清除算法在垃圾收集效率⽅⾯的缺陷, M.L.Minsky于1963年发表了著名的论⽂,“使⽤双 存储区的Lisp语⾔垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage) ”。M.L.Minsky在该论⽂中描述的 算法被⼈们称为复制(Copying) 算法, 它也被M.L.Minsky本⼈成功地引⼊到了Lisp语⾔的⼀个实现版本 中。
核心思想: 将活着的内存空间分为两块,每次只使⽤其中⼀块,在垃圾回收时将正在使⽤的内存中的存活对象复制 到未被使⽤的内存块中,之后清除正在使⽤的内存块中的所有对象,交换两个内存的⻆⾊,最后完成垃圾回收。

优点
- 没有标记和清除过程,实现简单,运⾏⾼效
- 复制过去以后保证空间的连续性,不会出现“碎⽚”问题。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为⼤量region的GC, 复制⽽不是移动, 意味着GC需要维护region之间对象引 ⽤关系, 不管是内存占⽤或者时间开销也不⼩。
此算法特别适合垃圾对象很多, 存活对象很少的场景; 例如:Young区的Survivor0和Survivor1区
应⽤场景
在新⽣代,对常规应⽤的垃圾回收,⼀次通常可以回收70号-99号的内存空间。 回收性价⽐很⾼。所以现在的商业虚拟机都是⽤这种收集算法回收新⽣代。

3. 标记-压缩算法
复制算法的⾼效性是建⽴在存活对象少、垃圾对象多的前提下的。这种情况在新⽣代经常发⽣,但是在 ⽼年代,更常⻅的情况是⼤部分对象都是存活对象。如果依然使⽤复制算法,由于存活对象较多,复制 的成本也将很⾼。因此,基于⽼年代垃圾回收的特性,需要使⽤其他的算法。
标记⼀清除算法的确可以应⽤在⽼年代中,但是该算法不仅执⾏效率低下,⽽且在执⾏完内存回收后还 会产⽣内存碎⽚, 所以JVM的设计者需要在此基础之上进⾏改进。标记-压缩(Mark-Compact) 算法由此 诞⽣。 1970年前后, G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集 器中,⼈们都使⽤了标记-压缩算法或其改进版本。
执⾏过程
- 第⼀阶段和标记清除算法⼀样,从根节点开始标记所有被引⽤对象
- 第⼆阶段将所有的存活对象压缩到内存的⼀端,按顺序排放。
- 之后,清理边界外所有的空间。

标记-压缩算法的最终效果等同于标记-清除算法执⾏完成后,再进⾏⼀次内存碎⽚整理,因此,也可以把 它称为标记-清除-压缩(Mark-Sweep-Compact) 算法。
⼆者的本质差异在于标记-清除算法是⼀种⾮移动式的回收算法,标记-压缩是移动式的。是否移动回收后 的存活对象是⼀项优缺点并存的⻛险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,⽽未被标记的内存会被清理掉。如此 ⼀来,当我们需要给新对象分配内存时,JVM只需要持有⼀个内存的起始地址即可, 这⽐维护⼀个空闲 列表显然少了许多开销。
指针碰撞(Bump the Pointer)
如果内存空间以规整和有序的⽅式分布,即已⽤和未⽤的内存都各⾃⼀边,彼此之间维系着⼀个记录下⼀次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第⼀个空闲内存位置上,这种分配⽅式就叫做指针碰撞(Bump the Pointer) 。
优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时, JVM只需要持有 ⼀个内存的起始地址即可。
- 消除了复制算法当中,内存减半的⾼额代价。
缺点:
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引⽤,则还需要调整引⽤的地址。
- 移动过程中,需要全程暂停⽤户应⽤程序。即:STW
小结:
| 标记清除 | 标记压缩 | 复制 | |
| 速度 | 中等 | 最慢 | 最快 |
| 空间 | 少(堆积内存碎片) | 少 (不堆积内存碎片) | 需要存活对象2倍的空间大小,不堆积内存碎片 |
| 移动对象 | 否 | 是 | 是 |
效率上来说,复制算法是当之⽆愧的⽼⼤,但是却浪费了太多内存。 ⽽为了尽量兼顾上⾯提到的三个指标,标记-压缩算法相对来说更平滑⼀些,但是效率上不尽如⼈意,它⽐复制算法多了⼀个标记的阶段,⽐标记清除多了⼀个整理内存的阶段。
4 分代收集算法
前⾯所有这些算法中,并没有⼀种算法可以完全替代其他算法,它们都具有⾃⼰独特的优势和特点。分代收集算法应运⽽⽣。
分代收集算法,是基于这样⼀个事实:不同的对象的⽣命周期是不⼀样的。因此,不同⽣命周期的对象 可以采取不同的收集⽅式,以便提⾼回收效率。⼀般是把Java堆分为新⽣代和⽼年代,这样就可以根据 各个年代的特点使⽤不同的回收算法,以提⾼垃圾回收的效率。 在Java程序运⾏的过程中,会产⽣⼤量的对象,其中有些对象是与业务信息相关, ⽐如Http请求中的 Session对象、线程、Socket连接, 这类对象跟业务直接挂钩,因此⽣命周期⽐较⻓。但是还有⼀些对 象,主要是程序运⾏过程中⽣成的临时变量,这些对象⽣命周期会⽐较短,⽐如:String对象,由于其 不变类的特性,系统会产⽣⼤量的这些对象,有些对象甚⾄只⽤⼀次即可回收。
⽬前⼏乎所有的GC都是采⽤分代收集(Generational Collecting) 算法执⾏垃圾回收的。 在HotSpot中, 基于分代的概念, GC所使⽤的内存回收算法必须结合年轻代和⽼年代各⾃的特点。
年轻代(Young Gen)
年轻代特点:区域相对⽼年代较⼩,对象⽣命周期短、存活率低,回收频繁。 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象⼤⼩有关,因此很适⽤于年轻代的回收。⽽复制算法内存利⽤率不⾼的问题, 通过hotspot中的两个survivor的设计得到缓解。
⽼年代(Tenured Gen)
⽼年代特点:区域较⼤,对象⽣命周期⻓、存活率⾼,回收不如年轻代频繁。 这种情况存在⼤量存活率⾼的对象,复制算法明显变得不合适。⼀般是由标记-清除或者是标记-清除与标 记-整理的混合实现。
- Mark阶段的开销与存活对象的数量成正⽐。
- Sweep阶段的开销与所管理区域的⼤⼩成正相关。
- Compact阶段的开销与存活对象的数据成正⽐。
以HotSpot中的CMS回收器为例, CMS是基于Mark-Sweep实现的, 对于对象的回收效率很⾼。⽽对于碎⽚问题, CMS采⽤基于Mark-Compact算法的Serial old回收器作为补偿措施:当内存回收不佳(碎⽚导致的Concurrent Mode Failure时) , 将采⽤Serial old执⾏Full GC以达到对⽼年代内存的整理。
分代的思想被现有的虚拟机⼴泛使⽤。⼏乎所有的垃圾回收器都区分新⽣代和⽼年代。
5. 增量收集算法
上述现有的算法, 在垃圾回收过程中, 应⽤软件将处于⼀种Stop the World的状态。在Stop the World 状态下, 应⽤程序所有的线程都会挂起, 暂停⼀切正常的⼯作,等待垃圾回收的完成。如果垃圾回收时 间过⻓,应⽤程序会被挂起很久,将严重影响⽤户体验或者系统的稳定性。为了解决这个问题,即对实 时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting) 算法的诞⽣。
基本思想:
如果⼀次性将所有的垃圾进⾏处理,需要造成系统⻓时间的停顿,那么就可以让垃圾收集线程和应⽤程 序线程交替执⾏。每次,垃圾收集线程只收集⼀⼩⽚区域的内存空间,接着切换到应⽤程序线程。依次 反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的⽅式完成标记、清理或复制⼯作。
缺点
使⽤这种⽅式,由于在垃圾回收过程中,间断性地还执⾏了应⽤程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下⽂转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
6. 分区算法
般来说,在相同条件下,堆空间越⼤,⼀次GC时所需要的时间就越⻓,有关GC产⽣的停顿也越⻓。 为了更好地控制GC产⽣的停顿时间,将⼀块⼤的内存区域分割成多个⼩块,根据⽬标的停顿时间,每次 合理地回收若⼲个⼩区间,⽽不是整个堆空间,从⽽减少⼀次GC所产⽣的停顿。
分代算法将按照对象的⽣命周期⻓短划分成两个部分,分区算法将整个堆空间划分成连续的不同⼩区间。
每⼀个⼩区间都独⽴使⽤,独⽴回收。这种算法的好处是可以控制⼀次回收多少个⼩区间。

注意:这些只是基本的算法思路,实际GC实现过程要复杂的多,⽬前还在发展中的前沿GC都是复合算 法,并且并⾏和并发兼备。
2. 垃圾回收相关概念
1. System.gc() 的理解
在默认情况下, 通过System.gc() 或者Runtime.getRuntime() .gc()的调⽤,会显式触发Full GC, 同时对⽼年代和新⽣代进⾏回收,尝试释放被丢弃对象占⽤的内存。
JVM实现者可以通过System.gc() 调⽤来决定JVM的GC⾏为,然⽽System.gc() 调⽤附带⼀个免责声明,⽆法保证对垃圾收集器的调⽤。通常情况下,垃圾回收应该是⾃动进⾏的,⽆须⼿动触发,否则就太过于麻烦了。在⼀些特殊情况下,如我们正在编写⼀个性能基准,我们可 以在运⾏之间调⽤System.gc() 。
public class SystemGCTest{ public static void main(String[] args) { new SystemGCTest(); System.gc();//提醒jvm的垃圾回收器执⾏gc, 但是不确定是否⻢上执⾏gc //与Runtime.getRuntime().gc(); 的作⽤⼀样。 System.runFinalization(); //强制调⽤使⽤引⽤的对象的finalize()⽅法 } @override protected void finalize() throws Throwable{ super.finalize(); System.out.println("System GC Test重写了finalize() "); } }
2. 内存溢出与内存泄漏
内存溢出(OOM): javadoc中对OutOfMemoryError的解释是, 没有空闲内存, 并且垃圾收集器也⽆法提供更多内存。
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸⾸之⼀。⼤多数情况下,GC会进⾏各种年龄段的垃圾回收,实在不⾏了就放⼤招,来⼀次独占式的Full GC操作, 这时候会回收⼤ᰁ的内存, 供应⽤程序继续使⽤。
⾸先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有⼆:
(1) Java虚拟机的堆内存设置不够。⽐如:可能存在内存泄漏问题;也很有可能就是堆的⼤⼩不合理,⽐如我们要处理⽐较可观的数据 ᰁ, 但是没有显式指定JVM堆⼤⼩或者指定数值偏⼩。我们可以通过参数-Xms、-Xmx来调整。
(2) 代码中创建了⼤量⼤对象,并且⻓时间不能被垃圾收集器收集(存在被引⽤),对于⽼版本的Oracle JDK, 因为永久代的⼤⼩是有限的, 并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型) ⾮常不积极,所以当我们不断添加新类型的时候, 永久代出现OutOfMemoryError也⾮常多⻅, 尤其是 在运⾏时存在⼤量动态类型⽣成的场合; 类似intern字符串缓存占⽤太多空间, 也会导致OOM问题。 对应的异常信息,会标记出来和永久代相关:“java.lang.OutofMemoryError:Perm Gen space”。JDK8后,随着元数据区的引⼊, ⽅法区内存已经不再那么窘迫, 所以相应的OOM有所改观, 出现OOM异常 信息则变成了:“java.lang.OutOfMemoryError:Metaspace”。直接内存不⾜, 也会导致OOM。
这⾥⾯隐含着⼀层意思是, 在拋出OutOfMemoryError之前, 通常垃圾收集器会被触发,尽其所能去清理出空间。
- 在引⽤机制分析中, 涉及到JVM会去尝试回收软引⽤指向的对象等。
- 在java.nio.BIts.reserve Memory() ⽅法中, 我们能清楚的看到, System.gc()会被调⽤,以清理空 间。
当然,也不是在任何情况下垃圾收集器都会被触发的
- ⽐如, 我们去分配⼀个超⼤对象, 类似⼀个超⼤数组超过堆的最⼤值,JVM可以判断出垃圾收集 并不能解决这个问题, 所以直接抛出OutOfMemoryError。
内存泄漏(Memory Leak):也称作“存储渗漏”。严格来说,只有对象不会再被程序⽤到了,但是GC⼜不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候⼀些不太好的实践(或疏忽)会导致对象的⽣命周期变得很⻓甚⾄导致OOM,也可以 叫做宽泛意义上的“内存泄漏”。
尽管内存泄漏并不会⽴刻引起程序崩溃,但是⼀旦发⽣内存泄漏,程序中的可⽤内存就会被逐步蚕⻝, 直⾄耗尽所有内存, 最终出现OutOfMemory异常,导致程序崩溃。
举例:
1、单例模式 单例的⽣命周期和应⽤程序是⼀样⻓的,所以单例程序中,如果持有对外部对象的引⽤的话,那么这个 外部对象是不能被回收的,则会导致内存泄漏的产⽣。
2、⼀些提供close的资源未关闭导致内存泄漏 数据库连接(dataSourse.getConnection() ) , ⽹络连接(socket) 和io连接必须⼿动close,否则是不能被 回收的。
3. Stop The World
Stop-the-World, 简称STW,指的是GC事件发⽣过程中,会产⽣应⽤程序的停顿。停顿产⽣时整个应 ⽤程序线程都会被暂停,没有任何响应,有点像卡死的感觉, 这个停顿称为STW。
可达性分析算法中枚举根节点(GC Roots) 会导致所有Java执⾏线程停顿。
- 分析⼯作必须在⼀个能确保⼀致性的快照中进⾏
- ⼀致性指整个分析期间整个执⾏系统看起来像被冻结在某个时间点上
- 如果出现分析过程中对象引⽤关系还在不断变化,则分析结果的准确性⽆法保证
被STW中断的应⽤程序线程会在完成GC之后恢复, 频繁中断会让⽤户感觉像是⽹速不快造成电影卡带⼀ 样, 所以我们需要减少STW的发⽣。
STW事件和采⽤哪款GC⽆关, 所有的GC都有这个事件。哪怕是G1也不能完全避免Stop-the-world情况发⽣, 只能说垃圾回收器越来越优秀,回收效率越来越 ⾼,尽可能地缩短了暂停时间。
STW是JVM在后台⾃动发起和⾃动完成的。在⽤户不可⻅的情况下, 把⽤户正常的⼯作线程全部停掉。 开发中不要⽤System.gc(),会导致Stop-the-world的发⽣。
4. 垃圾回收的并⾏与并发
并发(Concurrent)
在操作系统中,是指⼀个时间段中有⼏个程序都处于已启动运⾏到运⾏完毕之间,且这⼏个程序都是在 同⼀个处理器上运⾏。
并发不是真正意义上的“同时进⾏”, 只是CPU把⼀个时间段划分成⼏个时间⽚段(时间区间) , 然后在这 ⼏个时间区间之间来回切换, 由于CPU处理的速度⾮常快, 只要时间间隔处理得当,即可让⽤户感觉是多个应⽤程序同时在进⾏。
从⼀段时间来看,有多个任务在执⾏
从单⼀时间⽚上看,只有⼀个任务在执⾏
实际上就是CPU在快速切换任务交替执⾏

并⾏(Parallel)
当系统有⼀个以上CPU时, 当⼀个CPU执⾏⼀个进程时, 另⼀个CPU可以执⾏另⼀个进程,两个进程互不抢占CPU资源, 可以同时进⾏, 我们称之为并⾏(Parallel) 。 其实决定并⾏的因素不是CPU的数量, ⽽是CPU的核⼼数量, ⽐如⼀个CPU多个核也可以并⾏。 适合科学计算,后台处理等弱交互场景。
并发vs并⾏
- ⼆者对⽐: 并发,指的是多个事情,在同⼀时间段内同时发⽣了。
- 并⾏,指的是多个事情,在同⼀时间点上同时发⽣了。
- 并发的多个任务之间是互相抢占资源的。
- 并⾏的多个任务之间是不互相抢占资源的。
- 只有在多CPU或者⼀个CPU多核的情况中, 才会发⽣并⾏。 否则,看似同时发⽣的事情,其实都是并发执⾏的
垃圾回收的并发与并⾏
并发和并⾏,在谈论垃圾收集器的上下⽂语境中,它们可以解释如下:
并⾏(Parallel) :指多条垃圾收集线程并⾏⼯作, 但此时⽤户线程仍处于等待状态。 如Par New、Parallel Scavenge、Parallel old;
串⾏(Serial):相较于并⾏的概念,单线程执⾏。 如果内存不够, 则程序暂停, 启动JVM垃圾回收器进⾏垃圾回收。回收完, 再启动程序的线程。


并发(concurrent):指⽤户线程与垃圾收集线程同时执⾏(但不⼀定是并⾏的, 可能会交替执⾏),垃圾回收线程在执⾏时不会停顿⽤户程序的运⾏。
⽤户程序在继续运⾏, ⽽垃圾收集程序线程运⾏于另⼀个CPU上; 如:CMS、G1

5. 安全点与安全区域
安全点(Safe point):程序执⾏时并⾮在所有地⽅都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC, 这些位置 称为“安全点(Safe point)。
Safe Point的选择很重要, 如果太少可能导致GC等待的时间太⻓, 如果太频繁可能导致运⾏时的性能问题。⼤部分指令的执⾏时间都⾮常短暂,通常会根据“是否具有让程序⻓时间执⾏的特征”为标准。⽐ 如:选择⼀些执⾏时间较⻓的指令作为Safe Point, 如⽅法调⽤、循环跳转和异常跳转等。
如何在GC发⽣时,检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断:(⽬前没有虚拟机采⽤了) ⾸先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
- 主动式中断:设置⼀个中断标志,各个线程运⾏到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将⾃⼰进⾏中断挂起。
安全区域(Safe Region):
Safe point机制保证了程序执⾏时, 在不太⻓的时间内就会遇到可进⼊GC的Safe point。但是, 程序“不 执⾏”的时候呢? 例如线程处于Sleep状态或Blocked状态, 这时候线程⽆法响应JVM的中断请求,“⾛”到 安全点去中断挂起, JVM也不太可能等待线程被唤醒。对于这种情况, 就需要安全区域(Safe Region) 来解决。
安全区域是指在⼀段代码⽚段中,对象的引⽤关系不会发⽣变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safe point。
实际执⾏时
1、 当线程运⾏到Safe Region的代码时,⾸先标识已经进⼊了Safe Region,如果这段时间内发⽣GC, JVM 会忽略标识为Safe Region状态的线程;
2、当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了, 则继续运⾏, 否则线程必须等待直到收到可以安全离开Safe Region的信号为⽌;
3. 垃圾回收器
1. 垃圾回收器概述
垃圾收集器没有在规范中进⾏过多的规定,可以由不同的⼚商、不同版本的JVM来实现。 由于JDK的版本处于⾼速迭代过程中, 因此Java发展⾄今已经衍⽣了众多的GC版本。 从不同⻆度分析垃圾收集器,可以将GC分为不同的类型。
按线程数分:可以分为串⾏垃圾回收器和并⾏垃圾回收器

串⾏回收指的是在同⼀时间段内只允许有⼀个CPU⽤于执⾏垃圾回收操作,此时⼯作线程被暂停,直⾄垃圾收集⼯作结束。
- 在诸如单CPU处理器或者较⼩的应⽤内存等硬件平台不是特别优越的场合,串⾏回收器的性能表现 可以超过并⾏回收器和并发回收器。所以, 串⾏回收默认被应⽤在客户端的Client模式下的JVM中
- 在并发能⼒⽐较强的CPU上, 并⾏回收器产⽣的停顿时间要短于串⾏回收器。
和串⾏回收相反, 并⾏收集可以运⽤多个CPU同时执⾏垃圾回收, 因此提升了应⽤的吞吐ᰁ,不过并⾏ 回收仍然与串⾏回收⼀样,采⽤独占式,使⽤了“Stop-the-world”机制。
按照⼯作模式分:可以分为并发式垃圾回收器和独占式垃圾回收器。
- 并发式垃圾回收器与应⽤程序线程交替⼯作,以尽可能减少应⽤程序的停顿时间。
- 独占式垃圾回收器(Stop the world) ⼀旦运⾏, 就停⽌应⽤程序中的所有⽤户线程,直到垃圾回收过程 完全结束。

按碎⽚处理⽅式分:可分为压缩式垃圾回收器和⾮压缩式垃圾回收器。
压缩式垃圾回收器会在回收完成后,对存活对象进⾏压缩整理,消除回收后的碎⽚。 ⾮压缩式的垃圾回收器不进⾏这步操作。
按⼯作的内存区间分:⼜可分为年轻代垃圾回收器和⽼年代垃圾回收器。
2. 评估GC的性能指标
GC时间:暂停时间总和(暂停有很多次,每次可以很短)
程序运⾏时间
总运⾏时间 = GC时间+程序运⾏时间
吞吐量 = 程序运⾏时间 / 总运⾏时间
低延迟衡量的标准 = 每个GC时间
垃圾收集开销:吞吐量的补数,GC时间 / 总运⾏时间。
暂停时间:执⾏垃圾收集时,程序的⼯作线程被暂停的时间。
收集频率:相对于应⽤程序的执⾏,收集操作发⽣的频率。
内存占⽤:Java堆区所占的内存⼤⼩。
吞吐量、暂停时间、内存占⽤,这三者共同构成⼀个“不可能三⻆”。三者总体的表现会随着技术进步⽽ 越来越好。⼀款优秀的收集器通常最多同时满⾜其中的两项。
这三项⾥,暂停时间的重要性⽇益凸显。因为随着硬件发展,内存占⽤多些越来越能容忍,硬件性能的提升也有助于降低收集器运⾏时对应⽤程序的影响,即提⾼了吞吐量。⽽内存的扩⼤,对延迟反⽽带来负⾯效果。
简单来说,主要抓住两点:
- 吞吐量
- 暂停时间
吞吐量(throughput)
吞吐量就是CPU⽤于运⾏⽤户代码的时间与CPU总消耗时间的⽐值, 即吞吐量=运⾏⽤户代码时间/(运⾏ ⽤户代码时间+垃圾收集时间)。
⽐如:虚拟机总共运⾏了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
吞吐量优先, 意味着在单位时间内, STW的总时间最短:0.2+0.2=0.4

暂停时间 (pausetime)
“暂停时间”是指⼀个时间段内应⽤程序线程暂停,让GC线程执⾏的状态.
例如,GC期间1毫秒的暂停时间意味着在这1毫秒期间内没有应⽤程序线程是活动的。
暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1+0.1+0.1+0.1+0.1=0.5

吞吐量vs暂停时间
- ⾼吞吐量较好因为这会让应⽤程序的最终⽤户感觉只有应⽤程序线程在做“⽣产性”⼯作。直觉上,吞吐量越⾼程序运⾏越快。
- 低暂停时间(低延迟)较好因为从最终⽤户的⻆度来看不管是GC还是其他原因导致⼀个应⽤被挂起始终 是不好的。这取决于应⽤程序的类型,有时候甚⾄短暂的200毫秒暂停都可能打断终端⽤户体验。因 此,具有低的较⼤暂停时间是⾮常᯿要的,特别是对于⼀个交互式应⽤程序。
不幸的是“⾼吞吐量”,和“低暂停时间”是⼀对相互竟争的⽬标(⽭盾)
- 因为如果选择以吞吐量优先,那么必然需要降低内存回收的执⾏频率,但是这样会导致GC需要更⻓的暂停时间来执⾏内存回收。
- 相反的,如果选择以低延迟优先为原则,那么为了降低每次执⾏内存回收时的暂停时间,也只能频 繁地执⾏内存回收,但这⼜引起了年轻代内存的缩减和导致程吞吐量的下降。
在设计(或使⽤)GC算法时,我们必须确定我们的⽬标:⼀个GC算法只可能针对两个⽬标之⼀(即只 专注于较⼤吞吐量或最⼩暂停时间),或尝试找到⼀个⼆者的折衷: 在最⼤吞吐量优先的情况下,降低停顿时间。
3. 常见垃圾回收器
垃圾收集器发展史:
有了虚拟机,就⼀定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector。
- 1999年随JDK1.3.1⼀起来的是串⾏⽅式的Serial GC,它是第⼀款GC。ParNew垃圾收集器是Serial 收集器的多线程版本。
- 2002年2⽉26⽇,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2⼀起发布。
- Parallel GC在JDK6之后成为Hotspot默认GC。
- 2012年,在JDK1.7u4版本中,G1可⽤。
- 2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
- 2018年3⽉,JDK10中G1垃圾回收器的并⾏完整垃圾回收,实现并⾏性来改善最坏情况下的延迟。
- 2018年9⽉,JDK11发布。引⼊Epsilon垃圾回收器,⼜被称为“No-Op(⽆操作)”回收器。同时, 引⼊ZGC:可伸缩的低延迟垃圾回收器(Experimental)。
- 2019年3⽉,JDK12发布。增强G1,⾃动返回未⽤堆内存给操作系统。同时,引⼊Shenandoah GC:低停顿时间的GC(Experimental)。
- 2019年9⽉,JDK13发布。增强ZGC,⾃动返回未⽤堆内存给操作系统。
- 2020年3⽉,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macOS和Windows上的应⽤。
7款经典的垃圾收集器
串⾏回收器:Serial、Serial Old
并⾏回收器:ParNew、Parallel Scavenge、Parallel Old
并发回收器:CMS、G1

7款经典的垃圾收集器与垃圾分代的关系
新⽣代收集器:Serial、ParNew、Parallel Scavenge
⽼年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1


1. 两个收集器间有连线,表明它们可以搭配使⽤ Serial / Serial Old、Serial / CMS、ParNew / Serial Old、ParNew/CMS、Parallel Scavenge / Serial Old、Parallel Scavenge / Parallel Old 、G1;
2. 其中Serial Old作为CMS出现“Concurrent Mode Failure"失败的后备预案。
3. (红⾊虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew + serial Old这两 个组合声明为废弃(JEP 173),并在JDK 9中完全取消了这些组合的⽀持(JEP214),即:移除。
4. (绿⾊虚线)JDK14中:弃⽤Parallel Scavenge和Seria1 Old GC组合(JEP 366)
5. (⻘⾊虚线)JDK14中:删除CMS垃圾回收器(JEP 363)
⼩结
- 为什么要有很多收集器,⼀个不够吗?因为Java的使⽤场景很多,移动端,服务器等。所以就需要 针对不同的场景,提供不同的垃圾收集器,提⾼垃圾收集的性能。
- 虽然我们会对各个收集器进⾏⽐较,但并⾮为了挑选⼀个最好的收集器出来。没有⼀种放之四海皆准、任何场景下都适⽤的完美收集器存在,更加没有万能的收集器。所以我们选择的只是对具体应⽤最合适的收集器。
查看默认的垃圾回收器
- -XX:+PrintCommandLineFlags:查看命令⾏相关参数(包含使⽤的垃圾收集器)
- 使⽤命令⾏指令:jinfo -flag 相关垃圾回收器参数 进程ID
3.1 Serial回收器:串⾏回收
Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新⽣代唯⼀的选择。 Serial收集器作为Hotspot中Client模式下的默认新⽣代垃圾收集器。
Serial收集器采⽤复制算法、串⾏回收和“Stop the World”机制的⽅式执⾏内存回收。
除了年轻代之外,Serial收集器还提供⽤于执⾏⽼年代垃圾收集的Serial Old收集器。Serial Old收集器同 样也采⽤了串⾏回收和“Stop the World”机制,只不过内存回收算法使⽤的是标记-压缩算法。
Serial Old是运⾏在Client模式下默认的⽼年代的垃圾回收器 Serial Old在Server模式下主要有两个⽤途: 与新⽣代的Parallel Scavenge配合使⽤作为⽼年代CMS收集器的搂底垃圾收集⽅案。

这个收集器是⼀个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使⽤⼀个CPU或⼀条收集线程去完成垃圾收集⼯作,更重要的是在它进⾏垃圾收集时,必须暂停其他所有的⼯作线程,直到它收 集结束(Stop The World)。
优势:简单⽽⾼效(与其他收集器的单线程⽐),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专⼼做垃圾收集⾃然可以获得最⾼的单线程收集效率。
在⽤户的桌⾯应⽤场景中,可⽤内存⼀般不⼤(⼏⼗MB⾄⼀两百MB)可以在较短时间内完成垃圾收集 (⼏⼗ms⾄⼀百多ms),只要不频繁发⽣,使⽤串⾏回收器是可以接受的。
在HotSpot虚拟机中,使⽤-XX:+UseSerialGC参数可以指定年轻代和⽼年代都使⽤串⾏收集器。
- 等价于新⽣代⽤serial GC,且⽼年代⽤Serial Old GC
总结:
- 这种垃圾收集器⼤家了解,现在⼰经不⽤串⾏的了。⽽且在限定单核cpu才可以⽤。现在都不是单 核的了。
- 对于交互较强的应⽤⽽⾔,这种垃圾收集器是不能接受的。⼀般在Java web应⽤程序中是不会采⽤串⾏垃圾收集器的
3.2 ParNew回收器:并⾏回收
如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。 Par是Parallel的缩写,New:只能处理的是新⽣代。
ParNew收集器除了采⽤并⾏回收的⽅式执⾏内存回收外,两款垃圾收集器之间⼏乎没有任何区别。 ParNew收集器在年轻代中同样也是采⽤复制算法、"Stop-the-world"机制。
- ParNew是很多JVM运⾏在Server模式下新⽣代的默认垃圾收集器。

对于新⽣代,回收次数频繁,使⽤并⾏⽅式⾼效。
对于⽼年代,回收次数少,使⽤串⾏⽅式节省资源。
由于ParNew收集器是基于并⾏回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会⽐ Serial收集器更⾼效?
- ParNew收集器运⾏在多CPU的环境下,由于可以充分利⽤多CPU、多核⼼等物理硬件资源优势, 可以更快速地完成垃圾收集,提升程序的吞吐量。
- 但是在单个CPU的环境下,parNew收集器不⽐serial收集器更⾼效。虽然Serial收集器是基于串⾏回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产⽣的⼀些额外开销。
因为除Serial外,⽬前只有ParNew GC能与CMS收集器配合⼯作
在程序中,开发⼈员可以通过选项"-XX:+UseParNewGC"⼿动指定使⽤ParNew收集器执⾏内存回收任 务。它表示年轻代使⽤并⾏收集器,不影响⽼年代。 -XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。
3.3 Parallel回收器:吞吐量优先
HotSpot的年轻代中除了拥有ParNew收集器是基于并⾏回收的以外,Parallel Scavenge收集器同样也采 ⽤了复制算法、并⾏回收和"Stop The World"机制。
那么Parallel收集器的出现是否多此⼀举?
- 和ParNew收集器不同,Parallel Scavenge收集器的⽬标则是达到⼀个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
- ⾃适应调节策略也是Parallel Scavenge与ParNew—个重要区别。
⾼吞吐量则可以⾼效率地利⽤CPU时间,尽快完成程序的运算任务,主要适合在后台运算⽽不需要太多交互的任务。因此,常⻅在服务器环境中使⽤。例如,那些执⾏批量处理、订单处理、⼯资⽀付、科学 计算的应⽤程序。
Parallel收集器在JDKI.6时提供了⽤于执⾏⽼年代垃圾收集的Parallel Old收集器,⽤来代替⽼年代的 Serial Old收集器。 Parallel Old收集器采⽤了标记-压缩算法,但同样也是基于并⾏回收和"Stop—the—World"机制。

在程序吞叶量优先的应⽤场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下的内存 回收性能很不错。在Java8中,默认是此垃圾收集器。
参数配置
- -XX:+UseParallelGC ⼿动指定年轻代使⽤Parallel并⾏收集器执⾏内存回收任务。
- -XX:+UseParallelOldGC ⼿动指定⽼年代都是使⽤并⾏回收收集器。
1. 分别适⽤于新⽣代和⽼年代。默认jdk8是开启的。
2. 上⾯两个参数,默认开启⼀个,另⼀个也会被开启。(互相激活) - -XX:ParallelGCThreads 设置年轻代并⾏收集器的线程数。⼀般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
1. 在默认情况下,当CPU数量⼩于8个,ParallelGCThreads的值等于CPU数量。
2. 当CPU数量⼤于8个,ParallelGCThreads的值等于3+[5*CPU_Count]/8]。 - -XX:MaxGCPauseMiIIis 设置垃圾收集器最⼤停顿时间(即STW的时间)。单位是毫秒。
1. 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在⼯作时会调整Java堆⼤⼩ 或者其他⼀些参数。
2. 对于⽤户来讲,停顿时间越短体验越好。
3. 但是在服务器端,我们注重⾼并发,整体的吞吐量。 所以服务器端适合Parallel,进⾏控制。 该参数使⽤需谨慎。 - -XX:GCTimeRatio 垃圾收集时间占总时间的⽐例(=1/(N+1))。⽤于衡量吞叶量的⼤⼩。
1. 取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%。
2. 与前⼀个-XX:MaxGcpauseMillis参数有⼀定⽭盾性。暂停时间越⻓,Radio参数就容易超过设定的⽐例。 - -XX:+UseAdaptiveSizePoIicy 设置Parallel Scavenge收集器具有⾃适应调节策略。
1. 在这种模式下,年轻代的⼤⼩、Eden和survivor的⽐例、晋升⽼年代的对象年龄等参数会被⾃动调整,来达到在堆⼤⼩、吞吐量和停顿时间之间的平衡点。
2. 在⼿动调优⽐较困难的场合,可以直接使⽤这种⾃适应的⽅式,仅指定虚拟机的最⼤堆、⽬标 的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机⾃⼰完成调优⼯作。
3.4 CMS回收器:低延迟
在JDK1.5时期,Hotspot推出了⼀款在强交互应⽤中⼏乎可认为有划时代意义的垃圾收集器: CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程同时⼯作。
CMS收集器的关注点是尽可能缩短垃圾收集时⽤户线程的停顿时间。停顿时间越短(低延迟)就越适合与⽤户交互的程序,良好的响应速度能提升⽤户体验。
- ⽬前很⼤⼀部分的Java应⽤集中在互联⽹站或者B/S系统的服务端上,这类应⽤尤其重视服务的响应速度,希望系统停顿时间最短,以给⽤户带来较好的体验。CMS收集器就⾮常符合这类应⽤的需求。
CMS的垃圾收集算法采⽤标记-清除算法,并且也会"Stop-the-world"。
不幸的是,CMS作为⽼年代的收集器,却⽆法与JDK1.4.0中⼰经存在的新⽣代收集器Parallel Scavenge 配合⼯作,所以在JDK1.5中使⽤CMS来收集⽼年代的时候,新⽣代只能选择parNew或者serial收集器中的⼀个。
在G1出现之前,CMS使⽤还是⾮常⼴泛的。⼀直今天,仍然有很多系统使⽤CMS GC。

⼯作原理
CMS整个过程⽐之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
1. 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的⼯作线程都将会因为"Stop-the-world” 机制⽽出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。⼀旦标记完成之后就会恢复之前被暂停的所有应⽤线程。由于直接关联对象⽐较⼩,所以这⾥的速度⾮常快。
2. 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较⻓但是不需要停顿⽤户线程,可以与垃圾收集线程⼀起并发运⾏。
3. 重新标记(Remark)阶段:由于在并发标记阶段中,程序的⼯作线程会和垃圾收集线程同时运⾏或者交叉运⾏,因此为了修正并发标记期间,因⽤户程序继续运作⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间通常会⽐初始标记阶段稍⻓⼀些,但也远⽐并发标记阶段的时间短。
4. 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的⼰经死亡的对象,释放内存 空间。由于不需要移动存活对象,所以这个阶段也是可以与⽤户线程同时并发的。
说明补充
尽管CMS收集器采⽤的是并发回收(⾮独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执⾏"Stop-the-world”机制暂停程序中的⼯作线程,不过暂停时间并不会太⻓,因此可以说明⽬前所有的垃圾收集器都做不到完全不需要"stop-the-World”,只是尽可能地缩短暂停时间。
由于最耗费时间的并发标记与并发清除阶段都不需要暂停⼯作,所以整体的回收是低停顿的。
另外,由于在垃圾收集阶段⽤户线程没有中断,所以在CMS回收过程中,还应该确保应⽤程序⽤户线程有⾜够的内存可⽤。因此,CMS收集器不能像其他收集器那样等到⽼年代⼏乎完全被填满了再进⾏收 集,⽽是当堆内存使⽤率达到某⼀阈值时,便开始进⾏回收,以确保应⽤程序在CMS⼯作过程中依然有⾜够的空间⽀持应⽤程序运⾏。要是CMS运⾏期间预留的内存⽆法满⾜程序需要,就会出现⼀ 次"concurrent Mode Failure”失败,这时虚拟机将启动后备预案,临时启⽤serial Old收集器来重新进⾏⽼年代的垃圾收集,这样停顿时间就很⻓了。
CMS收集器的垃圾收集算法采⽤的是标记-清除算法,这意味着每次执⾏完内存回收后,由于被执⾏内存 回收的⽆⽤对象所占⽤的内存空间极有可能是不连续的⼀些内存块,不可避免地将会产⽣⼀些内存碎⽚。那么CMS在为新对象分配内存空间时,将⽆法使⽤指针碰撞(Bump the pointer)技术,⽽只能够选择空闲列表(Free List)执⾏内存分配。

有⼈会觉得既然Mark Sweep会造成内存碎⽚,那么为什么不把算法换成Mark Compact呢?
答案其实很简答,因为当并发清除的时候,⽤Compact整理内存的话,原来的⽤户线程使⽤的内存还怎么⽤呢?要保证⽤户线程能继续执⾏,前提的它运⾏的资源不受影响嘛。Mark Compact更适合"Stop the World,这种场景下使⽤。
CMS的优点:
- 并发收集
- 低延迟
CMS的弊端:
- 会产⽣内存碎⽚,导致并发清除后,⽤户线程可⽤的空间不⾜。在⽆法分配⼤对象的情况下,不得不提前触发Full GC。
- CMS收集器对CPU资源⾮常敏感。在并发阶段,它虽然不会导致⽤户停顿,但是会因为占⽤了⼀部分线程⽽导致应⽤程序变慢,总吞吐量会降低。
- CMS收集器⽆法处理浮动垃圾。可能出现“concurrent Mode Failure"失败⽽导致另⼀次Full GC的 产⽣。在并发标记阶段由于程序的⼯作线程和垃圾收集线程是同时运⾏或者交叉运⾏的,那么在并发标记阶段如果产⽣新的垃圾对象,CMS将⽆法对这些垃圾对象进⾏标记,最终会导致这些新产⽣的垃圾对象没有被及时回收,从⽽只能在下⼀次执⾏GC时释放这些之前未被回收的内存空间。
参数配置
- -XX:+UseConcMarkSweepGC ⼿动指定使⽤CMS收集器执⾏内存回收任务。
开启该参数后会⾃动将 -XX:+UseParNewGC打开。即:ParNew(Young区⽤)+ [CMS(Old区⽤) +SeriaOld]的组合。 - -XX:CMSlnitiatingOccupanyFraction设置堆内存使⽤率的阈值,⼀旦达到该阈值,便开始进⾏回收。
1. JDK5及以前版本的默认值为68,即当⽼年代的空间使⽤率达到68时,会执⾏⼀次CMS回收。
2. JDK6 及以上版本默认值为92% 如果内存增⻓缓慢,则可以设置⼀个稍⼤的值,⼤的阈值可以有效降低CMS的触发频率,减少⽼年 代回收的次数可以较为明显地改善应⽤程序性能。反之,如果应⽤程序内存使⽤率增⻓很快,则应该降低这个阈值,以避免频繁触发⽼年代串⾏收集器。因此通过 该选项便可以有效降低Full GC的执⾏次数。 - -XX:+UseCMSCompactAtFullCollection⽤于指定在执⾏完Full GC后对内存空间进⾏压缩整理,以此避 免内存碎⽚的产⽣。不过由于内存压缩整理过程⽆法并发执⾏,所带来的问题就是停顿时间变得更⻓了。
- -XX:CMSFullGCsBeforeCompaction设置在执⾏多少次Full GC后对内存空间进⾏压缩整理。
- -XX:ParallelCMSThreads设置CMS的线程数量。
CMS默认启动的线程数是(parallelGCThreads+3)/4,ParallelGCThreads是年轻代并⾏收集器的线程数。当CPU资源⽐较紧张时,受到CMS收集器线程的影响,应⽤程序的性能在垃圾回收阶段可能会⾮常糟糕。
⼩结
HotSpot有这么多的垃圾回收器,那么如果有⼈问,serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?
请记住以下⼝令:
如果你想要最⼩化地使⽤内存和并⾏开销,请选serial GC:
如果你想要最⼤化应⽤程序的吞吐量,请选 parallel GC;
如果你想要最⼩化GC的中断或停顿时间,请选CMS GC。
JDK后续版本中CMS的变化
JDK9新特性:
CMS被标记为Deprecate了(JEP291) 如果对JDK9及以上版本的Hotspot虚拟机使⽤参数-XX: +UseConcMarkSweepGC来开启CMS收集器的话,⽤户会收到⼀个警告信息,提示CMS未来将会 被废弃。
JDK14新特性:
删除CMS垃圾回收器(JEP363) 移除了CMS垃圾收集器,如果在JDK14中使⽤-XX:+UseconcMarkSweepGC的话,JVM不会报错, 只是给出⼀个warning信息,但是不会exit。JVM会⾃动回退以默认GC⽅式启动JVM。
3.5 G1回收器:区域化分代式
既然我们⼰经有了前面⼏个强⼤的GC,为什么还要发布Garbage First(G1) GC?
原因就在于应⽤程序所应对的业务越来越庞⼤、复杂,⽤户越来越多,没有GC就不能保证应⽤程序正常 进⾏,⽽经常造成STW的GC⼜跟不上实际的需求,所以才会不断地尝试对GC进⾏优化。G1(GarbageFirst)垃圾回收器是在Java7 update 4之后引⼊的⼀个新的垃圾回收器,是当今收集器技术发展的最前沿 成果之⼀。
与此同时,为了适应现在不断扩⼤的内存和不断增加的处理器数量,进⼀步降低暂停时间(pause time),同时兼顾良好的吞吐量。 官⽅给G1设定的⽬标是在延迟可控的情况下获得尽可能⾼的吞吐ᰁ,所以才担当起“全功能收集器”的重任与期望。
为什么名字叫做Garbage First(G1)呢?
因为G1是⼀个并⾏回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使⽤不 同的Region来表示Eden、幸存者0区,幸存者1区,⽼年。
G1 GC有计划地避免在整个Java堆中进⾏全区域的垃圾收集。G1跟踪各个Region⾥⾯的垃圾堆积的价值⼤⼩(回收所获得的空间⼤⼩以及回收所需时间的经验值),在后台维护⼀个优先列表,每次根据允许的收集时间,优先回收价值最⼤的Region。
由于这种⽅式的侧重点在于回收垃圾最⼤量的区间(Region),所以我们给G1⼀个名字:垃圾优先 (Garbage First)。
什么是Garbage First(G1) GC
1. G1(Garbage-First)是⼀款⾯向服务端应⽤的垃圾收集器,主要针对配备多核CPU及⼤容量内存的机器,以极⾼概率满⾜GC停顿时间的同时,还兼具⾼吞吐量的性能特征。
2. 在JDKI.7版本正式启⽤,移除了Experimental的标识,是JDK9以后的默认垃圾回收器,取代了CMS 回收器以及parallel+Parallel Old组合。被Oracle官⽅称为“全功能的垃圾收集器”
3. 与此同时,CMS⼰经在JDK9中被标记为废弃(deprecated)。在jdk8中还不是默认的垃圾回收器,需要使⽤-XX:+UseG1GC来启⽤。
优势
与其他GC收集器相⽐,G1使⽤了全新的分区算法,其特点如下所示:
并⾏与并发
- 并⾏性:G1在回收期间,可以有多个GC线程同时⼯作,有效利⽤多核计算能⼒。此时⽤户线程 STW
- 并发性:G1拥有与应⽤程序交替执⾏的能⼒,部分⼯作可以和应⽤程序同时执⾏,因此,⼀般来 说,不会在整个回收阶段发⽣完全阻塞应⽤程序的情况
分代收集
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和⽼年代,年轻代依然有Eden区和 Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者⽼年代都是连续的,也不再坚 持固定⼤⼩和固定数量。
- 将堆空间分为若⼲个区域(Region),这些区域中包含了逻辑上的年轻代和⽼年代。
- 和之前的各类回收器不同,它同时兼顾年轻代和⽼年代。对⽐其他回收器,或者⼯作在年轻代,或 者⼯作在⽼年代。

空间整合
- CMS:“标记-清除”算法、内存碎⽚、若⼲次GC后进⾏⼀次碎⽚整理
- G1将内存划分为⼀个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-compact)算法,两种算法都可以避免内存碎⽚。这种特性有利于程序⻓时间运⾏,分配⼤对象时不会因为⽆法找到连续内存空间⽽提前触发下⼀次GC。 尤其是当Java堆⾮常⼤的时候,G1的优势更加明显。
可预测的停顿时间模型(即:软实时soft real-time)
- 这是G1相对于CMS的另⼀⼤优势,G1除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为 M 毫秒的时间⽚段内,消耗在垃圾收集上的时间不得超过 M 毫秒。由于分区的原因,G1可以只选取部分区域进⾏内存回收,这样缩⼩了回收的范围,因此对于全局停顿情况的发⽣也能得到较好的控制。
- G1跟踪各个Region⾥⾯的垃圾堆积的价值⼤⼩(回收所获得的空间⼤⼩以及回收所需时间的经验值),在后台维护⼀个优先列表,每次根据允许的收集时间,优先回收价值最⼤的Region。保证了 G1收集器在有限的时间内可以获取尽可能⾼的收集效率。
- 相⽐于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
缺点
1. 相较于CMS,G1还不具备全⽅位、压倒性优势。⽐如在⽤户程序运⾏过程中,G1⽆论是为了垃圾收集产⽣的内存占⽤(Footprint)还是程序运⾏时的额外执⾏负载(Overload)都要⽐CMS要⾼。
2. 从经验上来说,在⼩内存应⽤上CMS的表现⼤概率会优于G1,⽽G1在⼤内存应⽤上则发挥其优势。平衡点在6-8GB之间。
参数配置
1. -XX:+UseG1GC ⼿动指定使⽤G1收集器执⾏内存回收任务。
2. -XX:G1HeapRegionSize设置每个Region的⼤⼩。值是2的幂,范围是1MB到32MB之间,⽬标是根据最⼩的Java堆⼤⼩划分出约2048个区域。默认是堆内存的1/2000。举例: -XX:G1HeapRegionSize=2m
3. -XX:MaxGCPauseMillis 设置期望达到的最⼤GC停顿时间指标(JVM会尽⼒实现,但不保证达到)。默认值是200ms。举例: -XX:MaxGCPauseMillis=100
4. -XX:ParallelGCThread 设置STW时GC并发线程数的值。最多设置为8。
5. -XX:ConcGCThreads设置并发标记的线程数。将n设置为并⾏垃圾回收线程数(ParallelGCThreads) 的1/4左右。
6. -XX:initiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占⽤率阈值。超过此值,就触发GC。默认值是45。
操作步骤
G1的设计原则就是简化JVM性能调优,开发⼈员只需要简单的三步即可完成调优:
第⼀步:开启G1垃圾收集器
第⼆步:设置堆的最⼤内存
第三步:设置最⼤的停顿时间
G1中提供了三种垃圾回收模式:Young GC、Mixed GC和Full GC,在不同的条件下被触发。
适⽤场景
1. ⾯向服务端应⽤,针对具有⼤内存、多处理器的机器。(在普通⼤⼩的堆⾥表现并不惊喜)
2. 最主要的应⽤是需要低GC延迟,并具有⼤堆的应⽤程序提供解决⽅案。如:在堆⼤⼩约6GB或更⼤ 时,可预测的暂停时间可以低于0.5秒:(G1通过每次只清理⼀部分⽽不是全部的Region的增ᰁ式 清理来保证每次GC停顿时间不会过⻓)。
3. ⽤来替换掉JDK1.5中的CMS收集器;在下⾯的情况时,使⽤G1可能⽐CMS好:
超过50%的Java堆被活动数据占⽤;
对象分配频率或年代提升频率变化很⼤:
GC停顿时间过⻓(⻓于0.5⾄1秒)。
4. Hotspot垃圾收集器⾥,除了G1以外,其他的垃圾收集器使⽤内置的JVM线程执⾏GC的多线程操作,⽽G1 GC可以采⽤应⽤线程承担后台运⾏的GC⼯作,即当JVM的GC线程处理速度慢时,系统会调⽤应⽤程序线程帮助加速垃圾回收过程。
分区Region:化整为零
使⽤G1收集器时,它将整个Java堆划分成约2048个⼤⼩相同的独⽴Region块,每个Region块⼤⼩根据 堆空间的实际⼤⼩⽽定,整体被控制在1MB到32MB之间,且为2的N次幂,即 1MB,2MB,4MB,8MB,16MB,32MB。可以通过-XX:G1HeapRegionsize=n设定。所有的Region⼤⼩相同, 且在JVM⽣命周期内不会被改变。
虽然还保留有新⽣代和⽼年代的概念,但新⽣代和⽼年代不再是物理隔离的了,它们都是⼀部分 Region(不需要连续)的集合。通过Region的动态分配⽅式实现逻辑上的连续。

⼀个region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是⼀个region只可能属于⼀个⻆ ⾊。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区 域。图中空⽩的表示未使⽤的内存空间。 G1垃圾收集器还增加了⼀种新的内存区域,叫做Humongous内存区域,如图中的H块。主要⽤于存储⼤对象,如果超过1.5个region,就放到H。
设置H的原因:
对于堆中的⼤对象,默认直接会被分配到⽼年代,但是如果它是⼀个短期存在的⼤对象,就会对垃圾收集器造成负⾯影响。为了解决这个问题,G1划分了⼀个Humongous区,它⽤来专⻔存放⼤对象。如果 ⼀个H区装不下⼀个⼤对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启 动Full GC。G1的⼤多数⾏为都把H区作为⽼年代的⼀部分来看待。
RememberedSet
⼀个对象被不同区域引⽤的问题
- ⼀个Region不可能是孤⽴的,⼀个Region中的对象可能被其他任意Region中对象引⽤,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
- 在其他的分代收集器,也存在这样的问题(⽽G1更突出)
- 回收新⽣代也不得不同时扫描⽼年代? 这样的话会降低Minor GC的效率
解决⽅法:
⽆论G1还是其他分代收集器,JVM都是使⽤Remembered Set来避免全局扫描。
每个Region都有⼀个对应的Remembered Set;
每次Reference类型数据写操作时,都会产⽣⼀个Write Barrier暂时中断操作;
然后检查将要写⼊的引⽤指向的对象是否和该Reference类型数据在不同的Region(其他收集器: 检查⽼年代对象是否引⽤了新⽣代对象)
如果不同,通过cardTable把相关引⽤信息记录到引⽤指向对象的所在Region对应的Remembered Set中;
当进⾏垃圾收集时,在GC根节点的枚举范围加⼊Remembered Set;就可以保证不进⾏全局扫描,也不会有遗漏。

垃圾回收过程
G1 GC的垃圾回收过程主要包括如下三个环节:
1. 年轻代GC(Young GC)
2. ⽼年代并发标记过程(concurrent Marking)
3. 混合回收(Mixed GC)
4. 如果需要,单线程独占式、⾼强度的Full GC还是继续存在的。针对GC的评估失败提供了⼀种失败保护机制,即强⼒回收。

顺序:young gc -> young gc + concurrent mark -> Mixed GC 顺序,进⾏垃圾回收。
具体流程
1. 应⽤程序分配内存,当年轻代的Eden区⽤尽时开始年轻代回收过程:G1的年轻代收集阶段是⼀个 并⾏的独占式收集器。在年轻代回收期,G1 GC暂停所有应⽤程序线程,启动多线程执⾏年轻代回 收然后从年轻代区间移动存活对象到Survivor区间或者⽼年区间,也有可能是两个区间都会涉及。
2. 当堆内存使⽤达到⼀定值(默认45%)时,开始⽼年代并发标记过程。
3. 标记完成⻢上开始混合回收过程。对于⼀个混合回收期,G1 GC从⽼年区间移动存活对象到空闲区间,这些空闲区间也就成为了⽼年代的⼀部分。和年轻代不同,⽼年代的G1回收器和其他GC不同,G1的⽼年代回收器不需要整个⽼年代被回收,⼀次只需要扫描/回收⼀⼩部分⽼年代的Region 就可以了。同时,这个⽼年代Region是和年轻代⼀起被回收的。
举个例⼦:⼀个web服务器,Java进程最⼤堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配⼤ 约2G的内存。G1会每45秒钟进⾏⼀次年轻代回收,每31个⼩时整个堆的使⽤率会达到45%,会开始⽼年代并发标记过程,标记完成后开始四到五次的混合回收
年轻代GC
JVM启动时,G1先准备好Eden区,程序在运⾏过程中不断创建对象到Eden区,当Eden空间耗尽时,G1 会启动⼀次年轻代垃圾回收过程。年轻代垃圾回收只会回收Eden区和survivor区。
YGC时,⾸先G1停⽌应⽤程序的执⾏(Stop-The-world),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。

G1回收过程⼀:年轻代GC
第⼀阶段,扫描根。
- 根是指static变量指向的对象,正在执⾏的⽅法调⽤链条上的局部变量等。根引⽤连同RSet记录的外部引⽤作为扫描存活对象的⼊⼝。
第⼆阶段,更新RSet。
- 处理dirty card queue中的card,更新RSet。此阶段完成后,RSet可以准确的反映⽼年代对所在的 内存分段中对象的引⽤。
第三阶段,处理RSet。
- 识别被⽼年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
第四阶段,复制对象。
- 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到survivor区中空的内存分段 survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中 空的内存分段。如果survivor空间不够,Eden空间的部分数据会直接晋升到⽼年代空间。
第五阶段,处理引⽤。
- 处理Soft,Weak,Phantom,Final,JNI Weak等引⽤。最终Eden空间的数据为空,GC停⽌⼯ 作,⽽⽬标内存中的对象都是连续存储的,没有碎⽚,所以复制过程可以达到内存整理的效果,减 少碎⽚。
对于应⽤程序的引⽤赋值语句object field=object,JVM会在之前和之后执⾏特殊的操作以在dirtycard queue中⼊队⼀个保存了对象引⽤信的card。在年经代回收的时候,G1会对Dirty CardQueue中所有的card进⾏处理,以更新RSet,保证RSet实时准确的反映引⽤关系。 那为什么不在引⽤赋值句处直更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很⼤,使⽤队列性能会好很多。
G1回收过程⼆:并发标记过程
- 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发⼀次年轻代GC。
- 根区域扫描(Root Region Scanning): G1 GC扫描Survivor区直接可达的⽼年代区域对象,并标记被引⽤的对象。这⼀过程必须在young GC之前完成。
- 并发标记(Concurrent Marking):在整个堆中进⾏并发标记(和应⽤程序并发执⾏),此过程可能 被youngGC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被⽴即 回收。同时,并发标记过程中,会计算每个区域的对象活性〈区域中存活对象的⽐例)。
- 再次标记(Remark):由于应⽤程序持续进⾏,需要修正上⼀次的标记结果。是STW的。G1中采⽤ 了⽐CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)
- 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收⽐例,并进⾏排序,识别可以混合回 收的区域。为下阶段做铺垫。是STW的。 这个阶段并不会实际上去做垃圾的收集.
- 并发清理阶段:识别并清理完全空闲的区域。
G1回收过程三:混合回收
当越来越多的对象晋升到⽼年代OId region时,为了避免堆内存被耗尽,虚拟机会触发⼀个混合的垃圾收集器,即Mixed GC,该算法并不是⼀个Old GC,除了回收整个Young Region,还会回收⼀部分的 Old Region.
这⾥需要注意:是⼀部分⽼年代,⽽不是全部⽼年代。可以选择哪些Old Region进⾏收集,从⽽可以对垃圾回收的耗时时间进⾏控制。也要注意的是Mixed GC并不是Full GC
并发标记结束以后,⽼年代中全部为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出 来。默认情况下,这些⽼年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收。
混合回收的回收集(Collection Set)包括⼋分之⼀的⽼年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全⼀样,只是回收集多了⽼年代的内存分段。具体过程请参考上⾯的年轻代回收过程。 由于⽼年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段⽐例越⾼ 的,越会被先回收。并且有⼀个阈值会决定内存分段是否被回收,- XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段⽐例要达到65%才会被回收。如果垃圾占⽐太低,意味着存活的对象占⽐⾼,在复制的时候会花费更多的时间。 混合回收并不⼀定要进⾏8次。有⼀个阈值-XX:G1HeapwastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的⽐例低于10%,则不再进⾏混合回收。因为GC会花费很多的时间但是回收到的内存却很少。
G1回收过程四:Full GC
G1的初衷就是要避免Full GC的出现。但是如果上述⽅式不能正常⼯作,G1会停⽌应⽤程序的执⾏(stopThe-world),使⽤单线程的内存回收算法进⾏垃圾回收,性能会⾮常差,应⽤程序停顿时间会很⻓。
要避免Full GC的发⽣,⼀旦发⽣需要进⾏调整。什么时候会发⽣Full GC呢?⽐如堆内存太⼩,当G1在 复制存活对象的时候没有空的内存分段可⽤,则会回退到full gc,这种情况可以通过增⼤内存解决。
导致G1Full GC的原因可能有两个:
- Evacuation的时候没有⾜够的to-space来存放晋升的对象;
- 并发处理过程完成之前空间耗尽。
从oracle官⽅透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与⽤户程序⼀起并发执⾏,但这件事清做起来⽐较复杂,考虑到G1只是回收⼀部分Region,停顿时间是⽤户可控制的,所以并不迫切去实现,
⽽选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。另外,还考虑到G1不是仅仅⾯向低延迟,停顿⽤户线程能够最⼤幅度提⾼垃圾收集效率,为了保证吞吐ᰁ所以才选择了完全暂停⽤户线程的实现⽅案。
G1回收器优化建议
年轻代⼤⼩,官方文档说使用G1回收器的时候不要指定年轻代大小,使用MaxGCPauseMillis参数让JVM自行决定大小
- 避免使⽤-Xmn或-XX:NewRatio等相关选项显式设置年轻代⼤⼩
- 固定年轻代的⼤⼩会覆盖暂停时间⽬标
暂停时间⽬标不要太过严苛
- G1 GC的吞吐量⽬标是90%的应⽤程序时间和10%的垃圾回收时间
- 评估G1 GC的吞吐量时,暂停时间⽬标不要太严苛。⽬标太过严苛表示你愿意承受更多的垃圾回收开销,⽽这些会直接影响到吞吐量。
垃圾回收器总结
截⽌JDK 1.8,⼀共有7款不同的垃圾收集器。每⼀-款不同的垃圾收集器都有不同的特点,在具体使⽤的 时候,需要根据具体的情况选⽤不同的垃圾收集器。

GC发展阶段:
Serial=>Parallel(并⾏) =>CMS(并发) =>G1=>ZGC
怎么选择垃圾回收器?
Java垃圾收集器的配置对于JVM优化来说是⼀个很重要的选择, 选择合适的垃圾收集器可以让JVM的性能有⼀个很⼤的提升。
优先调整堆的⼤⼩让JVM⾃适应完成。
| 串行收集器 | 并行收集器 | 并发收集器 | G1 |
|
多CPU、需要⾼吞吐ᰁ、允许停顿时间超过1秒, |
是多CPU、追求低停顿时间, 需快速响应 (⽐如延迟不能超过1秒, 如互联⽹应⽤) |
多处理器和大容量内存环境中,在实现高吞吐量的同时,低停顿。 现在互联⽹的项⽬,基本都是使⽤G1。 |
对于垃圾收集,⾯试官可以循序渐进从理论、实践各种⻆度深⼊,也未必是要求⾯试者什么都懂。但如果你懂得原理,⼀定会成为⾯试中的加分项。
这⾥较通⽤、基础性的部分如下:
- 垃圾收集的算法有哪些?
- 如何判断⼀个对象是否可以回收?
- 垃圾收集器⼯作的基本流程
4. GC日志分析
通过阅读GC⽇志, 我们可以了解Java虚拟机内存分配与回收策略。
内存分配与垃圾回收的参数列表
- -XX:+PrintGC 输出GC⽇志。类似:-verbose:gc
- -XX:+PrintGCDetails 输出GC的详细⽇志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以⽇期的形式,如2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进⾏GC的前后打印出堆的信息
- -Xloggc:../logs/gc.log ⽇志⽂件的输出路径
GC 日志说明:

- "[GC"和"[FullGC"说明了这次垃圾收集的停顿类型, 如果有"Full"则说明本次GC是Full GC发⽣了"Stop The World".
- 使⽤Serial收集器在新⽣代的名字是Default New Generation, 因此显示的是"[DefNew"
- 使⽤ParNew收集器在新⽣代的名字会变成"[ParNew", 意思是"Parallel New Generation"
- 使⽤Parallel Scavenge收集器在新⽣代的名字是"[PSYoungGen"
- ⽼年代的收集和新⽣代道理⼀样,名字也是收集器决定的
- 使⽤G1收集器的话, 会显示为"garbage-first heap"
- Allocation Failure 表明本次引起GC的原因是因为在年轻代中没有⾜够的空间能够存储新的数据了。
[PSYoung Gen:nK->nK(nK)]nK->nK(nK) [Times: user=0.00 sys=0.00, real=0.00 secs]
- 中括号内:GC回收前年轻代使用⼤⼩,回收后使用⼤⼩,(年轻代总⼤⼩)
- 括号外:GC回收前年轻代和⽼年代总共使用⼤⼩,回收后总共使用⼤⼩,(年轻代和⽼年代总⼤⼩)
- user代表⽤户态回收耗时, sys内核态回收耗时, rea实际耗时。由于多核的原因, 时间总和可能会超过real时间
Heap(堆)
PSYoungGen(Parallel Scavenge收集器新⽣代)
eden space(堆中的Eden区默认占⽐是8)
from space(堆中的Survivor, 这⾥是From Survivor区默认占⽐是1)
to space(堆中的Survivor, 这⾥是to Survivor区默认占⽐是1, 需要先了解⼀下堆的分配策略)
ParOldGen(⽼年代总⼤⼩和使⽤⼤⼩) object space(显示个使⽤百分⽐)
Metaspace(元空间总⼤⼩和使⽤⼤⼩)
如果是G1收集器,堆信息像如下:
Heap
garbage-first heap total 28672K, used 19552K [0x00000000fe200000, 0x00000000fe3000e0, 0x0000000100000000)
region size 1024K, 2 young (2048K), 1 survivors (1024K)
Metaspace used 3811K, capacity 4536K, committed 4864K, reserved 1056768K
class space used 410K, capacity 428K, committed 512K, reserved 1048576K


GC⽇志分析⼯具
可以⽤⼀些⼯具去分析这些gc⽇志。 常⽤的⽇志分析⼯具有: GC Viewer、GCEasy、GCHisto、GCLogViewer、 Hpjmeter、garbagecat等。
垃圾回收器的新发展
GC仍然处于⻜速发展之中,⽬前的默认选项G1 GC在不断的进⾏改进,很多我们原来认为的缺点, 例如 串⾏的Full GC、Card Table扫描的低效等, 都已经被⼤幅改进, 例如, JDK 10以后, Full GC已经是并 ⾏运⾏, 在很多场景下, 其表现还略优于Parallel GC的并⾏Full GC实现。 即使是Serial GC, 虽然⽐较古⽼, 但是简单的设计和实现未必就是过时的, 它本身的开销,不管是GC 相关数据结构的开销,还是线程的开销,都是⾮常⼩的, 所以随着云计算的兴起, 在Serverless等新的应⽤场景下, Serial GC找到了 新的舞台。 ⽐较不幸的是CMS GC, 因为其算法的理论缺陷等原因, 虽然现在还有⾮常⼤的⽤户群体, 但在JDK9中 已经被标记为废弃, 并在JDK14版本中移除。 现在G1回收器已成为默认回收器好⼏年了。 我们还看到了引⼊了两个新的收集器: ZGC(JDK11出现) 和Shenandoah (OpenJDK 12) 。
主打特点:低停顿时间
OpenJDK 12的shenandoah GC
OpenJDK 12的shenandoah GC: 低停顿时间的GC(实验性) Shenandoah⽆疑是众多GC中最孤ᇿ的⼀个。是第⼀款不由Oracle公司团队领导开发的HotSpot垃圾收 集器。不可避免的受到官⽅的排。⽐如号称Open JDK和 Oracle JDK没有区别的Oracle公司仍拒绝在OracleJDK 12中⽀持Shenandoah。 Shenandoah垃圾回收器最初由RedHat进⾏的⼀项垃圾收集器研究项⽬Pauseless GC的实现, 旨在针对JVM上的内存回收实现低停顿的需求。在2014年贡献给OpenJDK。 Red Hat研发Shenandoah团队对外宣称, Shenandoah垃圾回收器的暂停时间与堆⼤⼩⽆关,这意味 着⽆论将堆设置为200MB还是200GB,99.9%的⽬标都可以把垃圾收集的停顿时间限制在⼗毫秒以内。 不过实际使⽤性能将取决于实际⼯作堆的⼤⼩和⼯作负载。
- Shenandoah GC的弱项:⾼运⾏负担下的吞吐量下降。
- Shenandoah GC的强项:低延迟时间
⾰命性的ZGC
ZGC与Shenandoah⽬标⾼度相似,在尽可能对吞吐量影响不⼤的前提下,实现在任意堆内存⼤⼩下都 可以把垃圾收集的停顿时间限制在⼗毫秒以内的低延迟。
《深⼊理解Java虚拟机》⼀书中这样定义ZGC: ZGC收集器是⼀款基于Region内存布局的,(暂时)不设分代的,使⽤了读屏障、染⾊指针和内存多重映射等技术来实现可并发的标记⼀压缩算法的,以低延迟为⾸要⽬标的⼀款垃圾收集器。
ZGC的⼯作过程可以分为4个阶段:并发标记⼀并发预备重分配⼀并发重分配⼀并发重映射等。 ZGC⼏乎在所有地⽅并发执⾏的,除了初始标记的是sTW的。所以停顿时间⼏乎就耗费在初始标记上, 这部分的实际时间是⾮常少的。
虽然ZGC还在试验状态, 没有完成所有特性, 但此时性能已经相当亮眼,⽤“令⼈震惊、 ⾰命性”来形容,不为过。 未来将在服务端、⼤内存、低延迟应⽤的⾸选垃圾收集器。
JDK14新特性
JEP 364: ZGC应⽤在macOS上 JEP
365: ZGC应⽤在windows上
JDK14之前,ZGC仅Linux才⽀持
尽管许多使⽤ZGC的⽤户都使⽤类Linux的环境,但在Windows和macOS 上,⼈们也需要ZGC进⾏ 开发部署和测试。许多桌⾯应⽤也可以从ZGC中受益。因此,ZGC特性被移植到了Windows和 macOs上。
现在mac或Windows 上也能使⽤ZGC了,示例如下: -XX: +UnlockExperimentalVMOptions -XX:+UseZGC
其他垃圾回收器: AliGC
AliGC是阿⾥巴巴JVM团队基于G1算法,⾯向⼤堆(LargeHeap)应⽤场景。当然,其他⼚商也提供了各种独具⼀格的GC实现,例如⽐较有名的低延迟GC, Zing,有兴趣可以参考官⽹。

浙公网安备 33010602011771号