JVM之垃圾回收
# 1 如何判断对象可以回收
# 1.1 引用计数法
当一个对象被引用时,则会将该对象进行引用计数。数值的大小代表被引用的数量,当数值为0时,则会被当作垃圾进行回收。
弊端:当有对象形成互相引用的关系,那么则会造成内存泄漏的问题。
Java虚拟机 不使用该方式来进行垃圾回收判断
# 1.2 可达性分析算法
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看是否能够沿着 GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
哪些对象可以作为 GC Root ?
Tip:
使用 Eclipse 官方提供的可视化工具 Memory Analyzer 进行内存分析
官网下载地址:https://www.eclipse.org/mat/downloads.php
- 使用说明
- 使用
jps
命令定位到进程id- 使用
jmap -dump:format=b,live,file=<文件名.bin> 进程id
format=b
:转储文件格式live
:存活对象,会在快照前进行一次垃圾回收file=<文件名.bin>
:将内存快照存放到文件中使用案例
public class Demo2_1 { public static void main(String[] args) throws IOException { List<Object> list = new ArrayList<>(); list.add("a"); list.add("b"); System.out.println(1); System.in.read(); list = null; System.out.println(2); System.in.read(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13对以上代码进行运行时内存抓取,来查看控制台输出 1 和 2 时,内存中数据的不同之处
运行代码后,使用
jps
命令获取进程id,然后 dump 此时的内存状态,使用 Memory Analyzer 读取内存信息
dump 第二次内存状态,使用 Memory Analyzer 读取内存信息
# 1.3 四种引用
强引用
- 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身
弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身
虚引用(PhantomReference)
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
# 1.3.1 软引用
案例代码,用来演示内存不够时,GC 垃圾回收会做什么事情,需要添加虚拟机参数 -Xmx20m -XX:+PrintGCDetails -verbose:gc
public class Demo2_2 {
private static final int _4MB = 1024 * 1024 * 4;
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]);
}
}
}
2
3
4
5
6
7
8
9
分析以上代码结果:循环向数组中添加5次大小为 4M 的字节数组,因为将虚拟机堆的大小设置为 20M,所以该程序的结果就是,发生了几次 GC垃圾回收后,抛出异常 java.lang.OutOfMemoryError: Java heap space
使用软引用解决这个问题,代码修改为:将 byte[] 数据通过一个 SoftReference 软引用对象来实例
public class Demo2_2 {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new SoftReference<>(new byte[_4MB]));
list.forEach(ref -> System.out.print(ref.get() + "\t"));
System.out.println();
}
}
}
2
3
4
5
6
7
8
9
10
11
运行结果如下,可以发现
- 在加入第四个对象的时候,已经发生了内存空间不充裕的情况,调用了 Minor GC,清理了新生代的内存空间
- 在加入第五个对象的时候,使用 Minor GC 后发现根本没有垃圾可以清理,就调用了 Full GC 进行深度清理 ,再使用 Minor GC 尝试再次清理新生代,发现还是不起作用,再次使用 Full GC 时就进行了软引用内存的清理,使得前面存入的软引用数据全部清空了,但是软引用对象
SoftReference
不会进行释放 - 印证了:当内存实在不充裕的情况下,会将软引用数据清空
[B@404b9385
[B@404b9385 [B@6d311334
[B@404b9385 [B@6d311334 [B@682a0b20
[GC (Allocation Failure) [PSYoungGen: 3804K->496K(6144K)] 16092K->13164K(19968K), 0.0012198 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@404b9385 [B@6d311334 [B@682a0b20 [B@3d075dc0
[GC (Allocation Failure) --[PSYoungGen: 4704K->4704K(6144K)] 17372K->17436K(19968K), 0.0008617 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 4704K->4523K(6144K)] [ParOldGen: 12732K->12697K(13824K)] 17436K->17221K(19968K), [Metaspace: 4212K->4212K(1056768K)], 0.0059000 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 4523K->4523K(6144K)] 17221K->17245K(19968K), 0.0006258 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4523K->0K(6144K)] [ParOldGen: 12721K->781K(8704K)] 17245K->781K(14848K), [Metaspace: 4212K->4206K(1056768K)], 0.0073492 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
null null null null [B@214c265e
2
3
4
5
6
7
8
9
10
现在还有一个问题,软引用数据已经清空了,那软引用对象 SoftReference
什么时候、又是怎么被清空的?
- 需要使用
ReferenceQueue
队列来存储对应的需要进行垃圾回收的软引用对象,然后手动进行对应数组数据的删除,之后的软引用回收交给 GC垃圾回收 去做
public class Demo2_2 {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 软引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联软引用队列,将来软引用对象关联的数据 byte[] 被清空了之后,软引用自己会加入到队列中
list.add(new SoftReference<>(new byte[_4MB], queue));
list.forEach(ref -> System.out.print(ref.get() + "\t"));
System.out.println();
}
// 去除没用的软引用对象
System.out.println("=====华丽分界线=====");
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
运行结果可以发现所有空的软引用对象都从数组中删除了
[B@404b9385
[B@404b9385 [B@6d311334
[B@404b9385 [B@6d311334 [B@682a0b20
[B@404b9385 [B@6d311334 [B@682a0b20 [B@3d075dc0
null null null null [B@214c265e
=====华丽分界线=====
[B@214c265e
2
3
4
5
6
7
# 1.3.2 弱引用
使用弱引用来存放数据,需要添加虚拟机参数 -Xmx20m -XX:+PrintGCDetails -verbose:gc
public class Demo2_3 {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) throws IOException {
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new WeakReference<>(new byte[_4MB]));
list.forEach(ref -> System.out.print(ref.get() + "\t"));
System.out.println();
}
}
}
2
3
4
5
6
7
8
9
10
11
运行结果如下,分析这几次GC的结果
- 执行 Minor GC,清除新生代,获得内存空间
- 执行 Minor GC,清除新生代,将上一次还未放入老年代的弱引用数据清除,获得内存空间
- (3) -> (6) 同上步骤来获取内存空间
- 直到第 (7) 的时候,Minor GC 已经缓解不了当前内存空间不足的窘境,发起了 Full GC,将所有的弱引用数据全部清空,获取更多的内存空间
从上述引出了一个问题,不是说 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
?
是因为有些弱引用对象存活到了老年代中,而触发 Minor GC 却清理不到老年代中的内存,所以只有当触发了 Full GC 的时候才能进行老年代中的弱引用对象数据的清理。
弱引用对象的释放同软引用的释放方式,需要一个 WeakReferenceQueue
队列来进行弱引用对象的绑定。
# 2 垃圾回收算法
# 2.1 标记清除
定义: Mark Sweep
- 速度较快
凡是没有被任何 GC Root 引用的对象,都是没有用的数据,则会被清除
缺点:内存碎片过多,使得当有更大的内存需要使用时导致内存溢出
# 2.2 标记整理
定义:Mark Compact
- 速度慢
在内存清理时,顺便将内存碎片进行整理
# 2.3 复制
定义:Copy
- 不会有内存碎片
- 需要占用双倍内存空间
总有一半的内存空间用来进行内存重排
# 3 分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
- minor gc 会引发 stop the world(STW),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
# 3.1 相关 VM 参数
描述 | 命令 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx | -XX:MaxHeapSize=size |
新生代大小 | -Xmn | -XX:NewSize=size + -XX:MaxNewSize=size |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
# 3.2 GC分析
案例代码,需要使用虚拟机参数 -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public class Demo2_4 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
// TODO
}
}
2
3
4
5
6
7
8
9
10
运行以上案例代码,来分析一下打印的数据
def new generation
:新生代的信息,total 表示最大内存大小(伊甸园区 + form区),used 表示已使用,后面的十六进制代表地址eden space
:伊甸园,占比 80%from space
:from 分区,占比10%to space
:to 分区,占比10%
tenured generation
:老年代的信息,total 表示最大内存大小,uesd 表示已使用Metaspace
:元数据信息,并不在堆中,只是方便查看,目前不关心这块
Heap
def new generation total 9216K, used 2020K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf9048, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3164K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 345K, capacity 388K, committed 512K, reserved 1048576K
2
3
4
5
6
7
8
9
# 3.2.1 分析1
修改案例代码为以下代码,将大小为 7MB 的数据放入数组
public class Demo2_4 {
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
}
2
3
4
5
6
7
运行结果进行分析,发现
- 执行了一次 Minor GC,伊甸园最大空间为 8MB,开辟一块 7MB 的内存需要整理堆空间,将一部分数据放入了 from 分区
- 伊甸园占用了 94% 空间,此时还没用上老年代的空间
[GC (Allocation Failure) [DefNew: 1855K->600K(9216K), 0.0025878 secs] 1855K->600K(19456K), 0.0032786 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 8342K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 94% used [0x00000000fec00000, 0x00000000ff38f7a0, 0x00000000ff400000)
from space 1024K, 58% used [0x00000000ff500000, 0x00000000ff596228, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3223K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
2
3
4
5
6
7
8
9
10
# 3.2.2 分析2
在 分析1 的基础上再添加 512KB
public class Demo2_4 {
private static final int _512KB = 512 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
}
}
2
3
4
5
6
7
8
9
运行结果进行分析,发现
- 执行了一次 Minor GC,在 7MB 的基础上添加 512KB 伊甸园已经把内存用完了,此时老年代还未用上
[GC (Allocation Failure) [DefNew: 1685K->599K(9216K), 0.0011811 secs] 1685K->599K(19456K), 0.0012264 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 8791K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 100% used [0x00000000fec00000, 0x00000000ff400000, 0x00000000ff400000)
from space 1024K, 58% used [0x00000000ff500000, 0x00000000ff595d10, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
2
3
4
5
6
7
8
9
10
# 3.2.3 分析3
在 分析2 的基础上再添加 512KB
public class Demo2_4 {
private static final int _512KB = 512 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);
}
}
2
3
4
5
6
7
8
9
10
运行结果进行分析,发现
- 执行了两次 Minor GC,在第二次添加 512KB 内存时发生的 Minor GC 将新生代中的数据全部提升到了老年代(虽然此时还没到年龄阈值),使得伊甸园中的使用量大大减小
[GC (Allocation Failure) [DefNew: 1676K->598K(9216K), 0.0011451 secs] 1676K->598K(19456K), 0.0011917 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 8769K->540K(9216K), 0.0053774 secs] 8769K->8303K(19456K), 0.0054079 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 1218K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 8% used [0x00000000fec00000, 0x00000000feca9748, 0x00000000ff400000)
from space 1024K, 52% used [0x00000000ff400000, 0x00000000ff487368, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7762K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 75% used [0x00000000ff600000, 0x00000000ffd949f8, 0x00000000ffd94a00, 0x0000000100000000)
Metaspace used 3230K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
2
3
4
5
6
7
8
9
10
11
# 3.2.4 分析4-oom
当有以下情况时,直接添加 8MB 的数据
public class Demo2_4 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
}
2
3
4
5
6
7
运行结果如下,发现
- 不会进行垃圾回收,伊甸园内存 8MB,该数据的大小已经在伊甸园中放不下了,JVM 将其直接晋升到老年代进行存储
Heap
def new generation total 9216K, used 2183K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee21fb8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3276K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 355K, capacity 388K, committed 512K, reserved 1048576K
2
3
4
5
6
7
8
9
# 3.2.5 分析5
那如果在 分析4 的情况下再添加 8MB 的数据
public class Demo2_4 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}
}
2
3
4
5
6
7
8
运行结果如下,发现
- 在抛出
java.lang.OutOfMemoryError: Java heap space
异常前进行了两次垃圾回收,这两次垃圾回收是 JVM 尝试自救而触发的,当这两次的垃圾回收还无法解决时,就直接抛出了异常
[GC (Allocation Failure) [DefNew: 2019K->607K(9216K), 0.0018340 secs][Tenured: 8192K->8798K(10240K), 0.0032842 secs] 10211K->8798K(19456K), [Metaspace: 3254K->3254K(1056768K)], 0.0057250 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 8798K->8780K(10240K), 0.0015195 secs] 8798K->8780K(19456K), [Metaspace: 3254K->3254K(1056768K)], 0.0015379 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66800, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8780K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 85% used [0x00000000ff600000, 0x00000000ffe932e8, 0x00000000ffe93400, 0x0000000100000000)
Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 362K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.zhuhjay.Demo2_4.main(Demo2_4.java:19)
2
3
4
5
6
7
8
9
10
11
12
13
# 3.2.6 分析6
当一个线程发生了 OOM,会不会导致主线程停止运行呢?
public class Demo2_4 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep start...");
Thread.sleep(1000L);
System.out.println("sleep end...");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
主线程仍然能够正常的运行,并且线程抛出异常后,会将线程使用的资源进行释放
sleep start...
[GC (Allocation Failure) [DefNew: 4023K->840K(9216K), 0.0021618 secs][Tenured: 8192K->9030K(10240K), 0.0028407 secs] 12215K->9030K(19456K), [Metaspace: 4205K->4205K(1056768K)], 0.0050747 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 9030K->8973K(10240K), 0.0024870 secs] 9030K->8973K(19456K), [Metaspace: 4205K->4205K(1056768K)], 0.0025340 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.zhuhjay.Demo2_4.lambda$main$0(Demo2_4.java:20)
at com.zhuhjay.Demo2_4$$Lambda$1/1324119927.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
sleep end...
Heap
def new generation total 9216K, used 1540K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed81218, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8973K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 87% used [0x00000000ff600000, 0x00000000ffec37f8, 0x00000000ffec3800, 0x0000000100000000)
Metaspace used 4722K, capacity 4836K, committed 4992K, reserved 1056768K
class space used 527K, capacity 592K, committed 640K, reserved 1048576K
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4 垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人电脑
- 吞吐量优先
- 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
- 响应时间优先
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
# 4.1 串行
-XX:+UseSerialGC
通过以下部分组成
Serial
: 工作在新生代,是复制
算法SerialOld
: 工作在老年代,是标记整理
算法
# 4.2 吞吐量优先
-XX:+UseParallelGC
(JDK1.8 默认开启,使用复制算法,工作在新生代) ~ -XX:+UseParallelOldGC
(使用标记整理算法,工作在老年代)只要开启了其中一个,另一个就会联动开启,垃圾回收的线程数与CPU核数有关。在垃圾回收时,会瞬间把CPU拉满
-XX:+UseAdaptiveSiizePolicy
:动态调整新生代中伊甸园与幸存区的大小-XX:GCTimeRatio=ratio
:吞吐量目标1/(1+radio)
,默认 99,1/100=0.01 -> 100分钟内有1分钟的垃圾回收-XX:MaxGCPauseMillis=ms
:最大暂停毫秒数,默认 200,与上面的GCTimeRatio
互斥,需要折中考虑-XX:ParallelGCThreads=n
:控制运行时的线程数
# 4.3 响应时间优先
-XX:+UseConcMarkSweepGC
(并发标记清除算法,工作在老年代的 CMS垃圾回收器) ~ -XX:+UseParNewGC
(基于复制算法的垃圾回收器,工作在新生代) ~ SerialOld
(当 CMS 垃圾回收器并发失败时,会自动退化到该垃圾回收器)只有在 初始标记
和 重新标记
阶段才会触发 STW
-XX:ParallelGCThreads=n
(并行线程数,与CPU核数有关) ~-XX:ConcGCThreads=threads
(并发线程数)-XX:CMSInitiatingOccupancyFraction=percent
:CMS 垃圾回收触发比例(当老年代内存占用到预定值后进行垃圾回收),值越小,CMS 垃圾回收越频繁- 浮动垃圾:在并发清理的同时会产生新的垃圾,这些垃圾只能等到下一次垃圾清理时才能进行回收,需要预留一些空间
-XX:+CMSScavengeBeforeRemark
:在重新标记阶段,新生代对象可能会引用老年代的对象,这时候去做可达性分析需要扫描整个堆,为了避免这个耗时的操作,可以先对新生代进行一次垃圾回收,然后再进行老年代的可达性分析
Tip:
在使用 CMS 进行垃圾回收时,会造成内存碎片过多而使得并发失败的问题,这时候会将垃圾回收器退化到 SerialOld,进行一次串行的标记整理后才能恢复 CMS 垃圾回收,这时候垃圾回收的时间会变长。
# 4.4 G1
定义:Garbage First
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 超大堆内存,会将堆划分为多个大小相等的
Region
(区) - 整体上是
标记整理
算法,两个区域之间是复制
算法
相关 JVM 参数
-XX:+UseG1GC
:使用 G1 垃圾回收,JDK9 开始才默认开启-XX:G1HeapRegionSize=size
:G1 堆分区的大小(需要是2的指数次)-XX:MaxGCPauseMillis=time
:暂停目标 默认 200ms
# 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
:最大暂停时间
G1 垃圾回收器会根据最大暂停时间进行有选择的、回收价值较高的垃圾回收
# 5) Full GC
- SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足(两种情况)
- 垃圾产生速度低于垃圾回收速度(有能力进行回收):并发垃圾回收
- 垃圾产生速度高于垃圾回收速度(没有能力进行回收):串行垃圾回收,这时才是真正的 full gc
# 6) Young Collection 跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
- 将老年代引用新生代的对象标记为 脏卡
- 卡表与 Remembered Set(新生代对象记录脏卡对象)
- 在引用变更时通过 post-write barrier + dirty card queue(写屏障+脏卡队列,通过异步的方式让一个线程来维护)
- concurrent refinement threads 更新 Remembered Set
# 7) Remark
在并发标记阶段,当对象引用改变时,jvm会加入一个写屏障,引用改变,写屏障指令就会被执行,会将该对象加入一个队列中,将对象标记为待处理状态,等到整个并发标记结束,进入重新标记阶段会STW,对象出队列,进行标记检查
pre-write barrier
(预写屏障) +satb_mark_queue
(混合标记队列)
# 8) JDK 8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
(默认打开)
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果它们值一样,让它们引用同一个 char[]
- 注意,与 String.intern() 不一样
- String.intern() 关注的是字符串对象
- 而字符串去重关注的是 char[]
- 在 JVM 内部,使用了不同的字符串表
# 9) JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark
默认启用
# 10) JDK 8u60 回收巨型对象
- 一个对象大于 region 的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
# 11) JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为 FullGC
- JDK 9 之前需要使用
-XX:InitiatingHeapOccupancyPercent
- JDK 9 可以动态调整
-XX:InitiatingHeapOccupancyPercent
用来设置初始值- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
# 12) JDK 9 更高效的回收
- 250+增强
- 180+bug修复
- https://docs.oracle.com/en/java/javase/12/gctuning
# 5 调优
# 5.1 调优领域
- 内存
- 锁竞争
- cpu 占用
- io
# 5.2 确认目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS,G1,ZGC
- ParallelGC
- Zing(java虚拟机,几乎零延时的垃圾回收)
# 5.3 最快的GC
最快的 GC 是不发生 GC
- 查看 FullGC 前后的内存占用,考虑下面几个问题
- 数据是不是太多?
resultSet = statement.executeQuery("select * from 大表 limit n")
- 数据表示是否太臃肿?
- 对象图
- 对象大小占用16字节,Integer对象占用24字节,int类型占用4字节
- 是否存在内存泄漏?
static Map map = ...
,只存不释放,死数据过多- 使用软引用
- 使用弱引用
- 第三方缓存实现
- 数据是不是太多?
# 5.4 新生代调优
新生代的特点
- 所有的 new 操作的内存分配非常廉价
TLAB:thread-local allocation buffer
:线程局部分配缓冲区,为每一个线程分配专门的伊甸园空间,避免了线程安全问题,加快了对象创建的速度
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC 的时间远远低于 Full GC
- 所有的 new 操作的内存分配非常廉价
新生代堆内存分配越大越好?
新生代堆内存分配过大,会导致老年代的堆内存占用相对变小,使得当老年代内存紧张时,不断触发 Full GC,使得暂停时间变长,影响性能。Oracle 建议新生代内存大小占整个堆的 25%-50% 之间较为合适。
-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.
新生代能容纳所有
并发量 * (请求-响应)
的数据幸存区大到能保留
当前活跃对象+需要晋升对象
晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
:调整晋升阈值-XX:+PrintTenuringDistribution
:打印晋升详情信息
# 5.5 老年代调优
以 CMS 为例
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent