JVM与GC调优(四)-对象的生命周期和垃圾回收GC篇
本文最后更新于:2024年4月22日 下午
Java对象的生命周期,对象从创建过程,到内存中分配方式,如何分配以及何时进入老年代相关。JVM垃圾回收
对象的生命周期
对象创建的流程
创建流程
对象内存的分配方式
内存分配的方法有两种:不同垃圾收集器不一样
指针碰撞(Bump the Pointer)
内存地址是连续的(新生代),Serial 和ParNew 收集器
空闲列表(Free List)
内存地址不连续(老年代),CMS 收集器和 Mark-Sweep 收集器
对象内存分配的安全问题
在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况
在JVM中有两种解决办法
- CAS 是乐观锁的一种实现方式。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
 - TLAB本地线程分配缓冲(Thread Local Allocation Buffer即TLAB):为每一个线程预先分配一块内存
 
对象怎样才会进入老年代?
对象的分配对象情况如下
新生代:新对象大多数都默认进入新生代的Eden区。伊甸园(希腊神话)
老年代(四种情况):
存活年龄太大,默认超过15次【
-XX:MaxTenuringThreshold】动态年龄判断,MinorGC之后,发现Survivor区中的一批对象的总大小大于了这块Survivor区
的50%,那么就会将此时大于等于这批对象年龄最大值的所有对象,直接进入老年代
大对象直接进入老年代,前提是Serial和ParNew收集器
MinorGC后,存活对象太多无法放入Survivor
空间担保机制
当新生代无法分配内存的时候,我们想把新生代的老对象转移到老年代,然后把新对象放入腾空的新生代。此种机制我们称之为内存担保。
对象的内存布局
堆内存中,一个对象在内存中存储的布局可以分为三块区域
对象头(Header)Java对象头占8byte。如果是数组则占12byte。因为JVM里数组size需要使用
4byte存储
- 标记字段MarkWord
- 用于存储对象自身的运行时数据,它是synchronized实现轻量级锁和偏向锁的关键
 - 默认存储:对象HashCode、GC分代年龄、锁状态等等信息
 - 锁标志位的变化,存储数据发生变化
 
 - 类型指针KlassPoint
- 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
 - 开启指针压缩存储空间4byte,不开启8byte
 - JDK1.6+默认开启
 
 - 数组长度
- 如果对象是数组,则记录数组长度,占4个byte,如果对象不是数组则不存在
 
 - 对齐填充
- 保证数组的大小永远是8byte的整数倍
 
 
- 标记字段MarkWord
 实例数据(Instance Data)
- 生成对象的时候,对象的非静态成员变量也会存入堆空间
 
对齐填充(Padding)
- JVM内对象都采用8byte对齐,不够8byte的会自动补齐
 
如何访问一个对象
- 句柄:稳定,对象被移动只要修改句柄中的地址
 - 直接指针:访问速度快,节省了一次指针定位的开销
 
JVM垃圾收集器
概述
什么是垃圾?
在内存中,没有被引用的对象就是垃圾
如何找到这个垃圾?
主要是2种:引用计数法和根可达算法
引用计数法(
Reference Counting)- 引用计数算法不能解决循环引用问题
 
根可达算法(
GCRoots Tracing)通过一系列的名为
GCRoot的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(
Reference Chain),当一个对象到GCRoot没有任何引用链相连时,则证明此对象是不可用的,也就是不可达的
可作GCRoots的对象
- 虚拟机栈中,栈帧的本地变量表引用的对象
 - 方法区中,类静态属性引用的对象
 - 方法区中,常量引用的对象
 - 本地方法栈中,JNl引用的对象、
 
回收过程
垃圾对象在死亡前至少经历两次标记
第一次标记:如果对象可达性分析后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记
第二次标记:第一次标记后,接着会进行一次筛选。筛选条件:此对象是否有必要执行finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记
对象引用
引用分为强引用(
StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用强度依次逐渐减弱
如何清除垃圾?
Mark-Sweep 标记清除算法
Copying 拷贝算法
Mark-Compact 标记压缩算法
标记清除算法(Mark-Sweep)
最基本的算法,主要分为标记和清除2个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收
掉所有被标记的对象
缺点:
效率不高,标记和清除过程的效率都不高
空间碎片,会产生大量不连续的内存碎片,会导致大对象可能无法分配,提前触发GC
拷贝算法(Copying)
现在商业虚拟机都是采用这种收集算法来回收新生代
它将可用内存按容量划分为相等的两块,每次只使用其中的一块。当这一块的内存用完
了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
缺点:
- 存在空间浪费
 
标记整理算法(Mark-Compact)
标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。没有空间浪费,没有内存碎片化问题
缺点:
- 性能较低,因为除了拷贝对象以外,还需要对象内存空间进行压缩,所以性能较低。
 
分代回收(Generational Collection)
- 新生代,每次垃圾回收都有大量对象失去,选择复制算法
 - 老年代,对象存活率高,无人进行分配担保,就必须采用标记清除或者标记整理算法
 
用什么清除垃圾
两大类,串行收集器和并行收集器。有 8 种不同的垃圾回收器
新生代回收器:
Serial、ParNew、Parallel Scavenge老年代回收器:
Serial Old、Parallel Old、CMS整堆回收器:
G1、ZGC
串行收集器
Serial收集器
配置参数:-XX:+UseSerialGC
特点:
Serial新生代收集器,单线程执行,使用复制算法Serial Old老年代收集器,单线程执行,使用复制算法- 进行垃圾收集时,必须暂停用户线程(挂起,Safepoint)
 
Safepoint挂起线程的时机:
- 循环的末尾
 - 方法返回前
 - 调用方法的call之后
 - 抛出异常的位置
 
Parallel Scavenge并行收集器
配置参数:-XX:+UseParallelGC
特点:
- 吞吐量优先收集器,垃圾收集需要暂停用户线程
 - 新生代使用并行回收收集器,采用复制算法
 - 老年代使用串行收集器,采用标记-整理算法
 
Parallel Old收集器
配置参数: -XX:+UseParallelOldGC
特点:
- PS(Parallel Scavenge)收集器的老年代版本
 - 吞吐量优先收集器,垃圾收集需要暂停用户线程
 - 老年代使用并行收集器,采用标记-整理算法
 
ParNew收集器
配置参数:-XX:+UseParNewGC或者 -XX:ParallelGCThreads=n 设置并行收集器收集时使用的并行收集线程数。一般最好和计算机的CPU相当
特点:
- 新生代并行(
ParNew),老年代串行(Serial Old) - Serial收集器的多线程版本
 - 单核CPU不建议使用
 
CMS收集器
配置参数: -XX:+UseConcMarkSweepGC
特点:
- 低延迟:减少STW对用户体验的影响
 - 并发收集,可以同时执行用户线程
 - 不会等到堆填满再收集,到达阈值就开始收集
 - 采用标记-清除算法,所以会产生内存碎片
 
G1(Garbage-First)收集器(JDK1.8之后)
G1是一款面向服务端应用的垃圾收集器,大内存企业配置的垃圾收集器大多都是G1
配置参数: -XX:+UseG1GC
特点:
- 吞吐量和低延时都行的整堆垃圾收集器
 - G1最大堆内存支持64GB,最小堆内存2GB
 - 全局采用标记-整理算法收集,局部采用复制算法收集
 - 可预测的停顿
 
ZGC(Z Garbage Collector)
在 JDK11 中引入的一种可扩展的低延迟垃圾收集器,在 JDK15 中发布稳定版
配置参数: -XX:+UseZGC
特点:
并发
基于 region
压缩
NUMA 感知
使用彩色指针
使用负载屏障
