ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] 垃圾回收步骤 1. 确定哪些对象是垃圾:引用计数、可达性分析 2. 何时回收:垃圾算法 3. 如何回收:垃圾回收器 ![](https://img.kancloud.cn/eb/e9/ebe95c9f868f5297e4a3309d854bac2f_979x633.png) # 1. 找到垃圾 1.引用计数法 2.根可达 root searhing ## 1.1 引用计数法 ### 1.1.1 引用计数法原理 > 通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。 ![](https://img.kancloud.cn/2f/62/2f6247c573a859448c759d7e13a06c7d_894x347.png) ### 1.1.2 引用分类 强、软、弱、虚 #### 一、 强引用 new 出来的对象 “Object obj = new Object();”或者 String s=”abc”中变量s就是字符串对象”abc”的一个强引用,**任何被强引用指向的对象都不能被垃圾回收器回收**,这些对象都是在程序中需要的 #### 二、 软引用 1. 只有在jvm需要内存时,才会回收这些对象 2. 软引用可以很好的用来实现缓存,当JVM需要内存时,垃圾回收器就会回收这些只有被软引用指向的对象 Java中的软引用使用java.lang.ref.SoftReference类来表示,如下软引用缓存类 ```public class SoftCache<T> { // 引用队列 private ReferenceQueue<T> referenceQueue = new ReferenceQueue<>(); // 保存软引用集合,在引用对象被回收后销毁 private List<Reference<T>> list = new ArrayList<>(); // 添加缓存对象 public synchronized void add(T obj){ // 构建软引用 Reference<T> reference = new SoftReference<T>(obj, referenceQueue); // 加入列表中 list.add(reference); } // 获取缓存对象 public synchronized T get(int index){ // 先对无效引用进行清理 clear(); if (index < 0 || list.size() < index){ return null; } Reference<T> reference = list.get(index); return reference == null ? null : reference.get(); } public int size(){ return list.size(); } @SuppressWarnings("unchecked") private void clear(){ Reference<T> reference; while (null != (reference = (Reference<T>) referenceQueue.poll())){ list.remove(reference); } } } ``` #### 三、 弱引用 1. 如果一个对象只有**弱引用**指向它,垃圾回收器会立即回收该对象,这是一种急切回收方式 2. 而弱引用非常适合存储元数据,例如:存储ClassLoader引用。如果没有类被加载,那么也没有指向ClassLoader的引用。一旦上一次的强引用被去除,只有弱引用的ClassLoader就会被回收 #### 四、虚引用 拥有虚引用的对象可以在任何时候被垃圾回收器回收 ## 1.2 根可达 ### 可作为GC Roots的对象 > 通过一系列的称为“GCsRoots”的对象作为起始点,从这些节点向下开始搜索,搜索所走过的路径称为引用链(Reference Chain)。 当一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达)时,则证明该对象是不可用的。 ![](https://img.kancloud.cn/d6/8a/d68afbea5a0d9a4c1a50b0a66d3044b0_716x452.png) 可作为跟节点的对象 1. 虚拟机栈(栈帧中的本地变量表)中引用的对象 2. 方法区中类静态属性引用的对象 3. 方法区中常量引用的对象 4. 本地方法栈中JNI(即native方法)引用的对象 # 2. 清除垃圾 ## 2.1 标记清除 标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段 一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除 ![](https://img.kancloud.cn/d6/86/d68602b9064deda254f51850b17c83e5_1242x614.png) 缺点 * 涉及大量的内存遍历工作,所以执行性能较低,这也会导致“stop the world”时间较长,java程序吞吐量降低; * 对象被清除之后,被清除的对象留下内存的空缺位置,造成内存不连续,空间浪费。 ## 2.2 复制清除(年轻代:eden区和survivor区) 将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。 ![](https://img.kancloud.cn/c5/ac/c5ac0327a217d3720abe944ff6f58177_818x419.png) ## 2.3 mark-compact标记压缩 标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。 ![](https://img.kancloud.cn/99/75/997541cd7bb304bac02ab06128133f6e_810x383.png) 优点: 因为标记清除算法会造成内存的不连续,所以标记整理(压缩)算在标记清除算法的基础上,增加了整理过程,解决了标记清除算法内存不连续的问题。同时也消除了复制算法当中,内存减半的高额代价。 缺点: 标记整理(压缩)也会产生“stop the world”,不能和java程序并发执行。在压缩过程中一些对象内存地址会发生改变,java程序只能等待压缩完成后才能继续。 ## 2.4 分代收集算法(新生代的GC+老年代的GC) 当前商业虚拟机都采用分代收集算法。 分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。 在新生代,每次垃圾收集器都发现有大批对象死去,只有少量的存活,那就选择复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 在老年代因为对象的存活率较高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法进行回收。 注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。 # 3. 垃圾回收器 随着内存增大,垃圾回收器演进,垃圾回收主要针对年轻代和老年代,并且用的回收器也有多重组合方式,如下图的虚线连接 ![](https://img.kancloud.cn/ff/f0/fff0167ed4eea0f544a8aa83b3233063_1178x398.png) ## 3.1 CMS 用于老年代回收,常与ParNew搭配 1. gc root标记垃圾 2. 运行中也要标记 3. 纠错标记(运行后可能垃圾有变成有用的了) 4. 清除垃圾 ![](https://img.kancloud.cn/2f/0b/2f0b8e1d3edf6b3e6e571f11c0d3f97d_1068x424.png) ## 2.2 G1 年轻代YGC时会全部区域回收,当年轻代内存较大时,停顿会越来越长 ## 3.3 ZGC 弥补了G1的短板,不区分年轻代和老年代、 # 4. 内存泄漏和内存溢出 1. 内存泄漏最终导致内存溢出 2. 内存泄漏指:应该被垃圾回收测内存,没有被有效的回收,导致空间的浪费 3. 内存溢出:没有足够的内存给对象分配,导致的OOM ## 4.1 内存泄漏 > 内存回收,简单说就是应该被垃圾回收的对象没有被垃圾回收 ![](https://img.kancloud.cn/21/ff/21ff75bdc09a72ce89cce01ac2f06062_625x315.png) **1. 静态集合类引起内存泄漏** 静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。 ~~~java public class OOM { static List list = new ArrayList(); public void oomTests(){ Object obj = new Object(); list.add(obj); } } ~~~ **2. 单例模式**: 和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。 **3. 数据连接、IO、Socket等连接** 创建的连接不再使用时,需要调用 **close** 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。 ~~~text try { Connection conn = null; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("url", "", ""); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("...."); } catch (Exception e) { }finally { //不关闭连接 } } ~~~ **4. 变量不合理的作用域** 一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。 ~~~ public class Simple { Object object; public void method1(){ object = new Object(); //...其他代码 //由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放 object = null; } } ~~~ **5. 引用了外部类的非静态内部类** 非静态内部类(或匿名类)的初始化总是需要依赖外部类的实例。默认情况下,每个非静态内部类都包含对其**包含类**的隐式引用,若在程序中使用这个内部类对象,那么**即使在包含类对象超出范围之后,也不会被回收**(内部类对象隐式地持有外部类对象的引用,使其成不能被回收)。 **6. Hash 值发生改变** 对象Hash值改变,使用HashMap、HashSet等容器中时候,由于对象修改之后的Hah值和存储进容器时的Hash值不同,会导致无法从容器中**单独删除**当前对象,造成内存泄露。 **7. ThreadLocal** 造成的内存泄漏 ThreadLocal 可以实现变量的线程隔离,但若使用不当,就可能会引入内存泄漏问题。 ## 4.2 如何排查内存泄漏