默认情况下,调用 System.gc(),其实际是调用的 Runtime.getRuntime().gc()(其实一个 native 方法),会显式的触发 Full GC,同时对新生代和老年代进行回收。但其调用无法保证一定会进行 GC,JVM 会在适当的时候进行 GC。
调用 System.runFinalization(); 方法可以强制调用失去引用的对象的 finalize 方法
OOM,表示即便是 JVM GC 之后也没有空闲的内存可以使用了,就会导致 OOM。OOM 在新生代、老年代都会发生。
特殊的,当创建一个超大的对象时(比如占 200M 空间),但 JVM 设置的 堆空间(比如只有150M),那么堆空间是无法放下此对象的,直接报 OOM。
严格来说,只有对象不会再被程序使用,但又无法通过 GC 释放,就是内存泄漏。但实际使用中,由于各种疏忽,导致某些对象的生命周期超长,长时间无法释放,最后导致这种对象越来越多,最后 OOM,目前也称之为内存泄漏。
一般选择一下执行时间较长的指令来设置安全点,比如:方法调用、循环跳转和异常跳转等。
我们平时的代码中,几乎所有都是强引用。如:Object obj = new Object(); 。垃圾回收时,只要强引用关系存在,在 GC 时该对象不会被回收。
在系统将要发生内存溢出之前,将会把这些对象列如回收范围中,在第二次进行回收时(先回收不可达对象),不可达对象回收后内存任然不足时,进行回收。当第二次回收之后还没有足够的内存,才会抛出 OOM。
标记一个对象为软引用
Object obj = new Object();
Reference<Object> ref = new SoftReference<Object>(obj);
System.out.println(obj);
System.out.println(ref.get());
ref.clear();
// obj 是强引用,GC 的时候 Object 还是不会被回收,所以以上的写法要么不要定义 obj 引用,要么在后面将 obj = null,去掉强引用。
System.out.println(obj);
System.out.println(ref.get()); // null
修改如下:
Object obj = new Object();
Reference<Object> ref = new SoftReference<Object>(obj);
obj = null;
System.out.println(obj);// null
System.out.println(ref.get());// not null
System.gc();
// 等待 gc 完成
Thread.sleep(10000);
// obj 是强引用,GC 的时候 Object 还是不会被回收,所以以上的写法要么不要定义 obj 引用,要么在后面将 obj = null,去掉强引用。
System.out.println(obj);// null
System.out.println(ref.get()); // not null 回收不可达对象之后,内存还不足时才会回收
Reference 是 java.lang.ref 包下的抽象类,其下有SoftReference、WeakReference、PhantomReference 等实现类
当执行 GC 时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。软引用和弱引用都非常适合用来保存那些可有可无的缓存数据。
Object obj = new Object();
Reference<Object> ref = new WeakReference<>(obj);
obj = null;
System.out.println(obj);// null
System.out.println(ref.get());// not null
System.gc();
// 等待 gc 完成
Thread.sleep(10000);
System.out.println(obj);// null
System.out.println(ref.get()); // null 发现即回收
无法通过虚引用来获得一个对象的实例,为一个对象设置虚引用关联的唯一目的是在这个对象被垃圾收集时收到一个系统通知。虚引用对对象的生命周期没有任何影响。
// 虚引用,必须传入 ReferenceQueue 引用队列
Reference<Object> ref = new PhantomReference<>(new Object(),new ReferenceQueue<>());
System.out.println(ref.get()); // null 虚引用无法通过 get 方法获取
System.gc();
System.out.println(ref.get()); // null
引用强度:强引用 > 软引用 > 弱引用 > 虚引用
主要是以回收后,是否对内存碎片进行压缩整理(标记-压缩算法)
当前垃圾收集器最注重的2点:吞吐量 + 暂停时间。在最大吞吐量的前提下,尽量的降低暂停时间。
常用的回收器:
JDK 8 的垃圾收集文档中,(https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/),提及的两款垃圾收集器即为:CMS 和 G1
图片太大,可以使用新标签单独打开或者下载下来查看。
-XX:InitialHeapSize=264885120 -XX:MaxHeapSize=4238161920 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
我们最后会看到,-XX:+UseParallelGC ,说明 JDK 8 默认使用的垃圾收集器为:ParallelGC,其对应的老年代收集器为 ParallelOldGC
图中 13392 这个线程id就是我们当前的执行程序。
Serial 收集器是一个串行的收集器,一般在可用内存较小(几十 M 到一两百 M ),且垃圾收集可以在较短的时间内完成的情况下。使用参数 -XX:UseSerialGC 指定老年代和新生代分别使用 SerialOldGC 和 SerialGC。(此处注意:没有 -XX:UseSerialOldGC的参数)。目前此收集器很少使用。
ParNew 是并行回收器,其处理的是新生代的垃圾回收。其可以配合 CMS 和 Serial Old 来进行垃圾回收。使用参数 -XX:UseParNewGC 设置新生代使用 ParNew 收集器。使用 -XX:ParallelGCThreads 来限制线程的数量,默认数量为 CPU 的核数(建议不要超过 CPU 的核数)。目前此收集器也很少使用了。
Parallel 收集器和 ParNew 收集器同样采用了复制算法、并行回收等机制,但 Parallel 有吞吐量优先的特性, Parallel 还有自适应调节策略。
参数配置
尽可能的降低 STW 的时间(低延迟),采用并发式标记清除算法进行垃圾收集,只负责老年的垃圾收集,且不能与 Parallel Scavenge 配合使用,只能和 Serial 或 ParNew 配合使用 。JDK 9 中已不推荐使用,被标记为 Deprecate。
工作原理:初始标记、并发标记、重新标记、并发清理四个过程
由于并发标记和并发清除阶段是和用户线程一起并行的,所以 CMS 收集器可以达到低延迟的一个标准。
也是由于用户线程和垃圾收集线程并行,如果确实没有对内存进行执行,此时 JVM 会启用 Serial Old 对老年代进行垃圾收集。
也是由于用户线程和垃圾收集线程并行,所以说不能使用标记压缩算法的(压缩时会改变对象的地址,改变对象的地址用户线程就无法执行了)
CMS 收集器是使用标记清除算法,会产生内存碎片,也只能使用空闲列表的方式进行再分配。
本文中提到了很多的参数,这些参数的设置官方文档就是 java 命令的文档 (https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html)
区域化分代式垃圾收集器,G1 是并行收集器,它把内存分割为很多不想关的区域(Region),每个Region可能是 Eden区,幸存者1区或幸存者0区,老年代等,这样可以避免在整堆中进行垃圾收集,将整个堆化整为零,一次回收其中一个或一些 Region,G1 进行垃圾回收时,会优先收集价值比较大的Region(可以认为当前不可达对象比较多的 Region 为价值比较大)。其在尽可能的保证低延迟的情况下提高吞吐量,其即可以回收新生代又可以回收老年代。
G1 在 JDK 7 加入,JDK 9 被设定为默认的垃圾回收器。
垃圾收集的相关知识可参考 JDK 8 的官方文档 https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/
在通常情况下,我们可以直接提高堆内存大小来提高性能,但在无法提高堆内存大小的情况下,再来手动调整垃圾收集器
堆内存如果足够大(8G或以上,官网说明中是 10G),推荐使用 G1 收集器。
2023-10-09T16:26:42.467+0800: [GC (Allocation Failure) 6743K->1008K(29696K), 0.0009465 secs]
2023-10-09T16:26:43.108+0800: [GC (Allocation Failure) 8390K->1144K(29696K), 0.0009188 secs]
2023-10-09T16:26:43.752+0800: [GC (Allocation Failure) 7585K->1184K(29696K), 0.0021388 secs]
2023-10-09T16:26:44.382+0800: [GC (Allocation Failure) 7469K->1232K(29696K), 0.0011422 secs]
2023-10-09T16:26:45.012+0800: [GC (Allocation Failure) 7524K->1248K(29696K), 0.0010169 secs]
2023-10-09T16:26:45.655+0800: [GC (Allocation Failure) 7544K->1264K(28672K), 0.0007992 secs]
2023-10-09T16:26:46.284+0800: [GC (Allocation Failure) 7543K->1259K(29184K), 0.0012118 secs]
2023-10-09T16:26:46.927+0800: [GC (Allocation Failure) 7540K->1259K(29184K), 0.0007576 secs]
2023-10-09T16:26:47.571+0800: [GC (Allocation Failure) 7541K->1259K(28160K), 0.0005698 secs]
2023-10-09T16:26:48.215+0800: [GC (Allocation Failure) 7542K->1259K(28672K), 0.0011637 secs]
2023-10-09T16:26:48.629+0800: [GC (Allocation Failure) 5474K->1259K(28672K), 0.0003956 secs]
2023-10-09T16:26:49.044+0800: [GC (Allocation Failure) 5474K->1259K(28672K), 0.0007266 secs]
2023-10-09T16:26:49.472+0800: [GC (Allocation Failure) 5474K->1259K(28672K), 0.0005000 secs]
说明(注:此时我们设置的堆内存大小为 30M):
- 前面的时间是 -XX:+PrintGCDateStamps 参数的作用。
- GC (Allocation Failure):表示 GC 发生的原因,Allocation Failure 表示内存分配失败
6743K->1008K(29696K):表示 GC 之前内存占用 6743K,GC 之后内存占用 1008K(GC 之后空余内存 29696K)
0.0009465 secs :表示 GC 时间
2023-10-09T16:40:37.840+0800: [GC (Allocation Failure) [PSYoungGen: 6743K->1018K(9216K)] 6743K->1026K(29696K), 0.0012821 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2023-10-09T16:40:38.481+0800: [GC (Allocation Failure) [PSYoungGen: 8400K->1016K(9216K)] 8408K->1152K(29696K), 0.0009312 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2023-10-09T16:40:39.119+0800: [GC (Allocation Failure) [PSYoungGen: 7457K->1016K(9216K)] 7593K->1184K(29696K), 0.0007436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2023-10-09T16:40:39.763+0800: [GC (Allocation Failure) [PSYoungGen: 7301K->1016K(9216K)] 7469K->1240K(29696K), 0.0009514 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2023-10-09T16:40:40.406+0800: [GC (Allocation Failure) [PSYoungGen: 7308K->1016K(9216K)] 7532K->1256K(29696K), 0.0012255 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2023-10-09T16:40:41.045+0800: [GC (Allocation Failure) [PSYoungGen: 7312K->1000K(8192K)] 7552K->1264K(28672K), 0.0010059 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- PSYoungGen 当前 GC 的类型,当前我们是默认的 Parallel 收集器,所以是 PSYoungGen,不同的收集器显示不一样。
- [PSYoungGen: 6743K->1018K(9216K)]:中括号内,年轻代垃圾回收之前空间占用 6743K,回收后空间占用 1018K(回收后空闲 9216K)
- 中括号外:6743K->1026K(29696K),年轻代 + 老年代的空间占用情况。
[Times: user=0.00 sys=0.00, real=0.00 secs]:user表示用户态回收耗时,real 表示实际耗时,sys表示内核态回收耗时。
Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total
...
age 为 1 的对象是前一次 GC 幸存下来的对象,大小为 28992024 bytes,age 为 2 的对象是在前两次 GC 中幸存下来的对象,大小为 1366864 bytes,依此内推,每一行最后的 total 表示小于等于当前年龄的对象总大小。
示例中,28 992 024个字节在一次清除中幸存下来,并从eden复制到surviver空间,1 366 864个字节被年龄2的对象占用,等等。
示例:-XX:HeapDumpPath=./java_pid%p.hprof , %p 表示进程id