企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 垃圾回收 Garbage Collection,GC GC只在**堆和本地方法区**中起作用。   ## 一、确定对象存活 ### 1.1 引用计数法     在对象中添加一个引用计数器,每当有一个地方引用该对象的时候,计数器的值就加1;当引用失效的时候计数器的值就减1;计数器的值为0的对象就是不再被使用的对象。python就是使用这种方式管理内存的。 这种方式无法解决循环引用的问题,**Java中不使用这种方式判断一个对象是否存活,但是Netty的ByteBuf是采用这种方式进行回收的。**      ### 1.2 可达性分析算法 **Java中使用的算法**     通过一系列的“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,如果某个对象没有任何“引用链”可以到达,就证明该对象不再被使用。 哪些对象可以作为GC Root? * 系统类:Object,HashMap...; * 本地方法栈的类 Native Stack; * synchronized关键字持有的对象; * 栈帧中局部变量表的对象,堆栈中的参数,临时变量。 * 方法区中静态属性引用的对象。 * 基本类型对应的Class对象。      ## 二、引用类型 JKD1.2 版本之后将引用概念进行扩充,分为强引用、软引用、弱引用、虚引用。 ### 2.1 强引用 Strongly Reference     最传统的引用定义,直接使用关键new之后的即为强引用关系。强引用关系直接被GC Root对象引用。没有Gc Root引用时才会被回收。 ### 2.2 软引用 Soft Reference     用来修饰一些还有用,但是非必须的对象。在系统发生内存溢出之前,会对这些对象进行第二次回收,如果回收之后内存还不够,才会发生内存溢出异常。 使用`SoftReference类表示`。 ### 2.3 弱引用 Weak Reference     比软引用的关系更弱一些,被弱引用关联的对象只能存活到下次垃圾回收,不管内存是否足够都会进行回收。在ThreadLocalMap的Entry中就使用WeakReference来引用作为键的ThreadLocal对象。 【备注:关于ThreadLocal的内容可以查看《第一章 Java基础》-ThreadLocal】 使用`WeakReference类表示`。 ~~~  在弱引用和软引用中有个一般会关联一个引用队列,当引用对象被清理时,弱引用和软引用本身就会被放到引用队列中,等待被Reference Handler线程清理。可以不配合引用队列使用。 ~~~ ### 2.4 虚引用     引用直接内存对象的引用,Cleaner。`会关联一个引用队列。`     记录直接内存地址,当放到引用队列中时会被一个定时调用的线程调用Unsafe.freeMemory方法清理直接内存。      💡对象真正死亡时间!  一个对象就算没有被GC Roots的链所引用,也不一定会被清除;会经历两次标记过程:      1. 第一次筛选看是否有必要执行finalize()方法,如果对象没有覆盖finalize()方法或者该方法已经被调用过了,则都会认为没有必要执行。如果对象被认为有必要执行,则会被放入一个F-Queue队列中,有一条低优先级的Finalizer线程执行它们的finalize()方法,不一定会等方法执行完毕。      2. 第二次再对F-Queue队列进行筛选,如果对象还是没有被任何GC Roots链所引用,就会真正被清除掉。  ​  finalize()方法只会被执行一次。这种在finalize()方法中重新引用的方式也被称为终结器引用。      ## 三、垃圾回收算法     大多数的垃圾回收算法都是基于“分代收集”理论的(因此要注意在Java运行时数据区其实并没有分代的概念的,分代的概念是用在垃圾回收当中的)。 > 分代假说: > > 1. 弱分代假说:绝大数对象都是朝生夕灭的。 --> 引申出新生代的概念。 > > 2. 强分代假说:熬过越多次垃圾回收的对象越难消除。 --> 引申出老年代的概念。 > > 3. 跨分代假说:跨代引用相对于同代引用来说是非常少的。 > 注意:将堆划分出不同的区域之后,才有了"Minor GC","Major GC","Full GC"这些不同称呼的垃圾回收器和”标记-复制算法“,”标记-清除算法“,”标记-整理算法“这些针对不同对象生存情况的算法。      **垃圾回收器的分类** 1. 部分收集Partial GC: 1. 新生代收集 MinorGC:针对新生代的垃圾收集。 2. 老年代收集 MajorGC:针对老年代的垃圾收集。--> CMS收集器 3. 混合收集MixedGC:针对整个新生代的垃圾收集以及部分老年代的垃圾收集。--> G1收集器。 2. 整堆收集 Full GC:收集整个堆和方法区的垃圾收集器。      ### 3.1 算法 #### 3.1.1 标记清除算法 标记阶段:首先标记出所有需要回收的对象; 清除阶段:标记结束之后统一清除要回收的对象。 :-: ![](https://img.kancloud.cn/d2/9f/d29fa17ee0532684ebb75c504828226a_845x601.png) 优点:不需要额外的内存空间。 缺点: * 两次扫描,当堆中有大量的要回收的对象的时候会严重浪费时间。 * 产生内存碎片,当大对象无法找到足够的内存空间的时候又不得不触发另外一次垃圾回收。      #### 3.1.2 标记复制算法     简称为复制算法,解决标记清除算法中回收大量对象时慢的缺点。一开始提出是将内存容量分为两块大小一致的空间,每次只使用其中一块,当一块空间用完了,就将存活的对象复制到另外一块上去,清空掉原来的那块空间。     这种方式每次都对一整块空间操作,不用考虑内存碎片的问题,也比较快。     在**后来用到新生代**的时候,将新生代分为Eden空间和两块较小的Survivor空间(比例为8:1:1)。每次在使用的时候只使用Eden区和一块Survivor的空间(from区),在发生垃圾回收的时候将Eden区和From区的存活对象放入另外一块Survivor区中(to区),接着将Eden区和From区都清空掉。这种划分方法是基于对象都是“朝生夕灭”(分代假说)的,只用了额外的10%的空间。这个空间的比例可以通过虚拟机参数调整: ~~~  -XX:InitialSurvivorRatio=ratio ~~~ :-: ![](https://img.kancloud.cn/9e/c7/9ec7a43531d1bcf745a7f669350745c8_1248x484.png) > PS:当TO区不够存放存活的对象的时候,会通过一种叫做分配担保的机制将这些对象放入老年代。   #### 3.1.3 标记整理算法     上面两种算法更多针对新生代的,而标记整理算法更多针对老年代的。标记整理算法前期过程与标记清除算法类似,但是在标记完成之后会将所有存活的对象移动到一遍,然后清空掉另外一边的内存。 :-: ![](https://img.kancloud.cn/73/9b/739b525f7821873325f36b303d0ea710_1407x635.png)     在进行垃圾回收的时候需要移动对象的方式需要暂停应用程序的运行,官方称之为Stop The World。      ### 3.2 垃圾收集器 :-: ![](https://img.kancloud.cn/da/7a/da7a905d9c3e72673637ded16592e3cc_772x411.png) 图中的每个连线都是一种组合      #### 3.2.1 Serial收集器     **Serial作用于新生代**,Serial Old作用于老年代;都是单线程,在进行垃圾回收的时候需要暂停其他用户线程(STW)。 :-: ![](https://img.kancloud.cn/a8/ad/a8ad7ee187b031343d304d797b4368d0_997x347.png)     Serial收集器虽然是单线程的,但是仍然是HotSpot在客户端下的默认新生代的垃圾回收器,特别适合在微服务这种架构下单个服务占用的内存很少,即是使用单线程收集也不会占用太多时间。随之而来的好处就是简单,高效。      #### 3.2.2 ParNew收集器     是Serial的多线程版本,在新生代的垃圾回收中可以使用多个线程进行垃圾回收,但是仍然会造成STW。 ![](https://img.kancloud.cn/40/0c/400cba40020a9a730e4d4beb701255ed_987x347.png)     除了Serial Old外,只有ParNew能和CMS配合使用,多用于服务端的垃圾回收。在JDK9之前ParNew + CMS的组合是官方推荐的在服务端模式下的收集器的解决方案;在JDK9之后ParNew只能和CMS组合使用了。      #### 3.2.3 Parallel Scavenge收集器     `新生代收集器`,与上面两个一样基于标记-复制算法,也是能够并行的多线程收集器。该收集器更多的关注的是吞吐量优先,也被称为“吞吐量优先收集器”。     Parallel Scavenge Old基于标记整理算法的作用于老年代的收集器。      #### 3.2.4 CMS收集器     Concurrent Mark Sweep,基于标记清除算法的用于**老年代**的收集器,同时也是一款以获取最短回收停顿时间为目标的收集器。 其包含的过程为: 1. 初始标记`需要STW` 仅仅标记一下GC Roots能直接关联的对象,耗时非常快。 2. 并发标记 从GC Roots直接遍历整个对象图,这个过程比较耗时,但是不用暂停用户线程; 3. 重新标记`需要STW` 重新标记是为了再一次标记那些在并发标记过程中用户线程运行可能产生的垃圾,比初始标记耗时相对高一点点。 4. 并发清除 清除标记出来的对象,不需要移动对象,可以和用户线程一起运行。 :-: ![](https://img.kancloud.cn/3b/d1/3bd1c7755e5629d6b70d23814b9b2409_1198x373.png) CMS收集器可以实现并发低停顿。   #### 3.2.5 G1收集器 Garbage First,被官方称为“全功能的垃圾收集器”,与上面其他基于分代回收的垃圾回收器不同的是,G1回收器基于Region的内存布局进行回收,同时作用与老年代和新生代,因此G1也被称为为Mixed GC。JDK9之后G1开始替代Parallel Scanvenge和Parallel Old组合,称为服务端的默认垃圾回收器,同时CMS不被推荐使用。 > G1目标 -> 停顿时间模型: > 能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒。 G1的内存布局:将堆分为多块Region,每块Region的大小可以根据参数`-XX:G1HeapRegionSize`设置为1~32MB,且为2的N次幂大小。在G1中没有严格的新生代和老年代的概念,而是用多块(不一定连续)的Region来充当这些角色。同时Region中有一块特殊的Humongouns区域,用来保存大对象。 G1将Region作为回收的最小单元,同时在后台中跟踪记录收集每块Region的可获得空间大小和时间代价,并维护成一个优先级列表。每次在规定的停顿时间内(-XX:MaxGCPauseMillis,默认为200ms)就先对优先级高的Region进行回收。其包括4个如下的过程: 1. 初始标记:标记GC Roots能够关联到的对象,并且修改TAMS指针(用于并发标记时用户生成对象的分配),需要停顿线程。 2. 并发标记:从GC Roots开始对堆中的对象进行可达性分析,递归扫描整个堆,找出要回收的对象,可与用户线程并发执行。 3. 最终标记:停顿用户线程,处理并发阶段用户线程产生的垃圾。 4. 筛选回收:对Region的优先级进行排序,根据停顿时间收集优先级高的Region。收集的时候采用标记复制算法进行清除,将存活的对象复制到空的Region,然后清除掉整个Region。这个过程由多条线程执行,但是需要暂停用户线程 。 :-: ![](https://img.kancloud.cn/28/24/2824d107518e82924122f5168e4cfad0_1398x394.png) **CMS和G1的选择** 未来G1必定会替代CMS,但是在小内存的服务中仍然可以使用CMS,G1会多占用20%左右的堆内存来维护G1回收器的运行。     【参考】 1. 《深入理解Java虚拟机》第3章 垃圾收集器与内存分配策略