【目录】 【上一篇:String Table】 【下一篇:垃圾回收相关算法】
一、垃圾回收概述及相关概念
1、什么是垃圾
经典三问:哪些内存需要回收?什么是回收?如何回收?
💡 什么是垃圾?
垃圾是指在程序运行当中,没有任何指针指向的对象,这个对象就是需要被回收的垃圾
💡 为什么要回收?
如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占用的空间会一直保留到应用程序执行结束,被占用的空间无法被其他对象使用,极易导致内存溢出,从而使程序宕机。
2、为什么需要 GC
对于高级语言来讲,一个基本的认识是如果不进行垃圾回收,内存迟早会被消耗完。对于 GC,除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移动到堆的一段,以便 JVM 将整理出的内存分配给新的对象。
随着应用程序所应付的业务越来越大、复杂,用户越来越多,没有 GC 就不能保证应用程序的正常运行。而经常造成 STW 的 GC 又跟不上实际的需求,所以才会不断地尝试对 GC 进行优化。
3、Java 垃圾回收机制
- 自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄露和内存溢出的风险;
- 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心的专注于业务开发;
3.1、GC 的作用区域:
从次数上分类:
- 频繁地收集年轻代;
- 较少收集老年代;
- 基本不收集元空间。
4、内存泄露与内存溢出
4.1、内存泄露(Memory Leak)
- 严格意义上来说,只有对象不再被程序使用了,但是 GC 又不能回收它们所占用的内存,才叫内存泄露。
- 在实际情况有一些不太友好的操作,导致对象的生命周期非常的长(能通过 GC Roots 引用链找到这个对象,但实际上这个对象又没任何存在的意义),也可以叫做广意上的对象内存泄露。
- 内存泄露并不会立即引起程序崩溃,但是一旦发生内存泄露,程序中可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutOfMemory 异常。
💡 举例:
①、单例模式
单例对象的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的引用的话,并且这个引用又没有及时断开,那么这个外部对象是不能被回收的,从而产生内存泄露
②、一些提供 close() 的资源未关闭导致内存泄露
数据库连接(dataSource.getConnection()),网络连接(socket)和 io 连接必须手动 close,否则是不能被回收的。
4.2、内存溢出(OOM)
- 内存溢出是相对于内存泄露来说的,在 Javadoc 中对 OutOfMemoryError 的解释是:没有空闲内存,并且垃圾收集器也无法提供更多的内存。
💡 没有空闲内存的情况,说明 Java 虚拟机的堆内存不够,有两种情况:
①、Java 虚拟机的堆内存设置不够(可通过 -Xms、-Xmx 参数修改堆内存大小);
②、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(长时间被引用)。
4.3、Java 中内存泄露的 8 种情况
- 静态集合类
- 如 HashMap、LinkedList 等,如果这些容器为静态的,那么它们的生命周期与 JVM 的生命周期一致,则容器里面的对象生命周期也相同。
- 单例模式
- 因为单例的静态特性,在整个程序的运行期间,只会创建一次,所以其生命周期也和 JVM 的生命周期一样长。如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收。
- 内部类只有外部类
- 各种连接,如数据库连接、网络连接、IO连接等
- 变量不合理的作用域
- 一个变量定义的作用范围大于其使用范围
- 改变哈希值
- 当一个对象被存储进 HashSet 集合中以后,就 不能修改这个对象中的那些参与计算哈希值的字段,否者对象修改后的哈希值就与最初存储进 HashSet 集合中时的哈希值就不同了。
- 缓存泄露
- 不要使用 HashMap 来作为缓存容器,建议使用 WeakHashMap
- 监听器和回调
5、Minor GC、Major GC、Full GC详解
- JVM 在进行 GC 时,并非每次都对三个内存区域(新生代、老年代、方法区)一起回收,大部分时候回收的都是指新生代;
- 针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:一种是部分收集(Partial GC);一种是整堆收集(Full GC);
5.1、新生代 GC(Minor GC、YGC)触发机制:
当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。)
5.2、老年代 GC(Major GC/Full GC)触发机制:
- 指发生在老年代的 GC,对象从老年代消失时,就是 Major GC 或 Full GC 发生了;
- 出现 Major GC时,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程);
- 也就是说在老年代空间不足时,会先尝试触发 Minor GC,如果之后空间还不足,则触发 Major GC。
- Major GC 的速度一般会比 Minor GC 慢 10 倍以上, STW 的时间更长;
- 如果 Major GC 后,内存还不足,就会报 OOM。
5.3、Full GC 触发机制:
- 显式调用 System.gc() 时,系统建议执行 Full GC,但不是必然执行的;
- 老年代空间不足;
- 方法区空间不足;
- 通过 Minor GC 后,进入老年代的平均大小大于老年代的可用内存
- 由 Eden 区、survivor space0(Fom Space)区向 survivor space1(To Space)区复制时,对象大小大于 To Sace 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
💡 Full GC 是开发或调优中尽量避免的
发生时机
6、System.gc() 的概念
- 在默认情况下,通过 System.gc() 或者 Runtime.getRuntime().gc() 的调用,会 显示触发 Full GC,同时对老年代和新生代进行回收,尝试释放内存。
- 然而 System.gc() 调用附带一个免责声明,无法保证 100% 对垃圾收集器的调用。
- JVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况下,垃圾回收应该是自动进行的,无需手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,就可以在运行时调用 System.gc() 。
7、对象的 finalization 机制
Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象时,会回收该对象,但在回收之前,总会先调用这个对象的 finalize() ;
finalize() 定义在 Object 类中,允许在子类重写,用于对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理操作,如:关闭文件、套子节和数据库连接等。
永远 不要主动 调用 finalize(),应该交给垃圾回收机制调用:
💡 在 finalize() 时,可能会导致对象复活;
finalize() 的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,若不发生 GC ,则 finalize() 没有执行的机会;
一个糟糕的 finalize() 会严重影响 GC 性能。
由于 finalize() 的存在,虚拟机的对象一般处于三种可能的状态:
💡 如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象就需要被回收,但实际上,该对象也并非 “非死不可”,当第一次触发回收机制时,该对象暂时处于 “缓刑” 阶段。一个无法触及的对象有可能在某一个条件下 “复活” 自己。如果是这样,那么触发回收机制就对其进行回收,这是不合理的,为此,虚拟机中对对象定义了三种状态:
- 可触及的:从根节点开始,有引用链可达这个对象;
- 可复活的:对象的所有引用链都被释放,这个对象第一次被标记为 ”垃圾“,但是对象有可能在 finalize() 中复活;
- 不可触及的:对象的 finalize() 已经被调用过,并没有复活,那么就会进入不可触及的状态(或则第二次死亡时,就会直接变成不可触及的),最终被回收。不可触及的对象不能被复活,因为 finalize() 只会被调用一次(永远只有一次)。
判定一个对象是否可回收,至少要经历两次标记过程:
1、如果对象到 GC Roots 没有引用链,则进行第一次标记
2、进行筛选,判断此对象是否有必要执行 finalize()。
8、Stop The World
- Stop the world 简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时,整个用户线程都会被暂停,没有任何响应。
- 可达性分析算法中枚举根节点(GC Roots)会导致所有的 Java 执行线程停顿。
- 分析工作必须在一个能确保一致性的快照中进行;
- 一致性指整个分析期间整个执行系统看起来像是被冻结在某个时间点上;
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。
- 可达性分析算法中枚举根节点(GC Roots)会导致所有的 Java 执行线程停顿。
- 被 STW 中断的用户线程会在完成 GC 之后恢复,频繁中断会让用户觉得程序很卡,执行很慢。
- STW 事件在所有的 GC 收集器中都存在。
9、垃圾回收的并行与并发
9.1、并发:
在操作系统中,是一个时间段内,有多个程序都处于运行状态,且这几个程序都在一个处理器上运行。
并发不是真正意义上的“同时进行”,只是一个处理器把一个时间段划分成多个时间片段,然后多个程序在这些时间片段之间切换运行;
9.2、并行:
当系统有一个以上处理器时,多个处理器都在同时执行程序,程序与程序之间互不抢占 CPU 资源。
9.3、垃圾回收的并发与并行:
串行:串行回收是指在同一时间段内只允许有一个 CPU 用于执行垃圾回收操作,此时用户线程被暂停,直至垃圾收集工作结束;
并行:和串行相反,并行收集指在同一时间段内允许多个 CPU 同时执行垃圾收集,因此提升了应用的吞吐量,不过在垃圾收集时还是会暂停用户线程,直至垃圾收集工作结束;
并发:用户线程与垃圾收集线程同时执行;
10、安全点与安全区域
10.1、安全点:
用户线程在执行期间,并非在任意时刻都能暂停下来停止垃圾收集线程,只有在特定的位置才能停顿下来开始 GC,这些位置称为“安全点(Safe Point)。
Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据 “是否具有让程序长时间执行的特征” 为标准。比如:选择一些执行时间较长的指令作为 Safe Point,如方法调用、循环跳转和异常跳转等。
如何在 GC 发生时,检查所有线程是否都跑到最近的安全点停顿下来呢?
1、抢先式中断(目前所有虚拟机都不再采用):
首先中断所有线程,如果还有线程没有在安全点,就恢复线程,让其跑到安全点。
2、主动式中断:
设置一个中断标识,各个线程运行到 Safe Point 的时候,主动轮询这个标识,如果中断标识为真,则将自己进行中断挂起。
10.2、安全区域:
Safe Point 机制保证了程序执行时,在不太长的时间内就会遇到可以进入 GC 的 Safe Point。但是程序 “不执行” 的时候呢?例如线程处于 Sleep、Blocked 状态,这时候线程无法响应 JVM 的中断请求,”走“到安全点去挂起,JVM 也不太可能等待线程被唤醒,对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不再发生变化,在这个区域中的任何位置开始 GC 都是安全的。我们也可以把 Safe Region 看做是扩展了的 Safe Point。
实际执行:
1、当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果在这段时间内发生 GC,JVM 会忽略标识为 Safe Region 状态的线程。
2、当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成,则继续运行,否则线程必须等待、直到接收到可以安全离开 Safe Region 的信号为止。
11、JAVA 引用
👉 在 JDK1.2 之后,Java 对引用的概念进行了扩充,将引用分为 **强引用(Strong Reference)、软引用(Sofe Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)**4 种,这 4 种引用强度依次减弱
除强引用外,其他 3 种引用均可以在 java.lang.ref 包中找到。它们都继承至 Reference 接口。
- 强引用:最传统的 “引用” 定义,是指在程序代码中普遍存在的引用赋值,如 ”Object obj = new Object()“ 这种引用关系。无论在任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,JVM 将会抛出内存溢出的异常。
- 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论空间是否足够,都会回收掉被弱引用关联的对象。
- 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个实例对象。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
11.1、强引用
- 在 Java 中使用 new 关键字创建对象,并将其赋值给一个变量时,这个变量就成为指向该实例对象的一个强引用;
- 强引用的对象是可触及的,垃圾收集器永远不会回收被引用的对象;
- 强引用是造成 Java 内存泄露的主要原因之一。
11.2、软引用
- 软引用是用来描述一些还有用,但又不是必须的对象(比如高速缓存)。只要被软引用关联着的对象,在系统将要发生内存溢出之前(垃圾回收器认为内存不够),会把这些对象列进回收范围之中进行第二次回收,如果这次回收之后还没有足够的内存,才会抛出内存溢出的异常;
- 垃圾收集器在某个时刻决定回收软可达对象的时候,会清理软引用,并可选择的把软引用存放到一个软引用列表(Reference Queue)。
声明方式:
Object obj = new Object(); // 声明强引用,创建实例对象
SoftReference<Object> sf = new SoftReference<Object>(obj);// 软引用实例对象
obj = null; // 销毁强引用
------------------------------------
SoftReference<Object> sf = new SoftReference<Object>(new Object);
// 获取软引用对象
sf.get();
11.3、弱引用
- 弱引用也是用来描述一些还有用,但又非必须的对象。只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统 GC 时,只要发现弱引用,不管内存是否足够,都会回收;但是,由于垃圾收集器线程通常优先级很低,因此,并不一定能很快的发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长一段时间。
- 软引用、弱引用都非常适合来保存那些可有可无的缓存数据。当内存不足时,这些缓存数据会被回收,当内存充足时,这些缓存数据可以存在相当长一段时间,从而起到加速系统的作用。
声明方式:
WeakReference<Object> wr = new WeakReference<Object>(new Object);
弱引用与软引用对象最大的区别:当 GC 在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,是直接回收的,所以弱引用对象更容易被 GC 回收。
面试题:你在开发中使用过 WeakHashMap 吗?
11.4、虚引用 - 对象回收跟踪
- 所有引用类型中最弱的一个,一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没引用几乎是一样的,随时都有可能被垃圾回收机制回收。并且通过这个虚引用是无法获取到具体实例对象的。
- 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被手机器回收时收到一个系统通知。
- 虚引用必须和引用队列一起使用,由于虚引用可以跟踪对象的回收时间,因此也可以将一些资源释放操作放置在虚引用中执行记录。
声明方式:
Object obj = new Object();
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> pf = new PhantomReferene<>(obj, phantomQueue );
11.5、终结引用
它用以实现对象的 finalize() 方法