对象已死?
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。这也就是需要回收的对象。
引用计数算法是对象记录自己被多少程序引用,引用计数为零的对象将被清除
内存泄漏问题:
可达性分析法
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 从GCRoots开始向下搜索,搜索走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连时,就说明这个对象是不可用的,那虚拟机就会判断它是可回收的对象
- 哪些对象可以作为GCRoots?
虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象
四种引用
- 强引用:
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 - 软引用(SoftReference):
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象;可以配合引用队列来释放软引用自身
应用场景:如用来做一些网页缓存、图片缓存
/**
* 演示软引用, 配合引用队列
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_4 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
- 弱引用(WeakReference):
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_5 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
-
虚引用(PhantomReference):
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
-
终结器引用(FinalReference):
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象
垃圾回收算法
标记-清除算法
标记的是可回收对象
- 速度较快
- 会产生内存碎片
标记-整理算法
- 速度较慢
- 没有内存碎片
标记-复制算法
标记可回收对象,将存活对象复制到To区域,再交换From和To
- 没有内存碎片
- 可用内存空间减半
分代垃圾回收
分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用copy 复制到 to 中,存活的对象年龄加 1并且交换 from、to
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(占对象头4bit)
- 根据对象年龄有另外一个策略也会让对象进入老年代,不用等待15次GC之后进入老年代,他的大致规则就是,假如当前放对象的Survivor,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就可以直接进入老年代了
- 大对象直接进入老年代:如果设置了-XX:PretenureSizeThreshold这个参数,那么如果你要创建的对象大于这个参数的值,比如分配一个超大的字节数组,此时就直接把这个大对象放入到老年代,不会经过新生代
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
相关VM参数
垃圾回收器
串行
- 单线程
- 适合堆内存较小、处理器数量少,如个人电脑
-XX:+UseSerialGC = Serial + SerialOld
,Serial使用的是标记-复制算法(新生代),SerialOld使用的是标记-整理算法(老年代)
吞吐量优先
- 多线程
- 适合堆内存较大、多核CPU
- 让单位时间内,STW时间最短,垃圾回收时间占比最低,就能提高吞吐量,
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾回收时间)
-XX:+UseParallelGC
和-XX:+UseParallelOldGC
(开启其中一个,另一个会自动开启,JDK1.8下默认是开启的)-XX:+UseAdaptiveSizePolicy
:开启自适应调整伊甸园和survivor区比例以及晋升老年代的对象大小,会自动调整参数来达到最大吞吐量- 使用
-XX:GCTimeRatio=ratio
(关注吞吐量)和-XX:MaxGCPauseMillis=ms
(关注最大停顿时间)给虚拟机设定一个优化目标 -XX:ParallelGCThreads=n
:设定并行垃圾回收线程的数量- 这里的并行指多个垃圾回收线程同时运行
响应时间优先(低延迟)
- 多线程
- 适合堆内存大、多核CPU
- 低延迟:尽可能的缩短单次垃圾回收的时间(STW)
-XX:+UseParNewGC
和-XX:+UseConcMarkSweepGC
或SerialOld
(CMS采用标记-清除算法,当因为碎片过多无法存新对象时,会出现并发失败,会退化到SerialOld串行回收器)-XX:ParallelGCThreads=n
设置并行线程数
-XX:ConcGCThreads=threads
设置并发线程数(一般为并行线程数的1/4)-XX:CMSInitiatingOccupancyFraction=percent
:设置触发CMS垃圾回收的时机,即内存占用达到percent百分比时就进行垃圾回收(预留空间给浮动垃圾)-XX:+CMSScavengeBeforeRemark
:再回收老年代时,先对新生代进行一次垃圾回收- 这里的并发指垃圾回收线程和用户线程可以同时运行
G1回收器
关于G1
-
定义:G1即Garbage First
-
发展:
2004 论文发布
2009 JDK 6u14 体验
2012 JDK 7u4 官方支持
2017 JDK 9 默认 -
适用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 适合超大堆内存,会将堆划分为多个大小相等的
Region
- 整体上是
标记+整理
算法,两个区域之间是复制
算法
-
相关VM参数
-XX:+UseG1GC
:开启G1回收器(JDK9默认)-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1垃圾回收阶段
阶段一-Young Collection
- 新生代垃圾回收会STW(较短)
- 幸存的对象同样使用复制算法拷贝到幸存区
- 幸存区存活一定次数就会进入老年代
阶段二-Young Collection+并发标记
- 在 Young GC 时会进行 GC Root 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent
(默认45%)
阶段三-Mixed Collection
- 会对 E、S、O 进行全面垃圾回收
- 最终标记(Remark)会 STW
- 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms
设置垃圾回收最大停顿时间,当时间不够时,G1对于老年代的垃圾回收就只会回收那些回收价值较高的对象
总结Full GC
- SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足-分情况:并发失败才算重GC
G1 - 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足-分情况:当内存回收速度跟不上垃圾产生速度,才算重GC
Young Collection 跨代引用问题
- 新生代回收的跨代引用(老年代引用新生代)问题
- 老年代使用卡表,新生代使用Remembered Set记录有哪些脏卡
- 在引用变更时通过
post-write barrier
更新,该异步指令存放在dirty card queue
中 - concurrent refinement threads 更新 Remembered Set
- 卡表中引用了新生代的卡为脏卡
Remark重标记
- 重标记:进行进一步的检查,防止并发标记过程中对象引用改变导致错误
- 写屏障:
pre-write barrier
,将改变引用的对象加入到satb_mark_queue
- 下图:黑色(已处理的存活对象)、灰色(正在处理的对象)、白色(垃圾或未处理的对象)
G1的一些优化
JDK 8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
开启字符串去重
//JDK8中字符串底层是char数组
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查队列中是否有字符串重复
- 如果它们值一样,让它们引用同一个 char[]
注意,与 String.intern() 不一样
String.intern() 关注的是字符串对象
而字符串去重关注的是 char[]
在 JVM 内部,使用了不同的字符串表
JDK 8u40 并发标记类卸载
- 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark
默认启用
JDK 8u60 回收巨型对象
- 一个对象大于 region 的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 (即卡表中没有引用指向该巨型对象)的巨型对象就可以在新生代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为 FullGC
- JDK 9 之前需要使用
-XX:InitiatingHeapOccupancyPercent
- JDK 9 可以动态调整
-XX:InitiatingHeapOccupancyPercent
改为只用来设置初始值- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
垃圾回收调优
Windows下查看已设置的GC参数:"C:\Program Files\Java\jdk1.8.0_91\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
调优预备知识
掌握 GC 相关的 VM 参数,会基本的空间调整
掌握相关工具
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则
调优的方向
调优不仅仅局限于GC调优:
- 内存
- 锁竞争
- cpu 占用
- io
确定调优的目标
- 低延迟(如互联网项目)还是高吞吐量(如科学运算),选择合适的回收器
- 低延迟:CMS,G1,ZGC
- 高吞吐量:ParallelGC
- Zing
最快的GC是不发生GC
查看 FullGC 前后的内存占用,当频繁发生GC时,要考虑下面几个问题(是因为自身代码导致的问题?)
-
数据是不是太多?
例如:resultSet = statement.executeQuery(“select * from 大表”)
就会加载太多数据到内存,可以在sql后加limit n限制 -
数据表示是否太臃肿?
- 对象图
- 对象大小 (最小的对象都要占16字节) (Integer 24字节对比int 4字节)
-
是否存在内存泄漏?
例如使用Map存储了过多数据?
避免内存泄漏:- 使用软引用
- 使用弱引用
- 使用第三方缓存实现,如Redis
新生代的GC调优
新生代特点
- 所有的 new 操作的内存分配非常廉价
- 每个线程都在伊甸园中有一块私有区域:TLAB,即thread-local allocation buffer,来避免多线程分配伊甸园内存时互相干扰
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC 的时间远远低于 Full GC
新生代内存越大越好?
合适的新生代内存:
- 新生代能容纳所有【并发量 * (请求响应占用的内存)】的数据
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
若幸存区设置太小,会导致“短命”的幸存对象提前进入老年代 - 晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
老年代的GC调优
以CMS垃圾回收器为例:
- CMS 的老年代内存越大越好,预留更多空间,来避免因为浮动垃圾引起的并发回收失败(失败会退化到串行回收)
- 先尝试不做老年代调优,如果系统没有出现 Full GC,就先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent
:设置老年代内存达到多少就触发CMS垃圾回收
调优案例
- 案例1 Full GC 和 Minor GC频繁
是因为新生代内存太小?调整新生代内存大小 - 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)
通过打印日志查看是回收的哪个阶段耗时较长 - 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)
JDK1.7是使用永久代作为方法区的实现,故永久代的空间不足也会导致Full GC