前言
Garbage Collection,GC
GC只在堆和本地方法区中起作用。
一、确定对象存活
1.1 引用计数法
在对象中添加一个引用计数器,每当有一个地方引用该对象的时候,计数器的值就加1;当引用失效的时候计数器的值就减1;计数器的值为0的对象就是不再被使用的对象。python就是使用这种方式管理内存的。
这种方式无法解决循环引用的问题,Java中不使用这种方式判断一个对象是否存活。
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】
使用Weak Reference类表示
。
在弱引用和软引用中有个一般会关联一个引用队列,当引用对象被清理时,弱引用和软引用本身就会被放到引用队列中,等待被Reference Handler线程清理。可以不配合引用队列使用。
2.4 虚引用
引用直接内存对象的引用,Cleaner。会关联一个引用队列。
记录直接内存地址,当放到引用队列中时会被一个定时调用的线程调用Unsafe.freeMemory方法清理直接内存。
💡对象真正死亡时间!
一个对象就算没有被GC Roots的链所引用,也不一定会被清除;会经历两次标记过程:
1. 第一次筛选看是否有必要执行finalize()方法,如果对象没有覆盖finalize()方法或者该方法已经被调用过了,则都会认为没有必要执行。如果对象被认为有必要执行,则会被放入一个F-Queue队列中,有一条低优先级的Finalizer线程执行它们的finalize()方法,不一定会等方法执行完毕。
2. 第二次再对F-Queue队列进行筛选,如果对象还是没有被任何GC Roots链所引用,就会真正被清除掉。
finalize()方法只会被执行一次。这种在finalize()方法中重新引用的方式也被称为终结器引用。
三、垃圾回收算法
大多数的垃圾回收算法都是基于“分代收集”理论的(因此要注意在Java运行时数据区其实并没有分代的概念的,分代的概念是用在垃圾回收当中的)。
分代假说:
弱分代假说:绝大数对象都是朝生夕灭的。 --> 引申出新生代的概念。
强分代假说:熬过越多次垃圾回收的对象越难消除。 --> 引申出老年代的概念。
跨分代假说:跨代引用相对于同代引用来说是非常少的。
注意:将堆划分出不同的区域之后,才有了"Minor GC",“Major GC”,"Full GC"这些不同称呼的垃圾回收器和”标记-复制算法“,”标记-清除算法“,”标记-整理算法“这些针对不同对象生存情况的算法。
垃圾回收器的分类
-
部分收集Partial GC:
-
新生代收集 MinorGC:针对新生代的垃圾收集。
-
老年代收集 MajorGC:针对老年代的垃圾收集。–> CMS收集器
-
混合收集MixedGC:针对整个新生代的垃圾收集以及部分老年代的垃圾收集。–> G1收集器。
-
-
整堆收集 Full GC:收集整个堆和方法区的垃圾收集器。
3.1 算法
3.1.1 标记清除算法
标记阶段:首先标记出所有需要回收的对象;
清除阶段:标记结束之后统一清除要回收的对象。
优点:不需要额外的内存空间。
缺点:
-
两次扫描,当堆中有大量的要回收的对象的时候会严重浪费时间。
-
产生内存碎片,当大对象无法找到足够的内存空间的时候又不得不触发另外一次垃圾回收。
3.1.2 标记复制算法
简称为复制算法,解决标记清除算法中回收大量对象时慢的缺点。一开始提出是将内存容量分为两块大小一致的空间,每次只使用其中一块,当一块空间用完了,就将存活的对象复制到另外一块上去,清空掉原来的那块空间。
这种方式每次都对一整块空间操作,不用考虑内存碎片的问题,也比较快。
在后来用到新生代的时候,将新生代分为Eden空间和两块较小的Survivor空间(比例为8:1:1)。每次在使用的时候只使用Eden区和一块Survivor的空间(from区),在发生垃圾回收的时候将Eden去和From区的存活对象放入另外一块Survivor区中(to区),接着将Eden区和From区都清空掉。这种划分方法是基于对象都是“朝生夕灭”的,只用了额外的10%的空间。这个空间的比例可以通过虚拟机参数调整:
-XX:InitialSurvivorRatio=ratio
PS:当TO区不够存放存活的对象的时候,会通过一种将分配担保的机制将这些对象放入老年代。
3.1.3 标记整理算法
上面两种算法更多针对新生代的,而标记整理算法更多针对老年代的。标记整理算法前期过程与标记清除算法类似,但是在标记完成之后会将所有存活的对象移动到一遍,然后清空掉另外一边的内存。
这种移动对象的方式需要暂停应用程序的运行(Stop The World)。
3.2 垃圾收集器
3.2.1 Serial收集器
作用于新生代,Serial Old作用于老年代;单线程,在进行垃圾回收的时候需要暂停其他用户线程(STW)。
Serial收集器虽然是单线程的,但是仍然是HotSpot在客户端下的默认新生代的垃圾回收器,特别适合在微服务这种架构下单个服务占用的内存很少,即是使用单线程收集也不会占用太多时间。随之而来的好处就是简单,高效。
3.2.2 ParNew收集器
是Serial的多线程版本,在新生代的垃圾回收中可以使用多个线程进行垃圾回收,但是仍然会造成STW。
除了Serial外,只有ParNew能和CMS配合使用,多用于服务端的垃圾回收。在JDK9之前ParNew + CMS的组合是官方推荐的在服务端模式下的收集器的解决方案;在JDK9之后ParNew只能和CMS组合使用了。
3.2.3 Parallel Scavenge收集器
新生代收集器
,与上面两个一样基于标记-复制算法,也是能够并行的多线程收集器。该收集器更多的关注的是吞吐量优先,也被称为“吞吐量优先收集器”。
Parallel Scavenge Old基于标记整理算法的作用于老年代的收集器。
3.2.4 CMS收集器
Concurrent Mark Sweep,基于标记清除算法的用于老年代的收集器,同时也是一款以获取最短回收停顿时间为目标的收集器。
其包含的过程为:
- 初始标记
需要STW
仅仅标记一下GC Roots能直接关联的对象,耗时非常快。
- 并发标记
从GC Roots直接遍历整个对象图,这个过程比较耗时,但是不用暂停用户线程;
- 重新标记
需要STW
重新标记是为了再一次标记那些在并发标记过程中用户线程运行可能产生的垃圾,比初始标记耗时相对高一点点。
- 并发清除
清除标记出来的对象,不需要移动对象,可以和用户线程一起运行。
CMS收集器可以实现并发低停顿。
【参考】
- 《深入理解Java虚拟机》第3章 垃圾收集器与内存分配策略