本栏目讲叙JVM简介、JVM内存结构、垃圾回收机制和类加载与字节码技术
文章目录
五种引用
1、强引用
概述
:由 new 关键字创建的对象,只有所有 GC Roots 对象都不引用该对象,该对象才能被垃圾回收
// new Object()对象被obj强引用
Object obj = new Object();
// 只有当obj = null或线程运行结束,不引用该对象才会被回收,否则即使内存不足,JVM宁愿抛出OutOfMemoryError也不会回收
obj = null;
2、软引用
概述
:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象。可以配合引用队列来释放软引用自身
public class SoftReferenceDemo {
private static final int _4MB = 4 * 1024 * 1024;
// 将堆内存设置为20MB -Xmx20MB -XX:+PrintGCDetails -verbose:gc
// list -> softReference -> byte[]
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<SoftReference<byte[]>>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
for (int i = 0; i < 5; i++) {
// SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[_4MB]);
// 关联了引用队列,当软引用所关联的byte[]被回收时,软引用自己会加入到queue中去
SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[_4MB], queue);
list.add(ref);
System.out.println(list.size());
}
// 从队列中去除没软引用
Reference<? extends byte[]> reference = queue.poll();
while (reference != null) {
list.remove(reference);
reference = queue.poll();
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(Arrays.toString(ref.get()));
}
}
}
3、弱引用
概述
:仅有弱引用引用该对象。在垃圾回收时,无论内存是否充足,都会回收弱引用对象。可以配合引用队列来释放弱引用自身
public class WeakReferenceDemo {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<WeakReference<byte[]>> list = new ArrayList<WeakReference<byte[]>>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> reference = new WeakReference<byte[]>(new byte[_4MB]);
list.add(reference);
for (WeakReference<byte[]> ref : list) {
System.out.println(Arrays.toString(ref.get()));
}
System.out.println();
}
System.out.println("循环结束");
}
}
4、虚引用
概述
:必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 ReferenceHandler 线程调用虚引用相关方法释放直接内存
5、终结器引用
概述
:无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
可回收算法
1、引用计数法
概述
:一个对象被变量引用时计数加 1,当该对象不被变量引用则减 1,当计数为 0 时,可以被回收缺点
:会出现循环引用的情况,这样导致两个对象都不能被垃圾回收
2、可达性分析算法
概述
:在垃圾回收之前,对堆内存中的对象进行扫码,检测对象是否被根对象直接或间接的引用,如果被引用则不能被回收
垃圾回收算法
1、标记清除
步骤
- 标记没有引用的对象
- 将标记为垃圾的对象进行释放,即将对象所占用内存的起始、结束地址记录在空闲地址列表中,等再次分配时再从列表中获取
优点
:垃圾回收速度快缺点
:容易产生内存碎片
2、标记整理
步骤
- 标记没有引用的对象
- 将标记为垃圾的对象进行释放,并把没被标记回收的对象进行整理
优点
:不产生内存碎片缺点
:效率较慢
3、复制
步骤
- 标记没有引用的对象
- 将没有标记的对象从 FROM 区复制到 TO 区
- 清除 FROM 区,并且将 TO 区与 FROM 区交换
优点
:不产生内存碎片缺点
:占用双倍的内存空间
分代回收
- 对象首先分配在伊甸园
- 当新生代空间不足时,触发 Minor GC ,伊甸园和 From 幸存区存活的对象复制到 To 幸存区中,存活的对象年龄加 1 并且交换两个幸存区
- Minor GC 会引发 STW ,暂停其它用户线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升到老年代,该阈值默认为 15
- 当老年代空间不足时,会先尝试触发 Minor GC,如果空间仍不足,那么触发 Full GC (进行全部的清理),STW 时间更长
垃圾回收器
1、SerialGC
概述
:串行垃圾回收器,适用于堆内存较小,单核 CPU 的情况启动
:-XX:UseSerialGC = Serial + SerialOld(新生代使用复制算法,老年代使用标记整理算法)工作过程
:多个线程执行 Java 代码,当堆内存不足时,会在安全点暂停其他线程,进行垃圾回收,此时只有一个垃圾回收线程运行,其他线程处于阻塞中。当垃圾线程回收完毕,再唤醒其他线程
2、ParallelGC
概述
:并发垃圾回收器是以吞吐量优化的垃圾回收器。即单位时间内,多个线程进行垃圾回收,使多次 STW 平均时间变短,从而提高吞吐量场景
:适用于堆内存较多,多核 CPU 的场景
VM参数
- -XX:+UseParallelGC ~ XX:+UseParallelOldGC:新生代使用复制算法,老年代使用标记整理算法
- -XX:+UseAdaptiveSizePolicy:设置新生代的内存为自适应策略
- -XX:GCTimeRatio = ratio:调整吞吐量,一般设置为19。公式为1 / (1 + ratio),如1 / (1 + 99) = 0.01,即垃圾回收时间不能超过总时间的 0.01,如果达不到这个目标,则 GC 会自动调整堆大小来达到目标
- -XX:MaxGCPauseMillis = ms:垃圾回收时用户线程暂停时间,默认 200 ms,与 GCTimeRatio 参数是对立的
- -XX:ParallelGCThreads = n:设置垃圾回收线程数
3、响应时间优化 GC
概述
:多个线程进行垃圾回收,尽可能让单次 STW 的时间最短,从而降低响应时间场景
:适用于堆内存较多,多核 CPU 的场景
工作流程
- 当老年代内存不足时,线程在安全点暂停,CMS进行初始标记(只标记根对象)
- 初始标记后,唤醒其他用户线程,同时CMS进行并发标记(标记剩余垃圾)
- 然后暂停所有工作线程,进行重新标记
- 然后唤醒所有工作线程,并且进行并发清理
VM 参数
- -XX:+UseConcMarkSweepGC ~ -XX:+UseParkNewGC ~ SerialOld:CMS 是工作在老年代基于标记清除算法的 GC,如果并发失败时,CMS 会退化成 SerialGC,PN是工作在新生代基于复制算法的 GC
- -XX:ParallelGCThreads = n:并发垃圾回收线程数量,默认 CPU 核心数量
- -XX:ConcGCThreads = n:并行垃圾回收线程数量,一般为并行线程数量的 1/4
- -XX:CMSInitiatingQccupancyFaction = percent:设置老年代内存占比多少时进行垃圾回收,默认 65
- -XX:CMSScavingeBeforeRemark:重新标记前对新生代先进行垃圾回收,减少重新标记时新生代引用了老年代对象
4、G1
概述
:将堆划分成多个大小相等的区域,注重吞吐量和低延迟的垃圾回收器,整体使用标记整理算法,两个区域之间是复制算法。适用于超大堆内存的环境
工作流程
- Young Collection 阶段:新生代 GC 进行初始标记,并进行复制(E->S->O) ,如果老年代的对象引用新生代对象,则标记为脏卡,当进行GC Root遍历时只需要找脏卡,提高效率
- Concurrent Mark 阶段:当老年代占用堆空间比例达到阈值时,当垃圾回收的速度高于产生的速度,进行并发标记,否则,退化成串行回收,进行 Full GC
- Mixed Collection 阶段:进行最终标记和拷贝存活,这两个过程都会发生 STW
VM参数
- -XX:+UseG1GC:启用 G1
- -XX:InitiatingHeapQccupancyPercent = percent:设置老年代内存占比多少时进行垃圾回收,默认 45
- -XX:+G1HeapRegionSize = size:设置区域大小,每个区域都可以做为独立的新生代和老年代
- -XX:+MaxGCPauseMillis = time:垃圾回收时用户线程暂停时间
回收调优
1、相关 VM 参数
- -Xms:堆初始化大小
- -Xmx:堆最大大小
- -Xmn:新生代大小
- -XX:InitialSurvivorRatio = ratio 和 -XX+UseAdaptiveSizePolicy:幸存区比例(动态)
- -XX:SurvivorRatio = ratio:幸存区比例
- -XX:MaxTenuringThreshold = threshold:晋升阈值
- -XX:+PrintTenuringDistribution:晋升详情
- -XX:+PrintGCDetail -verbose:gc:GC 详情
- -XX:+ScavengeBeforeFullGC:FullGC 前 MinorGC
- -XX:+PrintFlagsFinal -version | findstr “GC”:查看虚拟机运行参数
2、新生代调优
特点
:创建对象时会在伊甸园中分配内存,速度非常快,死亡对象回收成本是零;伊甸园和幸存区 From 使用复制算法将数据复制到幸存区 To,清除数据的速度远远低于 Full GC新生代调优
:并发量 * (请求数据大小 - 响应数据大小),如并发量 1000,请求数据 - 响应数据:512 K,那么新生代应设置为 521 M幸存区调优
- 容量要尽量大,如果内存少,会提前把对象存入老年代,那么只有触发了 Full GC 时才会清除对象
- 晋升阈值要适当,让长时间存放对象尽快存入老年代
# 堆初始化大小
-Xms = size
# 设置新生代大小
-Xmn = size
# 幸存区比例
-XX:SurvivorRatio = ratio
# 设置晋升阈值
-XX:MaxTenuringThreshold = threshold
# 打印晋升阈值
-XX:PrintTenuringDistribution
2、老年代调优
避免 GC
- 数据是不是太多,查看是否查询了大表
- 数据表示是否太臃肿
- 是否内存泄漏,可以使用软、弱和第三方缓存解决
调优策略
:以 CMS 为例- CMS 老年代内存越大越好
- 在尝试新生代调优,再考虑老年代调优
- 观察发生 Full GC 时老年代内存占用,使用 -XX:CMSInitiatingOccupancyFaction = percent,将老年代内存上调 1/4 - 1/3
3、案例
# 案例1:Full GC 和 Minor GC频繁
解决:增大新生代空间
# 案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
解决:根据分析,CMS进行垃圾回收时,时间都消耗在重新标记,可以设置-XX:CMSScavingeBeforeRemark:
重新标记前对新生代先进行垃圾回收,减少重新标记时新生代引用了老年代对象
# 案例3:老年代充实情况下,发生 Full GC(1.7版本)
解决:根据分析,并不是空间不足或空间碎片过多导致并发失败后进行Full GC,而是因为1.7版本使用的是永久代,
永久代空间不足时也会发生Full GC,所以可以增加永久代大小,或使用1.8以上版本