【Java】JVM 垃圾回收基础
JVM 垃圾回收基础
五竹发布于 1 月 23 日
什么是垃圾
内存中不在被使用到的内存空间,就是垃圾。
Java中的内存是动态分配和自动回收的。学习垃圾收集机制,调优策略,能够帮助我们处理和应对各种工作中面临的内存泄漏问题。
Java 虚拟机运行时数据区分为程序计数器、虚拟机栈、本地方法栈、堆、方法区。
其中程序计数器、虚拟机栈、本地方法栈这 3 个区域是线程私有的,会随线程消亡而自动回收,所以不需要管理。
因此垃圾收集只需要关注堆和方法区。
Java的内存分配
堆上分配
- 如果启动了本地线程分配缓冲,将按线程优先分配在TLAB上。
- 对象优先在Eden上分配
- 大对象进入老年代
- 长期存活的对象进入老年代
栈上分配
基于逃逸分析技术,如果一个对象始终在一个方法内。确定这个对象不会逃逸方法之外,那让这个对象在栈上分配内存,这样对象就会随着方法的结束而自动销毁,降低了垃圾回收的压力。
如何判定垃圾
引用计数法
给对象添加一个引用计数器,有对这个对象的引用就对计数加1,引用失效就减1,任何时刻计数器为0的对象就是判定为垃圾。
- 优点:实现简单,效率高。
- 缺点:不能解决对象循环引用的问题。在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低。
可达性分析法
目前主流的虚拟机才有的算法。他是从根节点(GC Root)向下去搜索对象节点,搜索走过的路径称为引用链,当一个对象到根节点之间没有联通的话,这个对象判定为垃圾。
GC Root对象
可以作为GC Roots的对象包括:
- 虚拟机栈中引用的对象
- 方法区类静态属性信用的对象
- 方法去常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
引用类型
判断一个对象的存活跟引用有关,jdk1.2之前,引用只是指引用类型的数据中存储了另一块内存的地址。jdk1.2之后,java的引用分为以下四种。
- 强引用:Object obj = new Object();最常见的引用,这样通过new创建的会产生该对象的强引用。只要对象有强引用指向,并且GC Roots可达,那么就不会回收该对象。
- 软引用: SotfReference类来实现。表示一些还有用但是非必须的对象。在OOM前,垃圾收集器会把这些软引用指向的对象加入回收范围,对于软引用关联的对象,只有在内存不足的时候才会回收。
- 弱引用:WeakReference,表示非必要对象,在YGC时候会被回收。由于YGC时间不确定,所以弱引用随时都有可能被回收。
- 虚引用:PhantomReference,无法通过该引用获取指向的对象。在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须和引用队列配合使用:
如
ReferenceQueue<String> queue = new ReferenceQueue<>();PhantomReference<String> phantomReference = new PhantomReference<>("Hello", queue);
当垃圾回收器准备回收一个对象时,发现他有虚引用,就会在回收这对象的内存前,把这个虚引用加入队列,程序通过引用队列中是否有该虚引用,来了解对象是否要被垃圾回收器回收,然后可以在回收前做一些事情。
垃圾收集算法
标记-清除 (Mark-Sweep)
最基础的算法,第一步,先标记,从GC Roots出发,依次标记对象引用关系,然后,将没有被标记的对象清除。
缺点:一个是效率问题,标记和清除两个过程效率都不高;另外,清除过后会产生大量不连续的内存碎片。导致当我们需要分配大对象时候,无法找到足够的连续内存而又触发一次GC。
复制(Copy)
复制算法:把内存分为两块大小相同的区域,每次使用其中一块,当使用完了,就把这一块区域上还存活对象拷贝到另一块,然后把这一块清除掉。不会造成内存碎片,但是内存利用率不高,会造成一半的空间浪费。
新生代上的对象大多数“朝生夕死”,HotSpot默认将新生代内存划分为一个大的Eden区和两个小的Surivor区。在GC时,将Eden区和一个Surivor区中存活的对象,复制到另外一个Surivor区。Eden区和两个Surivor区比例默认8:1:1,所以这时候只会浪费10%的空间。
标记-整理(Mark-Compact)
和标记清除一样,先标记,但是后续不直接回收对象清理,而是让存活的对象向一端移动,然后直接清理掉端边界之外的内存。
由于复制算法在存活对象比较多的时候,效率比较低,且有空间浪费,所以老年代一般会选用标记-整理算法。
### 三种GC算法对比
回收算法 | 优点 | 缺点 |
---|---|---|
标记-清除 | 实现简单 | 存在内存碎片 |
复制 | 无碎片,性能好 | 内存使用率低 |
标记-整理 | 无碎片 | 整理过程开销大 |
分代收集
将上面的各种算法组合在一起,因地制宜。一般我们把堆空间分为新生代和老年代。根据他们的特定采用不同的垃圾回收算法,在新生代,每次GC都会有大量对象死去,少数存活,所以采用复制算法。只用付出少数对象的复制成本,不会造成不连续的内存碎片。而在老年代,对象存活率高,则采用标记整理或者标记清除。
垃圾收集器
术语
- STW: "Stop The World"简写,也叫全局停顿。会造成服务暂停,没有响应。
- 串行收集: GC单线程内存回收,会暂停所有的用户线程。比如Serial, Serial Old
- 并行收集: 多线程进行并发GC,此时暂停用户线程。比如Parallel
- 并发收集:用户线程和GC线程同时执行,不需要停顿用户线程.适合对响应时间有要求的场景。比如CMS收集器
Serial/Serial Old
Serial是一个单线程的收集器,它只会使用一个CPU或一个收集线程去做垃圾收集,并且在做垃圾收集时需要停止所有的工作线程,知道收集工作结束。Serial停止用户线程,采用复制算法收集年轻代,Serial Old采用标记-整理算法收集老年代。
特点:单线程收集,STW
使用 -XX:+UseSerialGC 开启Serial + Serial Old
ParNew
Serial的多线程版本,使用多线程进行垃圾收集。他是新生代的垃圾收集器。需要配合老年代的CMS收集器使用。所以开启需要使用CMS,新生代默认ParNew.
可以通过 ==-XX:ParallelGCThreads== 参数来控制收集的线程数,过程也是STW
Parallel Scavenge/Paraller Old
Parallel Scavenge是新生代的收集器,采用复制算法,多线程的收集器,Paraller Old是老年代收集器,也是多线程收集,采用标记-整理算法。
主要参数
- -XX:UseParallelGC 开启
- -XX:MaxGCPauseMillis 最大垃圾收集停顿时间
- -XX:GCTimeRatio 设置吞吐量大小
可控的吞吐量
通过参数 -XX:MaxGCPauseMillis 控制最大GC停顿时间。-XX:GCTimeRatio 设置吞吐量大小
提升吞吐量可以高效利用CPU时间,尽快完成程序任务。
自适应GC策略
处理提供了上面两个参数控制吞吐量大小以外,Parallel Scavenge还可以通过 -XX:+UseAdptiveSizePolicy, 开启指示仪GC策略。打开后,就不再需要手动设置新生代大小,Eden/Surivor比例等参数,虚拟机会根据系统运行状况,动态调整这些参数,从而达到最优的停顿时间,和最高吞吐量。
CMS
- 1.初始标记:只标记GC Roots能直接关联到的对象,会STW。
- 2.并发标记:进行GC Roots Tracing的过程,GC线程和用户线程同时执行。
- 3.重新标记 :修正并发标记期间,因程序运行导致标记发生变化的那些对象。会STW
- 4.并发清除:并发回收垃圾对象(GC线程和用户线程同时执行)
- 5.并发重置:清理本次CMS GC上下文信息,为下一次GC做准备。
优点:低停顿,并发执行
缺点:
- 由于并发执行,所以对CPU资源压力大。
- 无法处理在收集过程中产生的浮动垃圾。
- 由于采用标记-清除算法,所以会才是大量内存碎片。而导致在需要分配大对象是内存不足,触发FullGC。
使用 -XX:UseConcMarkSweepGC 开启 ParNew+CMS/SerialOld 收集器组合,即新生代采用ParNew,老年代CMS,当CMS出错后,SerialOld备用。
为了解决内存碎片问题,CMS可以通过-XX:+UseCMSCompactAtFullCollection,强制JVM在FullGC完成后对老年代进行压缩,执行碎片整理,同时会STW。想要减少STW次数,可以配置-XX:+CMSFullGCsBeforeCompaction参数,在执行设置的次数后,JVM再在老年代进行空间整理。
JDK9已经将CMS标记为弃用,在JDK14中已经将CMS删除。
G1
JDK7推出的新一代收集器,
是一个面向服务端应用的收集器。相比上面的收集器,
G1作用在整个堆,而其他的收集器都是只作用在新生代或老年代。
G1将Java Heap分割成一些大小相同的Region,通过参数-XX:G1HeapRegionSize指定Region的大小,取值范围为1~32M,应为2的N次幂。G1对每个Region做了分类, 分别包括:Eden,Surivor,Old, Humongous, 其中Humongous相当于一个大的Old,用来存放大对象。
如下图:G1的堆内存布局和传统的堆内存布局不同。
G1将空间分成多个区域,跟踪每个区域里面的垃圾堆积的价值大小,构建一个优先列表,优先收集垃圾最多的区域,这也是它为什么叫Garbage-First的原因。
G1与CMS相比的,有何特点
- 并发与并行:充分的利用多CPU,缩短STW时间。并发标记阶段可与用户线程并发执行,最终标记阶段GC线程可并行执行。
- 分代收集:G1可以不与其他垃圾收集器配合,独立完成整个GC堆的垃圾收集。
- 空间整合:G1整体看是采用“标记-整理”算法实现的,局部也有Eden和Surivor Region看上去是采用“复制”算法来实现。整个过程避免了产生内存碎片。
- 停顿时间可控制:G1除了追求低停顿,还能建立可预测的停顿时间模型,能让用户指定在一个时间段内,消耗在收集上的时间不超过一个时间段。
G1垃圾收集模式
Young GC
- 1.所有Eden Region都满了时,就会触发Young GC
- 2.Eden Region里的对象会转移到Surivor Region
- 3.原Surivor Region中的对象转移到另一个Surivor,或者晋升到Old Region
- 4.空闲Region会被放入空闲列表,等待下次使用。
Mixed GC
当老年代占整个Heap的大小百分比达到一个阀值(-XX:InitialingHeapOccupancyPercent)时,默认45%,就会触发Mixed GC,
收集整个新生代以及部分老年代。
Mixed GC 回收过程
- 1.初始标记:只标记GC Roots能关联到的对象。修改TAMS的值,此阶段会STW.
- 2.并发标记: 从GC Root开始对堆内存中的对象进行可达性分析,找出存活的对象,此过程可以和用户线程并发执行。
- 3.最终标记:修正在并发标记阶段用户线程继续运行导致标记产生变动的记录。此过程STW,但是可以并行执行。
- 4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划。
Full GC
当复制对象内存不够时,或无法分配足够空间时,触发Full GC
Full GC模式是采用Serial Old收集的,所以会STW。
如何减少Full GC?
- 增大-XX:G1ReserverPercent,来增加预留内存。
- 减少 -XX:InitialingHeapOccupancyPercent,当老年代达到这个值是就触发Mixed GC,
- 增加 -XX:ConcGCThreads 并发阶段的线程数。
总结
对以上介绍的垃圾回收器总结
- Serial 串行,作用于新生代,采用复制算法
- ParNew 并行,作用于新生代,采用复制算法
- Serial Old 串行,作用于老年代,标记-整理算法
- Parallel 并行, 作用于新生代, 复制算法
- Parallel Old 并行, 作用于老年代, 标记-整理算法
- CMS 并发, 作用于老年代, 标记-清除算法
- G1 并发+并行, 作用于整个堆,复制算法,标记-整理
参考资料
《深入理解Java虚拟机 JVM高级特性与最佳实践》
javajvmgc
阅读 12发布于 1 月 23 日
本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议
五竹
1 声望
0 粉丝
五竹
1 声望
0 粉丝
宣传栏
什么是垃圾
内存中不在被使用到的内存空间,就是垃圾。
Java中的内存是动态分配和自动回收的。学习垃圾收集机制,调优策略,能够帮助我们处理和应对各种工作中面临的内存泄漏问题。
Java 虚拟机运行时数据区分为程序计数器、虚拟机栈、本地方法栈、堆、方法区。
其中程序计数器、虚拟机栈、本地方法栈这 3 个区域是线程私有的,会随线程消亡而自动回收,所以不需要管理。
因此垃圾收集只需要关注堆和方法区。
Java的内存分配
堆上分配
- 如果启动了本地线程分配缓冲,将按线程优先分配在TLAB上。
- 对象优先在Eden上分配
- 大对象进入老年代
- 长期存活的对象进入老年代
栈上分配
基于逃逸分析技术,如果一个对象始终在一个方法内。确定这个对象不会逃逸方法之外,那让这个对象在栈上分配内存,这样对象就会随着方法的结束而自动销毁,降低了垃圾回收的压力。
如何判定垃圾
引用计数法
给对象添加一个引用计数器,有对这个对象的引用就对计数加1,引用失效就减1,任何时刻计数器为0的对象就是判定为垃圾。
- 优点:实现简单,效率高。
- 缺点:不能解决对象循环引用的问题。在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低。
可达性分析法
目前主流的虚拟机才有的算法。他是从根节点(GC Root)向下去搜索对象节点,搜索走过的路径称为引用链,当一个对象到根节点之间没有联通的话,这个对象判定为垃圾。
GC Root对象
可以作为GC Roots的对象包括:
- 虚拟机栈中引用的对象
- 方法区类静态属性信用的对象
- 方法去常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
引用类型
判断一个对象的存活跟引用有关,jdk1.2之前,引用只是指引用类型的数据中存储了另一块内存的地址。jdk1.2之后,java的引用分为以下四种。
- 强引用:Object obj = new Object();最常见的引用,这样通过new创建的会产生该对象的强引用。只要对象有强引用指向,并且GC Roots可达,那么就不会回收该对象。
- 软引用: SotfReference类来实现。表示一些还有用但是非必须的对象。在OOM前,垃圾收集器会把这些软引用指向的对象加入回收范围,对于软引用关联的对象,只有在内存不足的时候才会回收。
- 弱引用:WeakReference,表示非必要对象,在YGC时候会被回收。由于YGC时间不确定,所以弱引用随时都有可能被回收。
- 虚引用:PhantomReference,无法通过该引用获取指向的对象。在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须和引用队列配合使用:
如
ReferenceQueue<String> queue = new ReferenceQueue<>();PhantomReference<String> phantomReference = new PhantomReference<>("Hello", queue);
当垃圾回收器准备回收一个对象时,发现他有虚引用,就会在回收这对象的内存前,把这个虚引用加入队列,程序通过引用队列中是否有该虚引用,来了解对象是否要被垃圾回收器回收,然后可以在回收前做一些事情。
垃圾收集算法
标记-清除 (Mark-Sweep)
最基础的算法,第一步,先标记,从GC Roots出发,依次标记对象引用关系,然后,将没有被标记的对象清除。
缺点:一个是效率问题,标记和清除两个过程效率都不高;另外,清除过后会产生大量不连续的内存碎片。导致当我们需要分配大对象时候,无法找到足够的连续内存而又触发一次GC。
复制(Copy)
复制算法:把内存分为两块大小相同的区域,每次使用其中一块,当使用完了,就把这一块区域上还存活对象拷贝到另一块,然后把这一块清除掉。不会造成内存碎片,但是内存利用率不高,会造成一半的空间浪费。
新生代上的对象大多数“朝生夕死”,HotSpot默认将新生代内存划分为一个大的Eden区和两个小的Surivor区。在GC时,将Eden区和一个Surivor区中存活的对象,复制到另外一个Surivor区。Eden区和两个Surivor区比例默认8:1:1,所以这时候只会浪费10%的空间。
标记-整理(Mark-Compact)
和标记清除一样,先标记,但是后续不直接回收对象清理,而是让存活的对象向一端移动,然后直接清理掉端边界之外的内存。
由于复制算法在存活对象比较多的时候,效率比较低,且有空间浪费,所以老年代一般会选用标记-整理算法。
### 三种GC算法对比
回收算法 | 优点 | 缺点 |
---|---|---|
标记-清除 | 实现简单 | 存在内存碎片 |
复制 | 无碎片,性能好 | 内存使用率低 |
标记-整理 | 无碎片 | 整理过程开销大 |
分代收集
将上面的各种算法组合在一起,因地制宜。一般我们把堆空间分为新生代和老年代。根据他们的特定采用不同的垃圾回收算法,在新生代,每次GC都会有大量对象死去,少数存活,所以采用复制算法。只用付出少数对象的复制成本,不会造成不连续的内存碎片。而在老年代,对象存活率高,则采用标记整理或者标记清除。
垃圾收集器
术语
- STW: "Stop The World"简写,也叫全局停顿。会造成服务暂停,没有响应。
- 串行收集: GC单线程内存回收,会暂停所有的用户线程。比如Serial, Serial Old
- 并行收集: 多线程进行并发GC,此时暂停用户线程。比如Parallel
- 并发收集:用户线程和GC线程同时执行,不需要停顿用户线程.适合对响应时间有要求的场景。比如CMS收集器
Serial/Serial Old
Serial是一个单线程的收集器,它只会使用一个CPU或一个收集线程去做垃圾收集,并且在做垃圾收集时需要停止所有的工作线程,知道收集工作结束。Serial停止用户线程,采用复制算法收集年轻代,Serial Old采用标记-整理算法收集老年代。
特点:单线程收集,STW
使用 -XX:+UseSerialGC 开启Serial + Serial Old
ParNew
Serial的多线程版本,使用多线程进行垃圾收集。他是新生代的垃圾收集器。需要配合老年代的CMS收集器使用。所以开启需要使用CMS,新生代默认ParNew.
可以通过 ==-XX:ParallelGCThreads== 参数来控制收集的线程数,过程也是STW
Parallel Scavenge/Paraller Old
Parallel Scavenge是新生代的收集器,采用复制算法,多线程的收集器,Paraller Old是老年代收集器,也是多线程收集,采用标记-整理算法。
主要参数
- -XX:UseParallelGC 开启
- -XX:MaxGCPauseMillis 最大垃圾收集停顿时间
- -XX:GCTimeRatio 设置吞吐量大小
可控的吞吐量
通过参数 -XX:MaxGCPauseMillis 控制最大GC停顿时间。-XX:GCTimeRatio 设置吞吐量大小
提升吞吐量可以高效利用CPU时间,尽快完成程序任务。
自适应GC策略
处理提供了上面两个参数控制吞吐量大小以外,Parallel Scavenge还可以通过 -XX:+UseAdptiveSizePolicy, 开启指示仪GC策略。打开后,就不再需要手动设置新生代大小,Eden/Surivor比例等参数,虚拟机会根据系统运行状况,动态调整这些参数,从而达到最优的停顿时间,和最高吞吐量。
CMS
- 1.初始标记:只标记GC Roots能直接关联到的对象,会STW。
- 2.并发标记:进行GC Roots Tracing的过程,GC线程和用户线程同时执行。
- 3.重新标记 :修正并发标记期间,因程序运行导致标记发生变化的那些对象。会STW
- 4.并发清除:并发回收垃圾对象(GC线程和用户线程同时执行)
- 5.并发重置:清理本次CMS GC上下文信息,为下一次GC做准备。
优点:低停顿,并发执行
缺点:
- 由于并发执行,所以对CPU资源压力大。
- 无法处理在收集过程中产生的浮动垃圾。
- 由于采用标记-清除算法,所以会才是大量内存碎片。而导致在需要分配大对象是内存不足,触发FullGC。
使用 -XX:UseConcMarkSweepGC 开启 ParNew+CMS/SerialOld 收集器组合,即新生代采用ParNew,老年代CMS,当CMS出错后,SerialOld备用。
为了解决内存碎片问题,CMS可以通过-XX:+UseCMSCompactAtFullCollection,强制JVM在FullGC完成后对老年代进行压缩,执行碎片整理,同时会STW。想要减少STW次数,可以配置-XX:+CMSFullGCsBeforeCompaction参数,在执行设置的次数后,JVM再在老年代进行空间整理。
JDK9已经将CMS标记为弃用,在JDK14中已经将CMS删除。
G1
JDK7推出的新一代收集器,
是一个面向服务端应用的收集器。相比上面的收集器,
G1作用在整个堆,而其他的收集器都是只作用在新生代或老年代。
G1将Java Heap分割成一些大小相同的Region,通过参数-XX:G1HeapRegionSize指定Region的大小,取值范围为1~32M,应为2的N次幂。G1对每个Region做了分类, 分别包括:Eden,Surivor,Old, Humongous, 其中Humongous相当于一个大的Old,用来存放大对象。
如下图:G1的堆内存布局和传统的堆内存布局不同。
G1将空间分成多个区域,跟踪每个区域里面的垃圾堆积的价值大小,构建一个优先列表,优先收集垃圾最多的区域,这也是它为什么叫Garbage-First的原因。
G1与CMS相比的,有何特点
- 并发与并行:充分的利用多CPU,缩短STW时间。并发标记阶段可与用户线程并发执行,最终标记阶段GC线程可并行执行。
- 分代收集:G1可以不与其他垃圾收集器配合,独立完成整个GC堆的垃圾收集。
- 空间整合:G1整体看是采用“标记-整理”算法实现的,局部也有Eden和Surivor Region看上去是采用“复制”算法来实现。整个过程避免了产生内存碎片。
- 停顿时间可控制:G1除了追求低停顿,还能建立可预测的停顿时间模型,能让用户指定在一个时间段内,消耗在收集上的时间不超过一个时间段。
G1垃圾收集模式
Young GC
- 1.所有Eden Region都满了时,就会触发Young GC
- 2.Eden Region里的对象会转移到Surivor Region
- 3.原Surivor Region中的对象转移到另一个Surivor,或者晋升到Old Region
- 4.空闲Region会被放入空闲列表,等待下次使用。
Mixed GC
当老年代占整个Heap的大小百分比达到一个阀值(-XX:InitialingHeapOccupancyPercent)时,默认45%,就会触发Mixed GC,
收集整个新生代以及部分老年代。
Mixed GC 回收过程
- 1.初始标记:只标记GC Roots能关联到的对象。修改TAMS的值,此阶段会STW.
- 2.并发标记: 从GC Root开始对堆内存中的对象进行可达性分析,找出存活的对象,此过程可以和用户线程并发执行。
- 3.最终标记:修正在并发标记阶段用户线程继续运行导致标记产生变动的记录。此过程STW,但是可以并行执行。
- 4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划。
Full GC
当复制对象内存不够时,或无法分配足够空间时,触发Full GC
Full GC模式是采用Serial Old收集的,所以会STW。
如何减少Full GC?
- 增大-XX:G1ReserverPercent,来增加预留内存。
- 减少 -XX:InitialingHeapOccupancyPercent,当老年代达到这个值是就触发Mixed GC,
- 增加 -XX:ConcGCThreads 并发阶段的线程数。
总结
对以上介绍的垃圾回收器总结
- Serial 串行,作用于新生代,采用复制算法
- ParNew 并行,作用于新生代,采用复制算法
- Serial Old 串行,作用于老年代,标记-整理算法
- Parallel 并行, 作用于新生代, 复制算法
- Parallel Old 并行, 作用于老年代, 标记-整理算法
- CMS 并发, 作用于老年代, 标记-清除算法
- G1 并发+并行, 作用于整个堆,复制算法,标记-整理
参考资料
《深入理解Java虚拟机 JVM高级特性与最佳实践》
以上是 【Java】JVM 垃圾回收基础 的全部内容, 来源链接: utcz.com/a/107263.html
得票时间