6.垃圾回收
6.1 判断垃圾
6.1.1 引用计数法
当一个对象被引用一次则计数 1,失去引用计数-1,当计数为0则判断为垃圾。但当对象间存在循环引用时(如下图)会无法被回收。
6.1.2 可达性分析算法
Java中使用可达性分析算法来判断垃圾。肯定不会被垃圾回收的对象为根对象,可以经由根对象直接或间接引用的对象不会被垃圾回收,反则反之。打个比喻:连在串上的葡萄就是不可以被回收的对象,散在盘中的葡萄就是可以被垃圾回收的。
哪些对象可以作为根对象呢?使用eclipse的MAT(memory analyzer)可以进行分析。这个工具比jvisual更加专业,可以找到内存泄漏。
运行如下代码。
代码语言:javascript复制/**
* 演示GC Roots
*/
public class Demo2_2 {
public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read();
list1 = null;
System.out.println(2);
System.in.read();
System.out.println("end...");
}
}
使用命令jps
查看当前运行代码的进程为17332。
PS F:资料 解密JVM代码jvm> jps
13472 Launcher
3296 Jps
12868 RemoteMavenServer36
17332 Demo2_2
11180
在list回收前、后分别使用jamp
抓取目标进程内存的快照,转储为二进制文件,并设置live
参数在抓取快照前主动触发垃圾回收。回收前的操作命令如下。
jmap -dump:format=b,live,file=gcRootDemo.bin 17332
使用elipse的MAT工具可以来分析内存泄漏问题,官网下载安装MAT,https://www.eclipse.org/mat/downloads.php。
使用MAT工具,菜单栏file->open dump file打开刚才抓取的快照文件。打开文件时报错Invalid HPROF file header
,其中一个原因为人工改变了文件的编码格式,重新抓取并不要改变编码格式。将两个文件都打开以方便对照。如下图,查看文件的GC Roots。
可以看到GC Root的具体情况,被分为了4类。
System Class是程序运行所必须的核心类。
第二类是执行本地方法时操作系统所引用的Java对象的类。
Busy Monitor是指正在加锁的对象,如果这对象被回收了,则锁无法被释放,故不会被回收。
最后是活动着的线程中,局部变量所引用的对象不能被当成垃圾回收。比如下图中的ArrayList其实就是对应代码中list1被垃圾回收前所指对象,在list对象回收后的抓取的内存快照gcRootDemo2.bin中该对象不存在了。
6.2 五种引用
java中有五种引用类型。
6.2.1 强引用
只要沿着GC Root可以找到该对象,则不会被垃圾回收。如图中A1对象,只有当B,C对象对A1的引用都断开时,才会被垃圾回收。
6.2.2 软引用
当发生垃圾回收且内存不够时,则会对其进行进行回收。如图中A2对象,当B对象的引用断开,那么进行垃圾回收且内存不够时,A2对象将会被回收。
6.2.3 弱引用
当发生垃圾回收时,就会对其进行回收。如图中A3对象,当B对象的引用断开,那么进行垃圾回收,A3对象将会被回收。
特别的,软引用和弱引用本身也属于对象,可以配合引用队列进行使用。
6.2.4 虚引用
虚引用与终结器引用必须配合引用对象进行使用。如前文中提到的ByteBuffer对象,会创建一个虚引用Cleaner,并且会将ByteBufer所分配的直接内存传递给Cleaner引用。当ByteBufferber对象被垃圾回收后,Cleaner会进入引用队列。ReferenceHandler会定期扫描引用队列中新入队的对象,当Cleaner被扫描到就会执行其clean()方法,调用Unsafe对象的freememory()将直接内存释放。
6.2.5 终结器引用
所有对象都继承自Object,而Object中有一个finalize()方法,对象可以重写finalize()方法,在对象进行垃圾回收时该方法将被调用。但是对象已经没有强引用了,finalize()方法怎么被调用呢?其实就是通过终结器引用实现的。在B对象断开A4的强引用后,终结器引用会被加入引用队列,由一个优先级很低的finalizeHandler进行扫描,当扫描到引用队列中的终结器引用后,会执行其所引用的A4对象的finalize()方法。由于finalize()方法不会被立刻执行,而是先进行入队,并且负责扫描的finalizeHandler优先级低,可能导致finalize()迟迟得不到执行,因此不推荐使用它进行资源回收.
6.3 软引用应用
配置运行下列代码,显然会报OutOfMemoryError。
代码语言:javascript复制/**
* 演示软引用
* -Xmx20m
*/
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]);
}
}
}
而以上常见在实际编程中其实是常见的,比如在读取图片内容时。软引用可以解决这种内存占用的问题。
代码语言:javascript复制 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());
}
}
以上代码中,list和SoftReference是强引用,但是SoftReference和byte[]是软引用。打印结果如下。
代码语言:javascript复制[B@7f31245a
1
[B@6d6f6e28
2
[B@135fbaa4
3
[B@45ee12a7
4
[B@330bedb4
5
循环结束:5
null
null
null
null
[B@330bedb4
观察到在循环结束前可以调用到bye[]数组,但是循环结束前四个数组已经变成了null。
添加虚拟机参数:-XX: PrintGCDetails
-verbose:gc
,打印垃圾回收的细节与详细参数,然后再次运行查看垃圾回收完整过程。
[B@7f31245a
1
[B@6d6f6e28
2
[B@135fbaa4
3
[GC (Allocation Failure) [PSYoungGen: 2209K->488K(6144K)] 14497K->13130K(19968K), 0.0024761 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@45ee12a7
4
[GC (Allocation Failure) --[PSYoungGen: 4696K->4696K(6144K)] 17338K->17502K(19968K), 0.0020330 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 4696K->4466K(6144K)] [ParOldGen: 12806K->12675K(13824K)] 17502K->17142K(19968K), [Metaspace: 3331K->3331K(1056768K)], 0.0100720 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 4466K->4466K(6144K)] 17142K->17158K(19968K), 0.0024079 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4466K->0K(6144K)] [ParOldGen: 12691K->740K(8704K)] 17158K->740K(14848K), [Metaspace: 3331K->3331K(1056768K)], 0.0118198 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@330bedb4
5
循环结束:5
null
null
null
null
[B@330bedb4
Heap
PSYoungGen total 6144K, used 4377K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 77% used [0x00000000ff980000,0x00000000ffdc64f0,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 8704K, used 740K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
object space 8704K, 8% used [0x00000000fec00000,0x00000000fecb90f0,0x00000000ff480000)
Metaspace used 3352K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
观察到在第三个循环结束后,内存已经快不足,进行了新生代回收。在第四个循环结束后,又进行了一次新生代的回收,但是效果不理想(4696K->4696K),于是触发了一次Full GC。由于进行垃圾回收且内存仍然不足,又触发了一次新的垃圾回收,将软引用所引用的对象释放。像缓存的图片等不重要的对象,可以通过软引用来引用,当内存空间不足时就会回收它们。
同时我们也注意到,前四个软引用所指的对象已经是null了,没有必要再把这四个软引用保留在list集合中。可以配合引用队列来完成软引用的回收。
代码语言:javascript复制 public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
for (int i = 0; i < 5; i ) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], referenceQueue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
Reference<? extends byte[]> poll = referenceQueue.poll();
while (poll != null) {
list.remove(poll);
poll = referenceQueue.poll();
}
System.out.println("循环结束:" list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
结果如下。
代码语言:javascript复制[B@7f31245a
1
[B@6d6f6e28
2
[B@135fbaa4
3
[B@45ee12a7
4
[B@330bedb4
5
循环结束:1
[B@330bedb4
Process finished with exit code 0
6.4 弱引用
与软引用十分类似。
代码语言:javascript复制/**
* 演示弱引用
* -Xmx20m -XX: PrintGCDetails
*/
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());
}
}
打印的结果如下。其中第10 次循环时,由于弱引用本身也占有一定的内存,触发Full GC。
代码语言:javascript复制[B@7f31245a
[B@7f31245a [B@6d6f6e28
[B@7f31245a [B@6d6f6e28 [B@135fbaa4
[GC (Allocation Failure) [PSYoungGen: 2209K->504K(6144K)] 14497K->13139K(19968K), 0.0023913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 [B@45ee12a7
[GC (Allocation Failure) [PSYoungGen: 4712K->496K(6144K)] 17347K->13326K(19968K), 0.0025155 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null [B@330bedb4
[GC (Allocation Failure) [PSYoungGen: 4704K->504K(6144K)] 17534K->13350K(19968K), 0.0020861 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null [B@2503dbd3
[GC (Allocation Failure) [PSYoungGen: 4711K->504K(6144K)] 17557K->13382K(19968K), 0.0017767 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null [B@4b67cf4d
[GC (Allocation Failure) [PSYoungGen: 4710K->456K(6144K)] 17588K->13334K(19968K), 0.0017985 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null null [B@7ea987ac
[GC (Allocation Failure) [PSYoungGen: 4775K->504K(5120K)] 17653K->13398K(18944K), 0.0011480 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null null null [B@12a3a380
[GC (Allocation Failure) [PSYoungGen: 4735K->256K(5632K)] 17629K->13531K(19456K), 0.0016537 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 256K->0K(5632K)] [ParOldGen: 13275K->782K(8192K)] 13531K->782K(13824K), [Metaspace: 3345K->3345K(1056768K)], 0.0108212 secs] [Times: user=0.17 sys=0.00, real=0.01 secs]
null null null null null null null null null [B@29453f44
循环结束:10
当然,弱引用与软引用的区别是,只要触发垃圾回收,无论内存是否充足都会回收其引用对象。
代码语言:javascript复制public class WeakReferenceDemo {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>( new String( "hello" ));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
输出结果。
代码语言:javascript复制hello
null
6.5 回收算法
6.5.1 标记清除算法
先标记(将不可被GC Root直接或者间接访问的内存标记),再清除(并不是做清零操作,而是被空闲的内存起始地址放入空闲内存表,下次分配内存时就可以使用)。这种方式的优点是速度快,缺点是容易产生内存碎片,比如存储一个数组对象,总的内存空间足够,但是内存不连续,依然会导致内存溢出问题。
6.5.2 标记整理算法
先标记,再整理(移动对象)。优点是内存连续,缺点是消耗一定的时间,(对象移动,同时对象的地址发生变化,如果对象有引用,那么引用中保存的地址也需要随之发生改变。)
6.5.3 复制算法
先标记,后复制。将对象从from移动到to区域,在移动过程中就完成了内存整理工作。同时交换from和to区。优点是空间连续,缺点是需要使用双倍的内存空间。
6.6 分代回收机制
JVM同时综合使用了三种垃圾回收算法。这就是分代回收机制。
内存空间可以分为新生代和老年代,新生代又可以分为伊甸园和幸存者from,幸存者to。之所以采用分代回收机制,是为了使不同的垃圾回收策略。新生代用于存放朝生夕死的对象,会频繁的进行垃圾清理。
一个对象被创建后,首先会放入新生代的伊甸园中。
当新生区内存无法放入新的对象时,会触发一次Minor GC,将根据根可达算法判断伊甸园和幸存区From中哪些对象可以被回收,对于没有被垃圾回收的对象,根据复制算法将其复制到幸存区to中,交换幸存区From和幸存区To,并将未被回收对象寿命增1。Minor GC会引发STW(Stop the world),即进行垃圾回收时其他用户线程会被暂停。之所以要触发STW是因为垃圾回收的过程中会改变对象的地址,如果不暂停其他线程,当其他线程找不到对象会发生混乱。因为大部分对象都会被垃圾回收,需要通过复制算法改变内存地址的对象并不多,Minor GC的SWT较短。
当幸存区中的对象寿命到了阈值(最大为15{4bit}),说明这些对象的生命周期较长,这些对象将会被移到老年代中,当内存资源较为紧张,新生代存放不下更多对象,也可能将对象移到老年代中。老年代的垃圾回收频率较低。
如果堆中新生代快满了,放不进新的对象,同时老年代也快满了,会先尝试触发Minor GC,空间仍然不足就会触发Full GC。对整个堆进行垃圾回收。因为Full GC时老年代的回收算法耗时,同时要回收的对象数量较多,Full GC的SWT时间较长。如果Full GC后内存仍然不足就会触发Out of Memory。
6.7 GC分析
下面我们通过实例对GC的过程分析。开始GC分析之前,先了解一些GC常用的一些参数。其中上表中的晋升是指新生代晋升到老年代。
参考下面代码,设置参数并运行。其中参数-XX: UserSerialGC
是将垃圾回收器设置为UserSerialGC,这种垃圾回收器的幸存区不会进行自动调整,有助于我们观察现象。
/**
* 演示内存的分配策略
*/
public class Demo2_1 {
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;
// -Xms20M -Xmx20M -Xmn10M -XX: UseSerialGC -XX: PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
}
}
打印信息如下。
代码语言:javascript复制Heap
def new generation total 9216K, used 2311K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee41d50, 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 3268K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 347K, capacity 388K, committed 512K, reserved 1048576K
观察到我们分配的新生代内存是10M,但是打印的只有9M,这是因为伊甸园占用8M,幸存区From和To各占用1M,JVM认为幸存区中的内存始终有一块空间是需要空着的,不能存放内容,所以这部分空间没有被计算进来。
新生代的伊甸园只有8M内存,其中28%还已经被占用了,新增以下代码。
代码语言:javascript复制ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
果然触发了Minor GC。垃圾回收前新生代占用2147k,垃圾回收后占用749k,新生代总大小9216K。堆空间回收前占用2147K,垃圾回收后占用749K,总大小19456K。由于数组被放入了list集合中,而list集合被根GC Root所访问,不会被垃圾回收,所以byte[]数组被移到了幸存区中。垃圾回收后放入了7M的对象。伊甸园占用率93%。
代码语言:javascript复制[GC (Allocation Failure) [DefNew: 2147K->749K(9216K), 0.0128891 secs] 2147K->749K(19456K), 0.0129487 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
Heap
def new generation total 9216K, used 8327K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 92% used [0x00000000fec00000, 0x00000000ff366830, 0x00000000ff400000)
from space 1024K, 73% used [0x00000000ff500000, 0x00000000ff5bb4d8, 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)
再新增以下代码,创建一个1M大小的数组。
代码语言:javascript复制list.add(new byte[_1MB]);
打印信息如下。触发了两次GC操作,在第二次GC操作时,幸存区已经无法容纳这个1M的byte[]对象了,因此部分对象从幸存区晋升到了老年代中。
代码语言:javascript复制[GC (Allocation Failure) [DefNew: 2147K->748K(9216K), 0.0039741 secs] 2147K->748K(19456K), 0.0040840 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 8244K->26K(9216K), 0.0096121 secs] 8244K->7932K(19456K), 0.0096617 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 1216K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 14% used [0x00000000fec00000, 0x00000000fed29758, 0x00000000ff400000)
from space 1024K, 2% used [0x00000000ff400000, 0x00000000ff406bb8, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7905K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 77% used [0x00000000ff600000, 0x00000000ffdb8508, 0x00000000ffdb8600, 0x0000000100000000)
Metaspace used 3314K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
下面介绍一种大对象直接晋升老年代的情况。将之前的代码注释,直接在list集合中添加8M的byte[]数组。
代码语言:javascript复制list.add(new byte[_8MB]);
这种情况伊甸园肯定放不下这个数组,幸存区也放不下,JVM经过计算,发现即使触发了垃圾回收也无法在新生代存放这个对象,这种情况不会触发垃圾回收,如果老年代空间足够这个大对象就会直接晋升老年代。
代码语言:javascript复制Heap
def new generation total 9216K, used 2478K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 30% used [0x00000000fec00000, 0x00000000fee6bbe8, 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 3333K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
如果新生代,老年代都不足以存放了,就会Out of Memory。
思考一个问题。如果一个非主线程的其他线程发生内存溢出,会导致整个java进程退出吗?实验下。
代码语言:javascript复制 public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
System.out.println("I'm alive,Haha");
}
结果如下。一个非主线程的其他线程发生内存溢出,不会导致整个java进程退出。
代码语言:javascript复制sleep....
[GC (Allocation Failure) [DefNew: 4796K->990K(9216K), 0.0038712 secs][Tenured: 8192K->9179K(10240K), 0.0052058 secs] 12988K->9179K(19456K), [Metaspace: 4269K->4269K(1056768K)], 0.0094779 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 9179K->9124K(10240K), 0.0038569 secs] 9179K->9124K(19456K), [Metaspace: 4269K->4269K(1056768K)], 0.0039093 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at cn.itcast.jvm.t2.Demo2_1.lambda$main$0(Demo2_1.java:20)
at cn.itcast.jvm.t2.Demo2_1$$Lambda$1/1023892928.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
I'm alive,Haha
Heap
def new generation total 9216K, used 349K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 4% used [0x00000000fec00000, 0x00000000fec57530, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 9124K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 89% used [0x00000000ff600000, 0x00000000ffee9060, 0x00000000ffee9200, 0x0000000100000000)
Metaspace used 4294K, capacity 4708K, committed 4992K, reserved 1056768K
class space used 467K, capacity 528K, committed 640K, reserved 1048576K
6.8 垃圾回收器
6.8.1 垃圾回收器分类
有三类垃圾回收器。
1、串行垃圾回收器
- 单线程
- 适合堆内存较小场景,适合个人电脑。
2、吞吐量优先
- 多线程
- 适合堆内存较大场景,需要多核CPU支持
- 让单位时间内,SWT时间最短
3、响应时间优先
- 多线程
- 适合堆内存较大场景,需要多核CPU支持
- 尽可能使单次响应SWT时间最少
注:吞吐量优先追求的是单位时间的STW时间最短,响应时间优先是追求每次响应的速度最快。举例如下,算法1:0.2s/次 * 2次 =0.4s,算法2:0.1s/次 * 5次 =0.5s,算法1进行垃圾回收的总时间最短,吞吐量更大。算法2的单次垃圾回收时间更短,响应速度更快。
6.8.2 串行垃圾回收器
使用-XX: UseSerialGC = Serial SerialOld
可以开启串行垃圾回收器。其中新生代采用的算法是复制算法,老年代采用的是标记整理算法。在垃圾回收线程运行前,会先阻塞其他线程。
6.8.3 吞吐量优先
开启-XX: UseParrallelGC
或者-XX: UseParrallelGC
(开启一个另外一个会自动开启)使用吞吐量优先的垃圾回收器,其新生代算法仍为复制算法,老年代算法仍为标记整理算法。不过其特别之处在于:在垃圾回收前,用户线程会暂停,但是垃圾回收时会开启多个线程同时执行垃圾回收操作,开启的线程数量与cpu核数相同。当然,我们也可以使用-XX:ParallelGCThreads=n
来指定进行垃圾回收的线程数量。参数-XX: UseAdaptiveSizePolicy
可以使用自适应的策略来调整堆的大小,这里主要是新生代空间的调整。XX:GCTimeRatio=n
用于设置除垃圾回收时间外的时间占比,假设-XX:GCTimeRatio=19
,则垃圾收集时间为1/(1 19),默认值为99,即1%时间用于垃圾收集。-XX:ParrallelGCMills=ms
用于调整每一次垃圾回收的暂停时间。但是XX:GCTimeRatio=n
和
-XX:ParrallelGCMills=ms
这两个参数其实是有冲突的。当GCTimeRatio设置的更大,就要调整堆使堆更大,以增加吞吐量,而堆更大则每次垃圾回收的暂停时间就会更长。两者要进行合理取舍。(注:JVM的堆大小有起始值和最大值,堆在这个范围内进行大小调节)
6.8.4 响应时间优先
ConcMarkSweepGC
是工作在老年代的垃圾回收器。望文生义,响应时间优先垃圾回收器采取的垃圾回收策略是标记清除法(快,无需内存移动)。其中Con是concurrent的缩写,这表示响应时间优先的垃圾回收器在某些阶段采用的是并发策略(在某些阶段仍需STW):垃圾回收线程和其他用户线程并行执行,这样显然有利于提高程序的响应性能,但是也会牺牲吞吐量,与CMS垃圾回收器配合的垃圾回收器为ParNewGC
。不过,CMS垃圾回收器有时会并发失败,这时会采取补救措施,将CMS退化为SerialOld。
在老年代快满时,将会阻塞其他线程,然后由垃圾回收线程对于GC Root进行快速的标记,由于只标记GC Root,这个过程很短。然后其他线程就可以恢复执行了,同时垃圾回收与其他用户线程并发执行,垃圾回收并发标记除根对象外的其他要被回收的对象。在并发标记结束后再次STW,然后重新标记,防止由于用户线程的活动导致对象的地址发生变化。重新标记结束后用户线程又可以执行了,垃圾回收线程进行并发清理。
可以设置并行线程数和并发线程数,并行线程数一般与cpu的核数相同,一般建议将并发线程数设置为并发线程数的1/4,即垃圾回收线程与用户线程按照1:3来抢占cpu,并发执行。
在进行并发清理的过程中,不能把这个过程新产生的垃圾清理掉,这些垃圾需要下一次垃圾回收时进行清理,称为浮动垃圾。因为清理是并发的,可能还没有清理出足够的空间存放这部分浮动垃圾,因此不能够像其它垃圾回收器一样,等到堆内存不足了再进行垃圾回收,必须为他们预留空间,参数-XX:CMSInitiatingOccupancyFraction=percent
可以用来设置执行垃圾回收的时机:当内存的占比达到设置值就执行垃圾回收。
有可能新生代的对象引用老年代的对象,在进行重新标记时,要对整个堆的对象进行扫描,包括新生代的对象,然后根据这个新生代的对象扫描整个老年区的对象,做可达性的分析。这样无疑会消耗时间。使用参数-XX:CMSScanvengeBeforeRemark
会先使用ParNewGC对新生代进行扫描,将其中可以回收的对象进行回收。
标记清除可能会导致垃圾碎片过多,导致并发失败,CMS会退化为SerialOld,进行一次全面的垃圾整理。这无疑会造成很多的时间消耗,这也时CMS存在的一个问题。
6.8.5 G1垃圾回收器
JDK9的默认垃圾回收器,取代了之前的CMS垃圾回收器。
(1)垃圾回收阶段
首先是进行Young Collection,当老年代到达阈值时,进行Young Collection ConcurrentMark,最后进行Mixed Colletion。
(2)Young Collection
新创建的对象会被放入伊甸园。
当伊甸园满后(会分配总的大小)会进行Young Collection。通过复制算法将未被回收的对象移至幸存区。
当再次触发垃圾回收时,会将一部分没有被回收的幸存区对象移到老年代(达到年龄阈值),另一部分没有被回收的幸存区对象移到其他幸存区(未达到年龄阈值)。
(2)Young GC CM(Concurrent marking)
- 在Young GC(阶段1)的同时进行GC Root初始标记
- 在老年代内存占用达到阈值时,会触发并发标记(无SWT)
(3)混合收集
混合收集阶段会全面收集垃圾,但是值得注意的是可以通过参数设置最大暂停时间,为了达到最大暂停时间的设置目标,老年代的内存可能不会全部进行拷贝整理,而是优先整理垃圾最多的内存。这也称其为G1的原因(Garbage First).
(4)Full GC
对于SerialGC和ParrallelGC而言,当老年代内存不足发生的垃圾回收就是full gc,但是对于CMS和G1垃圾回收器,老年代进行回收时是并发操作的,并不会造成太长的SWT,并不是full gc,G1、CMS只有当垃圾回收的速度比垃圾产生的速度要慢时,导致老年也满了,退化为SerialGC,才会触发full gc。
(5) Young Collection跨代使用
在进行young collection时,我们要查找GC Root,有一部分根对象可能在老年代中存活,新生代的对象被老年代引用了,如果我们对于整个老年代的对象进行扫描,效率肯定很低。G1垃圾回收器采取卡牌策略,把老年代的块状空间进一步划分为卡牌,当某个卡牌中的对象引用了新生代中的对象,就被标记为脏卡。在新生代内存中使用RememberSet来记录所有的引用,在查找GC Root时就可以通过RememberSet直接关注到脏卡区域,无需扫描全部老年代内存空间。当引用关系发生变化时,post-writter barrier和dirty card queue会配合更新变化的引用关系,最后通过异步线程Concurent reinforce Threads更新RememberSet。
(6) Remark
通过post-writter barrier satb_mark_queue实现。具体过程如下。
在remark阶段,我们采用黑色表示已经完成mark处理的对象,用灰色表示正在进行remark操作的对象,用白色表示还未进行remark操作的对象。如下图,A已经完成mark,并且其被强引用,故不会被垃圾回收,B正在remark,C尚未开始mark。
如果B完成Remark,在对C进行mark时恰B与C的引用断开(标记阶段垃圾回收线程与用户线程是并发的),那么C就会被标记成为白色。如果在后面A又引用了C,那么垃圾回收时,C对象会被回收吗?答案当然是不会。这是因为当C对象被引用时,会执行post-writter barrier,并将C对象放入satb_mark_queue,置为灰色。在进行remark时,会对satb_mark_queue中的对象进行扫描,如扫描到被引用,则会将其置为黑色。
(7)字符串去重
在jdk8中,string字符串是存放在char数组中,如果通过new String的方式创建可能会导致重复创建。除了通过intern()方法来避免重复创建的发生,G1垃圾回收器会在新生代回收时并发检查是否存在重复创建的字符串,如果有则让他们指向同一个char数组。使用-XX: UseStringDeduplication
可以开启字符串去重(默认打开)。
(8)类卸载
在jdk8u40后,所有对象在经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器所有的类都不再被使用后(主要是框架、自定义类加载器),就会尝试对这些类进行类卸载。使用-XX: ClassUnloadConcurrentWithMark
可以开启类卸载(默认开启)。
(9)巨型对象
巨型对象指大小大于region的一半的对象,在jdk8u60后,可以回收巨型对象。
巨型对象有如下特别之处。
- G1不会复制巨型对象
- 垃圾回收时优先回收
- 当某个巨型对象被老年代的incoming引用为0时,将会在新生代垃圾回收时被回收(参考下图)。
(10)动态调整阈值
并发标记必须在堆占满前完成,否则将退化为full gc(注:在新版本的jvm中,full gc已经不是前文所提到的单线程,但是仍然有很长的STW时间,需要避免)。在jdk9之前我们采用-XX:InitiatingHeapOccupancyPercent
来设置开始并发标记的阈值,但是阈值如果设置过低则频繁GC,如果设置过高则易Full GC。jdk9中可以对于阈值进行动态调整。
jdk中还有很多对于垃圾回收器的改进。建议多读官网文档
6.9 GC调优
6.9.1 预备知识
(1) 常用命令
jvm调优需要对于一些常用的内存设置参数熟悉,可以查阅官网。或使用命令java -XX: PrintFlagsFinal -version | findstr "GC"
查看jvm中与GC相关的虚拟机参数。
查看的结果示例如下。
代码语言:javascript复制java version "16.0.2" 2021-07-20
Java(TM) SE Runtime Environment (build 16.0.2 7-67)
Java HotSpot(TM) 64-Bit Server VM (build 16.0.2 7-67, mixed mode, sharing)
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
uint ConcGCThreads = 3 {product} {ergonomic}
bool DisableExplicitGC = false {product} {default}
bool ExplicitGCInvokesConcurrent = false {product} {default}
uintx G1MixedGCCountTarget = 8 {product} {default}
uintx G1PeriodicGCInterval = 0 {manageable} {default}
bool G1PeriodicGCInvokesConcurrent = true {product} {default}
double G1PeriodicGCSystemLoadThreshold = 0.000000 {manageable} {default}
uintx GCDrainStackTargetSize = 64 {product} {ergonomic}
uintx GCHeapFreeLimit = 2 {product} {default}
uintx GCLockerEdenExpansionPercent = 5 {product} {default}
uintx GCPauseIntervalMillis = 201 {product} {default}
uintx GCTimeLimit = 98 {product} {default}
uintx GCTimeRatio = 12 {product} {default}
bool HeapDumpAfterFullGC = false {manageable} {default}
bool HeapDumpBeforeFullGC = false {manageable} {default}
size_t HeapSizePerGCThread = 43620760 {product} {default}
uintx MaxGCMinorPauseMillis = 18446744073709551615 {product} {default}
uintx MaxGCPauseMillis = 200 {product} {default}
int ParGCArrayScanChunk = 50 {product} {default}
uintx ParallelGCBufferWastePct = 10 {product} {default}
uint ParallelGCThreads = 13 {product} {default}
bool PrintGC = false {product} {default}
bool PrintGCDetails = false {product} {default}
bool ScavengeBeforeFullGC = false {product} {default}
bool UseAdaptiveSizeDecayMajorGCCost = true {product} {default}
bool UseAdaptiveSizePolicyWithSystemGC = false {product} {default}
bool UseDynamicNumberOfGCThreads = true {product} {default}
bool UseG1GC = true {product} {ergonomic}
bool UseGCOverheadLimit = true {product} {default}
bool UseMaximumCompactionOnSystemGC = true {product} {default}
bool UseParallelGC = false {product} {default}
bool UseSerialGC = false {product} {default}
bool UseShenandoahGC = false {product} {default}
bool UseZGC = false {product} {default}
(2) 掌握常用工具
(3) 调优与代码、平台相关,无万能范式。
6.9.2 调优内容
(1)调优领域
- 内存
- 锁竞争
- cpu占用
- io
(2) 调优目标
- 高吞吐量(科学运算):ParrellelGC
- 还是低延迟(互联网项目):CMS、G1、ZGC
- hotspot外的虚拟机:zing…
6.9.3 代码复核
查看full gc前后的内存占用,考虑以下几个问题。
- 数据量是不是太多?下面代码就会加载大量数据到堆内存中。
resultSet = statement.executeQuery("select * from xxx");
应该改为:
代码语言:javascript复制resultSet = statement.executeQuery("select * from xxx limit n");
- 数据表示太臃肿?
- 对象用到什么数据项查什么数据项
- 对象大小 Integer24byte ,int24byte
- 是否存在内存泄漏?如:
static Map map = new HashMap();
对长期存活的对象建议使用弱引用、软引用。
对于缓存类型的数据建议使用第三方缓存实现,如redis。
6.9.4 新生区内存调优
(1) 新生代的特点
- 对象分配极其廉价:使用TLAB,即Thread Local allocation buffer(参考:浅析java中的TLAB - 简书 (jianshu.com)),避免了线程竞争,提高了内存分配的效率。
- 对象的销毁代价小:采用复制算法整理内存,对于垃圾对象销毁代价小。
- 大部分对象朝生夕死,minor gc时间远低于full gc
由于新生代具有以上特点,对于新生代进行内存调优效果更明显,往往进行内存调优时先考虑新生代的内存调优。
(2) 设置新生代内存大小
参数-Xmn
可以设置新生代大小。如果新生代设置过小,将会导致minor gc频繁发生,耗费stw时间。如果新生代设置过大,或将导致只发生Full GC,占用的时间同样会很高,官方推荐设置为25%-50%。根据经验,我们一般将新生代的内存空间设置为:所容纳的最大并发量 * 一次请求响应的数据量。这样一次请求响应完成后大部分的内存将可以被释放,可以有效的减少GC的触发次数。
6.9.5 幸存区调优
(1) 设置幸存区大小
幸存区要至少能够存放当前活跃(将被回收)的对象 即将晋升(不被回收)的对象,如果幸存区的对象容纳不下,当前活跃的对象可能会被晋升到老年代,这就使一个本来拥有较短生命周期的对象在Full GC时才会被垃圾回收。
(2)设置合理晋升阈值
通过参数``-XX: PrintTenuringDistribution可以打印各个年龄的对象在内存中的占用,
-XX: MaxTenuringThreshold=threshold`调整晋升阈值。如果幸存区的晋升阈值设置过大,则需要晋升到老年代的对象可能不会被及时晋升。而新生代进行Minor GC时耗费时间主要发生在复制对象上,这就会导致STW时间变长。
6.9.6 老年代调优
以CMS为例。
CMS的老年代的内存要尽可能大,避免浮动垃圾又导致内存溢出,使老年代退化。一般先进行新生代调优,有必要再考虑老年代调优。如果没有发生Full GC,一般无需对老年代进行调优,如果发生了Full GC,可以观察发生Full GC时老年代的内存占用超过的阈值,将老年代内存大小调大1/41/3.另外也可以用`-XX: CMSInitiatingOccupyFraction=percent`设置老年代垃圾回收的时机,一般推荐设置为内存占用75�%时。
6.9.7 调优案例
- Full GC和Minor GC特别频繁
考虑新生代内存设置过小,通过增加新生代内存大小,避免频繁触发Minor GC,以及将生命周期较短的对象带入老年代进而引发Full GC。
- 请求高峰期发生Full GC,单次占用时间特别长(CMS)
查看GC日志,查看到底时CMS各阶段耗费时间。CMS再重新标记时耗时最多,根据日志发现有1s。由于在重新标记阶段,不仅会扫描老年代的对象,还会扫描新生代的对象,并根据根可达算法进行扫描,考虑在业务高峰器,新生代对象较多。可以将CMS重新标记前先将新生代内存进行一次整理。
在这里插入图片描述
- 老年代充足情况发生Full GC(CMS,jdk1.7)
查看输出提示信息并无并发失败、晋升失败等,说明老年代空间充裕,确认jdk版本为1.7,非jdk1.8。在jdk1.7及以前,方法区由元空间来管理,考虑元空间不足导致Full GC。