Java gc完整认识和常见问题
Java语言的独有的自动垃圾回收机制,可以认为是成也gc,败也gc; 好处是不用像C/C++一样自己要主动写代码去回收对象,能更好的专注业务逻辑开发;但是坏处是你必须关注gc,了解原理,否则将会出现各种问题。特别是OutOfMemory
和StopTheWorld
问题。所以生产项目必须要配置gc相关告警,这将有助于提早发现问题,往往这种告警有时候会比业务告警更加的及时和提前。
目录
- gc基础认识
- 谈论GC前必须要了解的一些知识
- Stop The World(STW)
- HotSpot 算法实现可达性分析
- 通过可达性分析来判定对象是否存活(什么是垃圾?)
- 可作为(GC Roots)的对象
- 4种引用(Reference)
- 强引用
- 软引用(SoftReference)
- 弱引用(WeakReference)
- 虚引用(PhantomReference)
- gc线程两次标记 & F-Queue & Finalizer线程
- `finalize()`自救代码演示
- 补充堆外内存
- DirectByteBuffer 如何回收
- 堆外内存溢出
- 堆外内存更适合存储的对象
- gc的发展
- (一) Serial(STW,单线程收集器)
- Serial Old
- (二) Parallel Scavenge(STW,多线程收集器)
- Parallel old
- ParNew (Parallel Scavenge的增强版)
- (三) CMS收集器(并发的垃圾回收器)
- (四) G1
- (五) ZGC
- gc组合
- 查看自己的java程序是什么gc
- 分代gc中 `new object()`的gc生命周期
- 线程TLAB局部缓存区域(Thread Local Allocation Buffer)
- 逃逸分析对象分配到栈
- 大对象直接进入老年代
- 长期存活的(`Age>某个值`)对象将进入老年代
- 动态对象的年龄判定
- gc术语认识
- Minor GC/ Young GC
- Major GC(老年代GC)
- Full GC
- jstat -gcutil
- CMS
- CMS流程
- 初始标记(CMS initial mark),需要短暂的`Stop The World`
- 并发标记(CMS concurrent mark)(一定会有错误标记)
- 重新标记(CMS remark)正确标记,需要`Stop The World`
- 并发清除(CMS concurrent sweep)
- 重置计数器(Reset Counting)
- CMS垃圾回收器的缺点
- CMS GC问题分析与解决
- G1
- g1相关概念
- g1流程
- young gc/minor gc(对年轻代的GC)
- mixed gc(young 和 old都gc)
- mix gc的过程
- G1停顿耗时的主要瓶颈
gc基础认识
目前生产项目上主流的仍然是CMS和G1两种,如下两张图
CMS作用于分代gc的老年代:
G1对于年轻代,老年代都作用,堆区域划分成如下图:
即
- 整个堆被分为一个大小相等的region集合,每个region是逻辑上连续的虚拟内存区域
- 逻辑上是分代的,但是物理上不分代,是大小相等的region区域
- Humongous区域:如果一个对象占用的空间超过了region容量(
region size
)50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在old
region,为了能找到连续的region来分配巨型对象,有时候不得不启动Full GC- H-obj直接分配到了old gen,防止了反复拷贝移动
- H-obj在
global concurrent marking
阶段的cleanup
和full GC
阶段回收 - 在分配H-obj之前先检查是否超过
initiating heap occupancy percent
和the marking threshold
, 如果超过的话,就启动global concurrent marking
,为的是提早回收,防止evacuation failures
和full GC
。
另外部分大项目采用zgc
谈论GC前必须要了解的一些知识
Stop The World(STW)
There is a term that you should know before learning about GC. The term is "stop-the-world." Stop-the-world will occur no matter which GC algorithm you choose. Stop-the-world means that the JVM is stopping the application from running to execute a GC. When stop-the-world occurs, every thread except for the threads needed for the GC will stop their tasks. The interrupted tasks will resume only after the GC task has completed. GC tuning often means reducing this stop-the-world time.
只有GC线程工作,其它线程都停止;当GC线程工作完成,其它线程恢复
HotSpot 算法实现可达性分析
可达性分析的"一致性"分析,GC进行时必须停顿所有Java执行线程
虚拟机有办法直接得知哪些地方存放着对象的引用,HotSpot中使用一组称为OopMap
的数据结构来达到这个目的:在类加载完成的时候,HotPot就把对象内什么偏移量是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
借助OopMap
,HotSpot可以快速且准确地完成GC Roots枚举,问题:
- 可能导致引用关系变化,或者说OopMap内容变化的指令特别多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高
实际上,HotSpot没有为每条指令都生成oopMap
, 安全点(Sagepoint
) GC, 选举以"是否具有让程序长时间执行的特征"为标准进行选定,如方法调用,循环跳转,异常跳转等,这些功能的指令会产生安全点
通过可达性分析来判定对象是否存活(什么是垃圾?)
算法的基本思路就是通过一系列的称为GC Roots
的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain)
,当一个对象到GC Roots
没有任何引用链相连(用图论的话来说,就是GC Roots
到这个对象不可达)时,则证明此对象是不可用的。
可作为(GC Roots)的对象
包括如下:
JVM stack, native method stack, run-time constant pool, static references in method area, Clazz
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 方法区中常量池引用的对象
- 方法区中的类静态属性引用的对象
- 加载的Clazz
参与:https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/
4种引用(Reference)
参考:types-references-java
强引用
使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如
//Strong Reference - by default
Gfg g = new Gfg();
//Now, object to which 'g' was pointing earlier is
//eligible for garbage collection.
g = null;
- 强引用与GC
static class M{
@Override
protected void finalize() {
System.out.println("finalize");
}
}
/**
* 强引用,强制gc
* 输出如下
* Main$M@736e9adb
* null
* finalize
*/
static void testM(){
M m = new M();
System.out.println(m);
m = null;
System.gc();
System.out.println(m);
}
软引用(SoftReference)
In Soft reference, even if the object is free for garbage collection then also its not garbage collected, until JVM is in need of memory badly.The objects gets cleared from the memory when JVM runs out of memory.To create such references java.lang.ref.SoftReference class is used.(JVM 内存紧张的时候会回收软引用对象)
用来描述一些还有用但并非必需的对象,对于软引用关联者的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
//Code to illustrate Soft reference
import java.lang.ref.SoftReference;
class Gfg
{
//code..
public void x()
{
System.out.println("GeeksforGeeks");
}
}
public class Example
{
public static void main(String[] args)
{
// Strong Reference
Gfg g = new Gfg();
g.x();
// Creating Soft Reference to Gfg-type object to which 'g'
// is also pointing.
SoftReference<Gfg> softref = new SoftReference<Gfg>(g);
// Now, Gfg-type object to which 'g' was pointing
// earlier is available for garbage collection.
g = null;
// You can retrieve back the object which
// has been weakly referenced.
// It successfully calls the method.
g = softref.get();
g.x();
}
}
- 软引用与GC
/**
* 内存不够用了,软引用才被回收
* 输出如下
* [B@736e9adb
* [B@736e9adb
* [B@6d21714c
* null
*/
static void testSoftReference() throws Exception{
SoftReference<byte[]> mSoft = new SoftReference<>(new byte[10 * 1024 * 1024]);
System.out.println(mSoft.get());
System.gc();
TimeUnit.SECONDS.sleep(1);
// gc没有回收软引用
System.out.println(mSoft.get());
byte[] b = new byte[11 * 1024 * 1024];
System.out.println(b);
// 由于强引用申请空间不够,必须要清除软引用了
System.out.println(mSoft.get());
}
弱引用(WeakReference)
// Strong Reference
Gfg g = new Gfg();
g.x();
// Creating Weak Reference to Gfg-type object to which 'g'
// is also pointing.
WeakReference<Gfg> weakref = new WeakReference<Gfg>(g);
//Now, Gfg-type object to which 'g' was pointing earlier
//is available for garbage collection.
//But, it will be garbage collected only when JVM needs memory.
g = null;
// You can retrieve back the object which
// has been weakly referenced.
// It successfully calls the method.
g = weakref.get();
g.x();
-
This type of reference is used in WeakHashMap to reference the entry objects .
-
If JVM detects an object with only weak references (i.e. no strong or soft references linked to any object object), this object will be marked for garbage collection.
-
To create such references java.lang.ref.WeakReference class is used.
-
These references are used in real time applications while establishing a DBConnection which might be cleaned up by Garbage Collector when the application using the database gets closed.
-
弱引用与GC
/**
* 只要有垃圾回收线程执行,弱引用直接会被回收
* 输出如下:
* Main$M@736e9adb
* null
* finalize
*/
static void testWeakReference(){
WeakReference<M> m = new WeakReference<>(new M());
System.out.println(m.get());
System.gc();
System.out.println(m.get());
}
虚引用(PhantomReference)
The objects which are being referenced by phantom references are eligible for garbage collection. But, before removing them from the memory, JVM puts them in a queue called ‘reference queue’ . They are put in a reference queue after calling finalize() method on them.To create such references java.lang.ref.PhantomReference class is used.
也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能够在这个对象被收集器回收时收到一个系统通知。
-
它的get()方法写死了,返回null(也就是跟前面不一样,不能通过get()方法获取被包装的对象)
-
Java中虚幻引用作用:管理直接内存(不属于堆)
-
虚引用与GC
-
虚引用get不到,因为get方法是直接返回的null
-
虚引用放到队列(
java.lang.ref.ReferenceQueue
)中,不断的从队列中取出来看是否被回收了
static final List<Object> LIST = new LinkedList<>();
static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
public static void main(String[] args) throws Exception{
PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
new Thread(()->{
while (true){
LIST.add(new byte[3 * 1024 * 1024]);
try{
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
}
System.out.println(phantomReference.get());
}
}).start();
new Thread(()->{
while (true){
Reference<? extends M> poll = QUEUE.poll();
if(poll != null) {
System.out.println("PhantomReference 被 jvm 回收了:" + poll.get());
}
}
}).start();
TimeUnit.SECONDS.sleep(2);
}
管理堆外内存:
- jvm对象用虚引用,指向堆外内存,虚引用必须和引用队列关联使用
- 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收;如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
gc线程两次标记 & F-Queue & Finalizer线程
第一次:通过GC roots遍历,找到不在引用链内的对象,并检查是否需要执行finalize()方法。(如果没重写finalize()则只需要标记一次,然后就可以gc掉)
在第一次标记中有finalize()
需要被执行的对象,会被丢到一个优先级较低的队列(F-Queue
:java.lang.ref.Finalizer.ReferenceQueue
)中执行,但不保证能被执行(因为是由低优先级的Finalizer线程
去处理的,试想低优先级线程不被执行到,那么重写了finalize()
的对象就永久在堆中不能被gc掉,即java.lang.ref.Finalizer
对象会占用很大的堆空间,甚至溢出)
第二次:对队列(F-Queue
)中的对象再遍历一次,看是否有自救,没有则进行GC
- 对象没有覆盖
finalize()
方法,或者finalize()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行" - 如果这个对象被判定为有必要执行
finalize()
方法,则这个对象会放置在一个叫做F-Queue
的队列之中,并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程
去执行它。
如果对象要在finalize()
中成功拯救自己,则只需要重新与引用链上的任何一个对象建立关联即可,GC对F-Queue
中对对象进行第二次标记时,会将它移出"即将回收"的集合;否则就会被回收
finalize()
自救代码演示
/**
* 此代码说明如下两点:
* 1. 对象可以在GC时自我拯救
* 2. 这种自救的机会只有一次,因为一个对象的`finalize()`方法最多只会被系统自动调用一次
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public String name;
public FinalizeEscapeGC(String name) {
this.name = name;
}
public void isAlive() {
System.out.println("yes, i am still alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
System.out.println(this.name);
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC("abc");
// 对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 执行finalize方法的线程优先级很低,暂停0.5秒等待它执行
Thread.sleep(500);
if(SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead");
}
// 下面这段代码与上面完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if(SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead");
}
}
}
- output
finalize method executed!
abc
yes, i am still alive
no, i am dead
补充堆外内存
除了堆内存,Java 还可以使用堆外内存,也称直接内存(Direct Memory)。
例如:在通信中,将存在于堆内存中的数据 flush 到远程时,需要首先将堆内存中的数据拷贝到堆外内存中,然后再写入 Socket 中;如果直接将数据存到堆外内存中就可以避免上述拷贝操作,提升性能。类似的例子还有读写文件。
很多 NIO 框架 (如 netty,rpc) 会采用 Java 的 DirectByteBuffer 类来操作堆外内存,DirectByteBuffer 类对象本身位于 Java 内存模型的堆中,由 JVM 直接管控、操纵。DirectByteBuffer 中用于分配堆外内存的方法 unsafe.allocateMemory(size) 是个 native 方法,本质上是用 C 的 malloc 来进行分配的。
堆外内存并不直接控制于JVM,因此只能等到full GC的时候才能垃圾回收!(direct buffer归属的的JAVA对象是在堆上且能够被GC回收的,一旦它被回收,JVM将释放direct buffer的堆外空间。前提是没有关闭DisableExplicitGC)。堆外内存包含线程栈,应用程序代码,NIO缓存,JNI调用等.例如ByteBuffer bb = ByteBuffer.allocateDirect(1024)
,这段代码的执行会在堆外占用1k
的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光
DirectByteBuffer 如何回收
JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner
对象,这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address)
,从而回收这块堆外内存。
Cleaner 继承了 PhantomReference(虚引用)
堆外内存溢出
- 最大的堆外内存设置的太小了
- 没有full gc,堆外内存没有及时被清理掉
- 元空间溢出
堆外内存更适合存储的对象
- 存储生命周期长的对象
- 可以在进程间可以共享,减少 JVM 间的对象复制,使得 JVM 的分割部署更容易实现
- 本地缓存,减少磁盘缓存或者分布式缓存的响应时间
gc的发展
首先认识并行和并发的概念
-
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍是被阻塞的等待状态
-
并发(Concurrent): 用户线程能与垃圾收集线程同时执行:即用户程序在一个CPU上继续运行,而垃圾收集程序运行于另一个CPU上
(一) Serial(STW,单线程收集器)
a stop-the-world,coping collector which uses a single GC thread
使用Coping算法
的单线程的收集器,但是它的"单线程"的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是它进行垃圾收集时,必须暂停其它所有的工作线程,直到收集结束即Stop The World
Serial Old
a stop-the-world, mark-sweep-compact collector which uses a single GC thread
(二) Parallel Scavenge(STW,多线程收集器)
a stop-the-world,coping collector which uses multi GC threads
使用Coping算法
的并行多线程收集器。Parallel Scavenge是Java1.8
默认的收集器,特点是并行的多线程回收,以吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))优先
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
Parallel old
a stop-the-world, mark-sweep-compact collector that uses multi GC threads
ParNew (Parallel Scavenge的增强版)
同Parallel Scavenge,但是是工作在年轻代的;且能与CMS
收集器配合工作。
(三) CMS收集器(并发的垃圾回收器)
- concurret mark sweep
- a mostly concurrent, low-pause collector
主要的几个阶段
- initial mark(stw)
- concurrent mark
- remark(stw)
- concurrent sweep
CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象
分代算法中,一般Serial或ParNew用于年轻代、CMS作为老年代垃圾回收器
(四) G1
G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。相对于CMS的优势而言是内存碎片的产生率大大降低;目标是用在多核,大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量
Java HotSpot(TM) 64-Bit Server VM (25.171-b11) for bsd-amd64 JRE (1.8.0_171-b11), built on Mar 28 2018 12:50:57 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 16777216k(1978920k free)
/proc/meminfo:
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
0.196: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0022978 secs]
[Parallel Time: 2.0 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 196.2, Avg: 196.2, Max: 196.3, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.6, Avg: 0.6, Max: 0.6, Diff: 0.0, Sum: 2.4]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.8, Avg: 1.1, Max: 1.3, Diff: 0.5, Sum: 4.3]
[Termination (ms): Min: 0.0, Avg: 0.3, Max: 0.5, Diff: 0.5, Sum: 1.1]
[Termination Attempts: Min: 1, Avg: 1.2, Max: 2, Diff: 1, Sum: 5]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 1.9, Avg: 2.0, Max: 2.0, Diff: 0.0, Sum: 7.8]
[GC Worker End (ms): Min: 198.2, Avg: 198.2, Max: 198.2, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 3072.0K(10.0M)->0.0B(9216.0K) Survivors: 0.0B->1024.0K Heap: 8629.0K(20.0M)->6823.5K(20.0M)]
[Times: user=0.00 sys=0.00, real=0.01 secs]
0.199: [GC concurrent-root-region-scan-start]
0.199: [GC concurrent-root-region-scan-end, 0.0005314 secs]
0.199: [GC concurrent-mark-start]
0.199: [GC concurrent-mark-end, 0.0000251 secs]
0.202: [GC remark 0.202: [Finalize Marking, 0.0000621 secs] 0.202: [GC ref-proc, 0.0000215 secs] 0.202: [Unloading, 0.0003779 secs], 0.0005651 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
0.203: [GC cleanup 13M->13M(20M), 0.0001664 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
garbage-first heap total 20480K, used 12967K [0x00000007bec00000, 0x00000007bed000a0, 0x00000007c0000000)
region size 1024K, 2 young (2048K), 1 survivors (1024K)
Metaspace used 3342K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 371K, capacity 388K, committed 512K, reserved 1048576K
(五) ZGC
新一代垃圾回收器ZGC的探索与实践
ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:
- 停顿时间不超过10ms;
- 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
- 支持8MB~4TB级别的堆(未来支持16TB)。
gc组合
- | 年轻代(别名) | 老年代 | JVM 参数 |
---|---|---|---|
组合一 | Serial (DefNew) | Serial Old(PSOldGen) | -XX:+UseSerialGC |
组合二 | Parallel Scavenge (PSYoungGen) | Serial Old(PSOldGen) | -XX:+UseParallelGC |
组合三(*) | Parallel Scavenge (PSYoungGen) | Parallel Old (ParOldGen) | -XX:+UseParallelOldGC |
组合四 | ParNew (ParNew) | Serial Old(PSOldGen) | -XX:-UseParNewGC |
组合五(*) | ParNew (ParNew) | CMS+Serial Old(PSOldGen) | -XX:+UseConcMarkSweepGC |
组合六(*) | G1 | G1 | -XX:+UseG1GC |
查看自己的java程序是什么gc
- 查看java版本和默认gc:
java -XX:+PrintCommandLineFlags -version
- 查看进程的gc配置
jcmd 64567 VM.flags
分代gc中 new object()
的gc生命周期
回顾内存区域分布(java1.8)如下:
如下图记忆:
线程TLAB局部缓存区域(Thread Local Allocation Buffer)
Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁(基于 CAS 的独享线程(Mutator Threads)),因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配(堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的)
逃逸分析对象分配到栈
- 方法逃逸:就是当一个对象在方法中定义后,它可能被外部方法访问到,比如说通过参数传递到其它方法中
- 线程逃逸:就是当一个对象在方法中定义后,它可能赋值给其它线程中访问的变量
如果不满足逃逸分析就会在栈上分配。栈上分配的好处就是方法出栈后内存就释放了,不用gc流程。
大对象直接进入老年代
这里所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息。(更坏的是:一群"朝生夕灭"的"短命大对象"),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来"安置"它们
-XX:PretenureSizeThreshold
参数,另大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制(复习一下:新生代采用复制算法收集内存)
长期存活的(Age>某个值
)对象将进入老年代
内存回收时,必须识别哪些对象应该在新生代,哪些对象应放到老年代。
虚拟机给每个对象定义了一个对象年龄(Age
)计数器。如果对象在Eden
出生并经历过第一次Minor GC
后仍然存活,并且能被Survivor
容纳的话,将被移动到Survivor
空间,并且对象的年龄为1.对象在Survivor
区中每"熬过"一次Minor GC
,年龄就增加1岁,当他的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
设置
动态对象的年龄判定
为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold
才能晋升老年代,如果在Survivor
空间中相同年龄所有对象大小的总和大于Survivor
空间的一半,年龄大于或等于该年龄对象就可以直接进入老年代,无须等到MaxTenuringThreshold
要求的年龄。
gc术语认识
Minor GC/ Young GC
Minor GC,也被称为Young GC,主要关注于新生代(Young Generation)的内存管理。
分代内存中新生代通常分为Eden区、From Space(也称为Survivor From或S0)和To Space(也称为Survivor To或S1)三个区域。
-
Eden区满: Eden区是年轻代中的一个区域,用于存放新创建的对象。当Eden区满时,触发Minor GC。在Minor GC中,Eden区中的存活对象将被移动到Survivor区,而不再需要的对象将被清理。
-
Survivor区空间不足: 在两个Survivor区(通常称为S0和S1)之间进行对象的复制。当一个Survivor区满时,或者在对象晋升到老年代之前,可能触发Minor GC。在Minor GC中,存活的对象将被移动到另一个Survivor区,或者直接晋升到老年代。
Major GC(老年代GC)
Major GC主要关注清理老年代的内存区域
Full GC
Full GC是针对整个新生代、老生代、元空间的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
可以通过System.gc()
执行full gc,但它并不保证JVM会立即执行Full GC。JVM实现可能会忽略这个请求。
一般是JVM使用情况决定是否执行Full GC, 例如如下的一些场景:
- 老年代空间不足:老年代无法容纳更多从新生代晋升的对象, 当老年代没有足够的空间存放从新生代晋升过来的对象时,JVM会触发Full GC以尝试回收老年代中的无用对象。
- 大对象分配:大对象无法直接放入新生代,且老年代空间不足。如果某个对象在创建时就被判断为“大对象”(即大小超过了新生代对象晋升到老年代的阈值),且老年代空间不足以容纳该对象,也可能触发Full GC。
- 晋升失败:Minor GC后,Survivor区对象晋升到老年代时空间不足。 在进行Minor GC后,如果Survivor区中的对象由于太大而无法直接复制到另一个Survivor区,且这些对象需要晋升到老年代,但老年代的剩余空间不足以容纳这些对象时,也会触发Full GC。
- 使用CMS(Concurrent Mark-Sweep)垃圾回收器时的并发失败: CMS是一种以减少应用程序停顿时间为目标的垃圾回收器,但它可能会因为一些原因(比如老年代空间不足)而导致并发失败,从而触发Full GC。
- …
jstat -gcutil
模拟不断分配对象导致OutOfMemory
jstat -gcutil 67011 20 5
查看进程
使用cms的full gc日志
CMS
参考:"https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
The Concurrent Mark Sweep (CMS) collector is designed for applications that prefer shorter garbage collection pauses and that can afford to share processor resources with the garbage collector while the application is running.
一种以获取最短回收停顿时间为目标的收集器(以牺牲了吞吐量为代价):希望系统停顿时间最短,给用户带来较好但体验。基础算法是Mark-Sweep
算法;不会整理、压缩堆空间,会产生内存碎片
要求多CPU;为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切换效率太低。并且,重新标记阶段,为保证Stop The World
快速完成,也要用到更多的甚至所有的CPU资源。
CMS流程
cms gc日志也能看到这些阶段:
初始标记(CMS initial mark),需要短暂的Stop The World
这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
并发标记(CMS concurrent mark)(一定会有错误标记)
这个阶段紧随初始标记
阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。(并发标记所有老年代)
重新标记(CMS remark)正确标记,需要Stop The World
这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"根对象"开始向下追溯,并处理对象关联。
并发清除(CMS concurrent sweep)
清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行;会产生浮动垃圾(使用标记-清除算法)
重置计数器(Reset Counting)
重置CMS收集器的数据,为下次垃圾收集做准备
CMS垃圾回收器的缺点
-
CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程,使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,所做的事情就是在
并发标记
和并发清除
的时候让GC线程和用户线程交替运行,尽量减少GC线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,已经不推荐) -
CMS处理器无法处理浮动垃圾(
Floating Garbage
)。由于CMS在并发清除阶段有用户线程还在运行着,伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法在当次收集过程中处理掉它们,所以只有等到下次gc时候再清理掉,这一部分垃圾就称作"浮动垃圾";因此CMS收集器不能像其它收集器那样等到老年代几乎完全被填满了再进行收集,而是需要预留一部分空间提高并发收集时的程序运作使用。 -
CMS是基于(
mark-sweep
)"标记-清除"算法实现的,所以在收集结束的时候会有大量的空间碎片
产生。空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发full gc
。
为了解决这个问题,CMS提供了一个开关参数(-XX: UseCMSCompactAtFullCollection
),用于在CMS要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了。另外一个参数(-XX: CMSFullGCsBeforeCompaction
)用于设置执行多少次不压缩的full gc后,跟着来一次带压缩的(默认值为0,表示每次进入full gc时都进行碎片整理)
CMS GC问题分析与解决
推荐:美团技术文章:https://blog.csdn.net/MeituanTech/article/details/109664525
G1
参考文档:https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
g1相关概念
When performing garbage collections, G1 operates in a manner similar to the CMS collector. G1 performs a concurrent global marking phase to determine the liveness of objects throughout the heap. After the mark phase completes, G1 knows which regions are mostly empty. It collects in these regions first, which usually yields a large amount of free space. This is why this method of garbage collection is called Garbage-First.As the name suggests, G1 concentrates its collection and compaction activity on the areas of the heap that are likely to be full of reclaimable objects, that is, garbage. G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
(概括起来:仍然类似CMS采用并发标记,不过垃圾回收时不是回收全部,而是对那些垃圾较多的region区域进行回收;根据此次回收可以对下次回收进行一个预测)
- region size:
1M-32M
,通过-XX:G1HeapRegionSize
可指定 - G1的每个
region
都有一个Remember Set(RSet)
,用来保存别的region
的对象对该region的对象的引用,通过Remember Set
可以找到哪些对象引用了当前region
里面的对象; - Collection Set则表示本次垃圾清理的region集合;避免一次对整个堆的垃圾进行回收,而是一次回收称为
collection set
的部分,collection set
就是垃圾比较多的那些region
- SATB(Snapshot-At-The-Beginning)是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。结合
三色标记
g1流程
young gc/minor gc(对年轻代的GC)
Live objects are evacuated to one or more survivor regions. If the aging threshold is met, some of the objects are promoted to old generation regions.
(young gc的处理效果就是:存活在Eden
的对象会进入survivor
区域,如果达到aging阈值则进入到old
区域)
This is a stop the world (STW) pause. Eden size and survivor size is calculated for the next young GC. Accounting information is kept to help calculate the size. Things like the pause time goal are taken into consideration.
This approach makes it very easy to resize regions, making them bigger or smaller as needed.
(这是一个stop the world的停顿,eden
和survivor
区域会重新计算分配,停顿时间是要考虑到的)
mixed gc(young 和 old都gc)
mix gc的过程
Phase | Description |
---|---|
(1) Initial Mark (Stop the World) | This is a stop the world event. With G1, it is piggybacked on a normal young GC. Mark survivor regions (root regions) which may have references to objects in old generation. |
(2) Root Region Scanning | Scan survivor regions for references into the old generation. This happens while the application continues to run. The phase must be completed before a young GC can occur. |
(3) Concurrent Marking | Find live objects over the entire heap. This happens while the application is running. This phase can be interrupted by young generation garbage collections. |
(4) Remark(Stop the World) | Completes the marking of live object in the heap. Uses an algorithm called snapshot-at-the-beginning (SATB) which is much faster than what was used in the CMS collector. |
(5) Cleanup(Stop the World Event and Concurrent) | * Performs accounting on live objects and completely free regions. (Stop the World); * Scrubs the Remembered Sets. (Stop the world); * Reset the empty regions and return them to the free list. (Concurrent) |
(*) Copying (Stop the World) | These are the stop the world pauses to evacuate or copy live objects to new unused regions. This can be done with young generation regions which are logged as [GC pause (young)]. Or both young and old generation regions which are logged as [GC Pause (mixed)]. |
- Concurrent Marking(并发标记)对比CMS,标记扫描的region不是所有的,因为上一步Root Region扫描排除了哪些有根对象应用的region
- Remark(重新标记)对比CMS,使用更快的SATB
- Cleanup(清理),Stop-The-World,采用复制清理,只清理垃圾多的region
G1停顿耗时的主要瓶颈
G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法)
G1的混合回收过程可以分为标记阶段、清理阶段和复制阶段
-
标记阶段停顿分析
- 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
- 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
- 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
-
清理阶段停顿分析
- 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。
-
复制阶段停顿分析
- 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。
四个STW过程中
- 初始标记因为只标记GC Roots,耗时较短。
- 再标记因为对象数少,耗时也较短。
- 清理阶段清点出有存活对象的分区和没有存活对象的分区,因为内存分区数量少,耗时也较短。
- 复制阶段的转移过程要处理所有存活的对象,耗时会较长。
因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。