三、垃圾回收
1、如何判断对象可以回收
1)引用计数法
定义:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
但是存在循环引用的问题:
2)可达性分析算法
一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过
程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,
或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的
参数、局部变量、临时变量等 - 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
3)四种引用
-
强引用:强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object
obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 -
软引用 (SoftReference):在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身 -
弱引用 (WeakReference):在垃圾回收时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
可以配合引用队列来释放弱引用自身 -
虚引用 (PhantomReference):无法通过虚引用获取一个对象实例。必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
-
终结器引用 (FinalReference):无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象
软引用的例子:
设置堆内存大小为 20m,直接list.add(byte[]),会报 java.lang.OutOfMemoryError: Java heap space
异常,因为是强引用,无法垃圾回收。
使用 SoftReference
可以添加到 list 集合中,不会报错,但是执行到内存不足时,会进行垃圾回收。
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_3 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
/*List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();*/
soft();
}
public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get()); // null
}
}
}
输出结果中软引用被为null,可以通过引用队列释放软引用自身
/**
* 演示软引用, 配合引用队列
*/
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());
}
}
}
弱引用的例子:
/**
* 演示弱引用
* -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());
}
}
2、垃圾回收算法
1)标记清除(Mark-Sweep)
- 速度较快
- 会造成内存碎片
2)标记整理(Mark-Compact)
- 速度慢
- 没有内存碎片
3)复制(Copy)
- 不会有内存碎片
- 需要占用两倍内存空间
3、分代垃圾回收
老年代:常时间使用的对象,垃圾回收很久发生一次。单线程的垃圾收集器Serial Old(串行),多线程并发Parallel Old(吞吐量优先)使用的垃圾回收方法是 标记-整理,多线程并发的垃圾收集器CMS(Concurrent Mark Sweep)(响应时间优先)使用的垃圾回收方法是 标记-清除
新生代:经常需要更换的对象,垃圾回收频繁,"朝生夕灭"的特点。垃圾收集器(Serial)使用的方法是 复制
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的
对象年龄加 1并且交换 from to - minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
1) 相关JVM参数
4、垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
- 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
- 响应时间优先
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
1)串行
-XX:+UseSerialGC = Serial + SerialOld
Serial收集器:它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强
调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的。
2)吞吐量优先
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)
Parallel Scavenge收集器:老年代的垃圾回收器,支持多线程并发收集,基于标记-整理算法实
现。
3)响应时间优先
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
CMS(Concurrent Mark Sweep) 收集器: 是一种低延迟、响应时间优先的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为
关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非
常符合这类应用的需求。
5、G1 (Garbage First)
使用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200ms
- 超大堆内存,会将堆划分为多个大小相等的 Region
- 整体上是标记 + 整理算法,两个区域之间是复制算法
相关 JVM 参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
1)G1 垃圾回收阶段
2)Young Collection
会 STW
3)Young Collection + CM(并发标记)
- 在 Young GC 时会进行 GC Root 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
4)Mixed Collection
会对 E(伊甸园区)、S(幸存区)、O(老年代)进行全面垃圾回收
- 最终标记(Remark)会 STW
- 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms
5)Full GC
- SerialGC
- 新生代内存不足发生的垃圾收集 -minor gc
- 老年代内存不足发生的垃圾 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
6)跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
- 卡表与 Remembered Set
- 在引用变更时通过 post-write barrier + dirty card queue
- concurrent refinement threads 更新 Remembered Set
7)Remark
pre-write barrier + satb_mark_queue
重新标记阶段
在垃圾回收时,收集器处理对象的过程中
- 黑色:已被处理,需要保留的
- 灰色:正在处理中的
- 白色:还未处理的
6、垃圾调优
C:\Program Files\Java\jdk1.8.0_271\bin\java -XX:+PrintFlagsFinal -version | findstr "GC"
可以根据参数去查询具体的信息
1)调优领域
- 内存
- 锁竞争
- cpu 占用
- io
- gc
2)确定目标
低延迟/高吞吐量? 选择合适的GC
- CMS,G1, ZGC
- ParallelGC
- Zing
3) 最快的 GC是不发生 GC
- 查看 FullGC 前后的内存占用,考虑下面几个问题
- 数据是不是太多?
- resultSet = statement.executeQuery(“select * from 大表 limit n”)
- 数据表示是否太臃肿?
- 对象图
- 对象大小 16 Integer 24 int 4
- 是否存在内存泄漏?
- static Map map =
- 软
- 弱
- 第三方缓存实现
- 数据是不是太多?
4)新生代调优
- 新生代的特点
- 所有的 new 操作的内存分配非常廉价
- TLAB thread-lcoal allocation buffer
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC 的时间远远低于 Full GC
- 所有的 new 操作的内存分配非常廉价
一般对新生代调优,可以调优的空间大
- 越大越好吗?
-Xmn (设置堆空间中新生代的内存大小)
Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is
performed in this region more often than in other regions. If the size for the young generation is
too small, then a lot of minor garbage collections are performed. If the size is too large, then only
full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
回答:新生代的空间越大,吞吐量越大,(Full GC的次数少了),但是空间大到一定程度时,吞吐量会下降,因为空间太大,一旦触发 Full GC 时,垃圾回收时间会大大增加。
- 新生代能容纳所有【并发量 * (请求-响应)】的数据
- 幸存区大到能保留【当前活跃对象 + 需要晋升对象】
- 晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold -XX:+PrintTenuringDistrubution
5)老年代调优
以 CMS 为例
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent
值越低,老年代触发垃圾回收的时机越早
6)案例
案例1:Full GC 和 Minor GC 频繁
案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
案例3:老年代充裕情况下,发生 Full GC(jdk1.7前)