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 感知
使用彩色指针
使用负载屏障