JVM(十四)垃圾回收的一些额外点
JVM(十四)垃圾回收的一些额外点
1 System.gc()的理解
- 在默认情况下,通过
System.gc()
或者Runtime.getRuntime().gc()
的调用,会显式触发Full GC
,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存 System.gc()
调用附带一个免责声明,无法保证对垃圾收集器的调用- JVM可以通过
System.gc()
的调用来决定JVM的GC行为,但是一般情况下垃圾回收都是自动进行的,无需手动触发
如下代码,控制器有时会打印有时则不会打印相关内容,但是打开System.runFinalization()
的注释之后,每次就一定会打印该内容。
public class SystemGCTest { public static void main(String[] args) { new SystemGCTest(); System.gc(); // System.runFinalization(); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("调用了finalize()"); } }
- 调用
System.runFinalization()
实际上等效于调用Runtime.getRuntime().runFinalization()
- 该方法会强制调用已经失去引用的对象的finalize方法
手动gc理解不可达对象的回收行为
public class LocalVarGC { // youngGC不会回收,最终放入老年代 public void localVarGC1() { byte[] buffer = new byte[10 * 1024 * 1024]; System.gc(); } // youngGC就会回收 public void localVarGC2() { byte[] buffer = new byte[10 * 1024 * 1024]; buffer = null; System.gc(); } // youngGC不会回收,最终放入老年代 public void localVarGC3() { { byte[] buffer = new byte[10 * 1024 * 1024]; } System.gc(); } // youngGC就会回收 public void localVarGC4() { { byte[] buffer = new byte[10 * 1024 * 1024]; } int value = 10; System.gc(); } // localVarGC1()的youngGC不会回收,最终放入老年代,随后第二次YGC则会回收 public void localVarGC5() { localVarGC1(); System.gc(); } public static void main(String[] args) { LocalVarGC localVarGC = new LocalVarGC(); localVarGC.localVarGC5(); } }
对于localVarGC4()和localVarGC5(),首先localVarGC4()会被回收而localVarGC3()不会,可以通过jclasslib看一下两个方法栈帧的局部变量表:



可以看到两个局部变量表的大小都是2(localVarGC3()的第一个局部变量由于在代码块里面,所以超出作用域不显示在局部变量表里面,但是并没有删除这个局部变量,而是在出现新的局部变量的时候覆盖掉这个slot插槽),这也是localVarGC4()的value显示在位置1的原因,因此根据可达性分析localVarGC3()的引用变量指向的堆中的对象不会被回收,而localVarGC4()会。
localVarGC5()就很好理解了,localVarGC1()的调用结束,意味着整个localVarGC1()栈帧出栈,其中的局部变量表将从GC Roots中移除,根据可达性分析算法,自然会回收掉堆中相应的对象。
2 内存溢出与内存泄露
2.1 内存溢出
-
内存溢出(Out Of Memory)指的是没有空闲的内存空间,并且垃圾回收器也无法提供更多的内存的情况
-
主要的原因有两个:
- Java虚拟机设置的堆内存不够
- 代码中创建了大量的大对象,并且长时间不能被垃圾收集器收集
老版本的jdk永久代在堆中,因此经常出现
java.lang.OutOfMemoryError:PermGen space
,而随着元数据区的引入,直接内存不足的时候,就会出现java.lang.OutOfMemoryError:MetaSpace
-
通常在OOM之前,都会触发一次垃圾收集器的垃圾清除,如尝试回收软引用指向的对象,仍然不足则报OOM
-
当一个超大的对象的大小超过堆的最大值的时候,就不会触发垃圾收集器而是直接报OOM
2.2 内存泄露
- 内存泄露是指对象已经不会被应用程序用到了,但是垃圾回收器又不能回收它们的情况
- 内存泄露不会立刻引起程序崩溃,而是在耗尽内存的时候会出现内存溢出
- 例如:
- 单例模式,单例的生命周期和应用程序是一样长的,而当当前单例程序中,如果持有对外部对象的引用的话,那么这个外部对象就不能被回收了,从而导致内存泄露
- 一些提供关闭的资源未关闭导致垃圾泄露,如数据库连接、网络连接、IO等
3 Stop The World
- Stop The World简称STW,指的是在GC时间发生过程中,会使整个应用程序暂停
- 具体来说,是GC过程的垃圾标记阶段的根可达性分析算法(GC Roots)导致的Java执行线程的停顿,这是由于:
- 分析工作必须在一个能够保证一致性的快照中进行
- 如果出现分析过程中对象的引用关系还在不断变化,则分析结果的准确性将无法保证
- 具体来说,是GC过程的垃圾标记阶段的根可达性分析算法(GC Roots)导致的Java执行线程的停顿,这是由于:
- 被STW中端的应用程序将在完成GC后恢复,频繁GC将影响系统的停歇,因此要减少STW的发生
- STW跟采用哪种垃圾回收器无关,所有的GC都存在
- 哪怕是G1也不能避免STW,只是尽可能缩短暂停时间
- STW是JVM在后台自动发起、完成的,是在用户不可见的情况下把用户正常工作线程全部停掉
- 尽量不要使用
System.gc()
,会导致STW
4 垃圾回收的并发与并行
立即回收器的并行与并发,指的是:
并行
(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态串行
(Serial):如果内存不足,则程序暂停启动JVM垃圾收集器单线程执行进行垃圾回收,回收完成后再启动程序的线程

并发
(Concurrent):指用户线程和垃圾收集线程并发工作(不一定是并行的,也有可能是交替执行),垃圾回收线程在执行的时候不会暂停用户程序的执行- 用户程序在执行,而垃圾回收器运行在另外的CPU核心
- 如CMS、G1
5 安全点与安全区域
-
安全点
(safe point):程序执行过程中并非所有地方都能停顿下来GC,只有在特定位置才能停顿进行GC,这些位置称作安全点
(safe point)safe point的选择很重要,如果太少可能导致GC等待的时间太长,太多则可能导致性能问题。
通常情况下会根据“是否具有让程序长时间执行的特征”为标准进行选择,比如选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等
-
怎么在GC发生之前,检查所有的线程跑到最近的安全点停顿下来呢?
抢断式中断
:首先中断所有的线程,如果还有线程不在安全点,则恢复该线程运行到安全点再中断(目前基本没有虚拟机使用了)主动式中断
:设置一个中断标志,各个线程运行到Safe Point的时候,会主动轮询这个标志,如果中断标志为真,则将自己主动挂起
该机制保证了程序执行的时候,能够在不太长的时间内就进入GC的Safe Point
-
安全区域
(Safe Region):安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域内任何地方进行GC都是安全的,这是为了防止不执行的程序无法响应JVM中断请求的情况(如现成处于Sleep或者Blocked状态) -
总得来说,实际运行的时候
- 当线程运行到Safe Region的时候,就会标识已经进入Safe Region,在此期间发生GC,垃圾收集器就会忽略标识为Safe Region的线程
- 当线程离开Safe Region,就会检查JVM是否完成GC,完成则继续执行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止
6 Java引用
Java中的引用分为了强引用
(Strong Reference)、软引用
(Soft Reference)、弱引用
(Weak Reference)、虚引用
(Phantom Reference)四种引用,四种引用的强度依次减弱
6.1 强引用:不回收
-
强引用是最常见的普通对象引用,也是默认的引用类型
-
一般使用一个new操作符创建一个对象并将其赋值给一个变量的时候,这个变量就成为了指向这个对象的强引用
-
强引用的对象是可触及的,因此垃圾回收器永远不会回收强引用指向的对象
-
一个普通对象如果没有其他的引用关系,并且超过了引用的作用域或者显式地将其强引用赋值为null,就可以被当做垃圾收集了
-
强引用是造成垃圾泄露的主要原因,因为即使发生OOM也不会回收强引用指向的对象
如下测试表明被强引用的对象不会被垃圾回收
public static void main(String[] args) throws InterruptedException { StringBuffer str = new StringBuffer("Hello"); StringBuffer str1 = str; str = null; System.gc(); Thread.sleep(1000); System.out.println(str1); }
6.2 软引用:内存不足即回收
- 只要是被软引用关联的对象,在系统内存空间不够之前,会把这些对象列入垃圾回收范围内进行第二次回收,如果回收之后仍没有足够的内存,就会报内存溢出异常
- 垃圾回收器在某个时刻决定回收软可达对象的时候,会清理软引用,并可选地把引用存到一个专业队列(Reference Queue)
- 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用,如果还有空闲内存就可以暂时保留缓存,当内存不足的时候就清理掉,这样就能保证在使用缓存的时候不会耗尽内存
- 类似弱引用,只不过Java虚拟机会尽量软引用的存活时间更长一点,迫不得已才去清除
public static void main(String[] args) throws InterruptedException { //SoftReference<User> userSoftRef = new SoftReference<>(new User("lxg", 1)); // 等同于下面的写法 User user = new User("lxg", 1); SoftReference<User> userSoftRef = new SoftReference<>(user); user = null; System.out.println(userSoftRef.get()); System.gc(); System.out.println("After GC."); //Thread.sleep(1000); // 内存足够,不会回收软引用 System.out.println(userSoftRef.get()); try { byte[] bytes = new byte[1024 * 1024 * 7]; } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(userSoftRef.get()); } }
结果:
User{userName='lxg', id=1} After GC. User{userName='lxg', id=1} null Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.hikaru.java.reference.SoftReferenceTest.main(SoftReferenceTest.java:54)
注意!软引用是在内存紧张的时候进行回收,而不一定是在内存溢出的时候才进行:
public static void main(String[] args) throws InterruptedException { //SoftReference<User> userSoftRef = new SoftReference<>(new User("lxg", 1)); User user = new User("lxg", 1); SoftReference<User> userSoftRef = new SoftReference<>(user); user = null; System.out.println(userSoftRef.get()); System.gc(); System.out.println("After GC."); //Thread.sleep(1000); System.out.println(userSoftRef.get()); try { byte[] bytes = new byte[1024 * 7185 - 1024 * 626]; } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(userSoftRef.get()); } }
上面代码并没有出现内存溢出,但是软引用还是被回收了
User{userName='lxg', id=1} [GC (System.gc()) [PSYoungGen: 1728K->488K(2560K)] 1728K->684K(9728K), 0.0012236 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 196K->614K(7168K)] 684K->614K(9728K), [Metaspace: 3207K->3207K(1056768K)], 0.0039028 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] After GC. User{userName='lxg', id=1} [GC (Allocation Failure) [PSYoungGen: 60K->160K(2560K)] 674K->774K(9728K), 0.0002132 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 160K->128K(2560K)] 774K->742K(9728K), 0.0002047 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 128K->0K(2560K)] [ParOldGen: 614K->610K(7168K)] 742K->610K(9728K), [Metaspace: 3215K->3215K(1056768K)], 0.0040388 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 610K->610K(9728K), 0.0002728 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 610K->592K(7168K)] 610K->592K(9728K), [Metaspace: 3215K->3215K(1056768K)], 0.0039398 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] null Heap PSYoungGen total 2560K, used 173K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000) eden space 2048K, 8% used [0x00000000ffd00000,0x00000000ffd2b418,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 7168K, used 7151K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000) object space 7168K, 99% used [0x00000000ff600000,0x00000000ffcfbc68,0x00000000ffd00000) Metaspace used 3288K, capacity 4500K, committed 4864K, reserved 1056768K class space used 355K, capacity 388K, committed 512K, reserved 1048576K Process finished with exit code 0
6.3 弱引用:发现即回收
-
只被弱引用关联的对象只能生存到下一次垃圾收集发生为止
-
发生GC的时候,不管系统堆空间是否充足,都会回收掉只被弱引用关联的对象
但是垃圾回收器的线程通常优先级较低,因此并能够一定很快地发现持有弱引用的对象,在这种情况下,弱引用可以存在较长的时间
-
在构造弱引用的时候,可以指定一个
引用队列
,当弱引用被回收的时候,就会加入该指定的引用队列,通过这个队列可以追踪对象的回收情况 -
弱引用也适合保存那些可有可无的缓存数据,当系统内存不足的时候缓存数据就会被回收,从而不会导致内存的溢出,而当内存充足的时候缓存数据又能够存在相当长的时间,从而起到加速系统的作用
-
jdk1.2后通过
java.lang.ref.WeakReference
来实现软引用Object obj = new Object(); WeakReference<Object> objectWeakReference = new WeakReference<>(obj); WeakReference<Object> objectWeakReference1 = new WeakReference<>(new Object()); -
WeakHashMap
的内部类继承了WeakReference
,意味着使用WeakHashMap
会在内存不足的时候进行数据的回收
public class WeakReferenceTest { public static void main(String[] args) { Object obj = new Object(); WeakReference<Object> objectWeakReference = new WeakReference<>(obj); obj = null; System.out.println(objectWeakReference.get()); System.gc(); System.out.println(objectWeakReference.get()); } }
java.lang.Object@677327b6 null
6.4 虚引用:对象回收跟踪
-
虚引用又称
幽灵引用
、幻影引用
,是所有引用对象中最弱的一个 -
一个对象是否有虚引用,完全不会决定对象的生命周期;对象仅持有虚引用,那么跟不持有任何引用是一样的,随时都有可能被垃圾回收器回收
-
虚引用不能被单独使用,当尝试通过虚引用获取被引用的对象的时候,总会得到null
-
为对象设置虚引用关联的目的在于跟踪对象的垃圾回收过程,比如在这个对象被回收的时候能够收到一个系统通知
-
虚引用必须和引用队列一起使用:虚引用在创建的时候必须提供一个引用队列作为参数,当垃圾回收器回收对象发现有虚引用指向的时候,就把这个虚引用加入引用队列,以通知应用程序该对象的回收情况
由于虚引用可以跟踪对象的回收时间,因此可以将一些资源释放操作放置在虚引用中执行和记录
-
jdk1.2之后提供
PhantomReference
来实现虚引用
package com.hikaru.java.reference; import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class PhantomReferenceTest { public static PhantomReferenceTest obj; static ReferenceQueue<PhantomReferenceTest> phantomQueue = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("调用当前方法的finalize"); obj = this; } public static void main(String[] args) { Thread thread = new Thread(() -> { while(true) { if(phantomQueue != null) { PhantomReference<PhantomReferenceTest> reft = null; try { reft = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove(); } catch (InterruptedException e) { e.printStackTrace(); } if(reft != null) { System.out.println("虚引用指向的实例被回收了"); } } } }); // 设置线程为守护线程 thread.setDaemon(true); thread.start(); phantomQueue = new ReferenceQueue<>(); obj = new PhantomReferenceTest(); PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<>(obj, phantomQueue); // 无法通过虚引用获取对象 System.out.println(phantomRef.get()); // 去掉强引用 obj = null; // finalize方法会复活对象 System.gc(); try { Thread.sleep(1000); if(obj == null) { System.out.println("obj是null"); } else { System.out.println("obj存活"); } } catch (InterruptedException e) { e.printStackTrace(); } obj = null; // finalize只能被调用一次,因此第二次垃圾回收会回收掉obj System.gc(); try { Thread.sleep(1000); if(obj == null) { System.out.println("obj是null"); } else { System.out.println("obj存活"); } } catch (InterruptedException e) { e.printStackTrace(); } } }
null 调用当前方法的finalize obj存活 虚引用指向的实例被回收了 obj是null
6.5 终结器引用
- 它用以实现对象的finalize()方法,无需手动编码,内部配合队列使用
- 在GC的时候,终结器引用入队,由Finalizer线程通过终结器引用找到被引用的对象调用它的finalize()方法,然后再第二次GC的时候才能回收该对象
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2022-07-12 【Vue项目】商品汇前台(一)组件拆分+请求封装
2022-07-12 【Vue】前端解决跨域问题