1 System.gc()的理解

在默认情况下,通过system.gc()者Runtime.getRuntime().gc() 的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
然而system.gc() )调用附带一个免责声明,无法保证对垃圾收集器的调用。(不能确保立即生效)
JVM实现者可以通过system.gc() 调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。

2 内存溢出

内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现ooM的情况。
大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的FullGC操作,这时候会回收大量的内存,供应用程序继续使用。

首先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:

  • Java虚拟机的堆内存设置不够。

比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms 、-Xmx来调整。

  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

在抛出OutofMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。当然,也不是在任何情况下垃圾收集器都会被触发的。比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutofMemoryError。

3 内存泄漏

也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,也可以叫做宽泛意义上的“内存泄漏”。
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现outofMemory异常,导致程序崩溃。

3.1 举例

  • 单例模式

单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。

  • 一些提供close的资源未关闭导致内存泄漏

数据库连接(dataSourse.getConnection() ),网络连接(socket)和io连接必须手动close,否则是不能被回收的。

4 对象已死?

堆里几乎存放着java中所有的实例对象,在对堆进行回收前,第一件事情就是要确定这些对象有哪些还 “存活” 着 ?哪些已经 “死去” (不可能再被任何途径使用的对象)。

4.1 引用计数算法

给对象中增加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效,计数器值就建1;计算器为0的对象就是不可能再被使用的。
引用计数算法的实现简单,判断效率也很高,但是它很难解决对象之间的相互循环引用的问题。所以Java没有选用引用计数算法来管理内存。

  1. package **;
  2. /**
  3. * @author hll
  4. * @date 2019/8/19.
  5. */
  6. public class Gc {
  7. public Object instance = null;
  8. private static final int _1M = 1024 * 1024;
  9. private byte [] bigSize = new byte[2 * _1M];
  10. public static void testGc() {
  11. Gc gc1 = new Gc();
  12. Gc gc2 = new Gc();
  13. gc1.instance = gc2;
  14. gc2.instance= gc1;
  15. gc1 = null;
  16. gc2 = null;
  17. System.gc();
  18. }
  19. public static void main(String[] args) {
  20. testGc();
  21. }
  22. }

结果:

image.png

JVM的内存由134466K->1561K说明了gc1,gc2两个对象的内存还是被回收了,说明idea的虚拟机并不是通过引用计数法来判断对象是否存活。

4.2 根搜索算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象到“GC Roots”没有任何引用链相连的话,也就是GC Roots到这个对象不可达时,证明此对象已经不可用,可以被回收了。
可作为GC roots的对象的包括下面几种:

  • 栈中的对象引用、
  • 方法区中常量的引用、
  • 方法区中静态对象的引擎、
  • 本地方法区中native对象的引用

垃圾回收概述(二) - 图3

5 Stop The World

stop-the-world,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。

  • 分析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
STW事件和采用哪款GC无关所有的GC都有这个事件。哪怕是G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。开发中不要用system.gc() 会导致stop-the-world的发生。

6 安全点与安全区域

在 JVM 中如何判断对象可以被回收 一文中,我们知道 HotSpot 虚拟机采取的是可达性分析算法。即通过 GC Roots 枚举判定待回收的对象。那么,首先要找到哪些是 GC Roots。有两种查找 GC Roots 的方法:

  • 一种是遍历方法区和栈区查找(保守式 GC)。
  • 一种是通过 OopMap 数据结构来记录 GC Roots 的位置(准确式 GC)。

保守式 GC 的成本太高。准确式 GC 的优点就是能够让虚拟机快速定位到 GC Roots。对应 OopMap 的位置即可作为一个安全点(Safe Point)。在执行 GC 操作时,所有的工作线程必须停顿,这就是所谓的”Stop-The-World”。
为什么呢?
因为可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。

6.1 安全点

程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)”。
Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
如何在gc发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断:(目前没有虚拟机采用)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  • 主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)

注意:程序运行到安全点,不是一定要进行垃圾回收。而是在这些点上进行垃圾回收,较为安全。所以叫做安全点。

6.2 安全区域

Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?例如线程处于sleep-状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
执行流程:

  • 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Relgion,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程
  • 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止。

7 强引用、软引用、虚引用、弱引用

7.1 强引用

当内存不足,jvm开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收。这也是Java中最常见的普通对象的引用,只要还有强引用指向这个对象,就不会被垃圾回收。
当这个对象没有了其他的引用关系,只要是超过了引用的作用域,或者显示的将强引用赋值为null,一般就可以进行垃圾回收了。

7.2 软引用

软引用是相对强引用弱化了一些的引用,对于软引用的对象来说:

  • 当内存充足时,它不会被回收。
  • 当内存不足时。会被回收。

通常用在对内存敏感的程序中,就像高速缓存。

7.3 弱引用

发现即回收
弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。
软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
在JDK1.2版之后提供了WeakReference类来实现弱引用

  1. // 声明强引用
  2. Object obj = new Object();
  3. // 创建一个弱引用
  4. WeakReference<Object> sf = new WeakReference<>(obj);
  5. obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用

弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。

  • 面试题:你开发中使用过WeakHashMap吗?

WeakHashMap用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM

7.4 虚引用

也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个
一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
虚引用无法获取到我们的数据
在JDK1.2版之后提供了PhantomReference类来实现虚引用。

  1. // 声明强引用
  2. Object obj = new Object();
  3. // 声明引用队列
  4. ReferenceQueue phantomQueue = new ReferenceQueue();
  5. // 声明虚引用(还需要传入引用队列)
  6. PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
  7. obj = null;