一、垃圾回收的分类
针对HotSpot JVM的实现,它里面的GC其实准确分类只有两大种:
-
Partial GC:部分收集模式
- Young GC:只收集年轻代的GC
- Old GC:只收集老年代的GC。只有CMS中有这个模式。
- Mixed GC:收集整个年轻代以及部分老年代的GC。只有G1有这个模式
-
Full GC:收集整个堆和方法区。
堆是垃圾回收的主要区域,方法区很少会被回收。
本文所讨论的均指HotSpot JVM
二、死亡对象判断方法
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
1. 引用计数法
给每个对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 每当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
但是引用计数法很难解决对象之间循环引用的问题,因此目前主流的虚拟机中并没有选择这个算法。
2. 根可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。
哪些对象可以作为 GC Roots 呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 类静态常量引用的对象
- 常量池中被引用的对象
- 所有被同步锁持有的对象
对象被回收前如果该对象重写了finaize()方法则需先执行此方法后才能被回收。Object 类中的 finalize 方法一直被认为是一个糟糕的设计,影响了 Java 语言的安全和 GC 的性能,JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。
参考:Java基础知识点之finalize方法详解
三、引用类型分类
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
Java中将引用分为了强引用、软引用、弱引用、虚引用四种。非强引用通常用来指向某些只需要暂时缓存的数据。
1. 强引用
引用变量默认就是强引用,以下其它三种将引用通过特殊包装的才能形成其它引用。
强引用的对象在GC Roots可达时不会被回收。
2. 软引用 SoftReference
软引用是一种相对强引用弱化了一些的引用,用java.lang.ref.SoftReference实现,可以让对象豁免一些垃圾收集。在可达时,当系统内存充足的时不会被回收,系统内存不足时则会被回收。
public class SoftReferenceDemo {
public static void main(String[] args) {
Object a = new Object();
SoftReference<Object> softReference = new SoftReference<>(a);//软引用
//a和软引用指向同一个对象
System.out.println(a);//java.lang.Object@4554617c
System.out.println(softReference.get());//java.lang.Object@4554617c 10
//内存够用,软引用不会被回收
a==null;
System.gc();//内存够用不会自动gc,手动唤醒gc
System.out.println(softReference.get());//java.lang.Object@4554617c 16
//内存不够用时
try { //配置Xms和Xmx为5MB
byte[] bytes = new byte[1024*1024*30];//设置30MB超内存
} catch (Throwable e){e.printStackTrace();}
finally {
System.out.println(softReference.get());//null,被回收
}
}
}
软引用的一个应用场景:一个应用需要读取大量的本地图片,如果每次读取都从硬盘读取会严重影响性能,如果一次性全部加载到内存,内存可能会溢出。可以使用软引用解决这个问题,使用一个HashMap来保存图片路径和图片对象管理的软引用之间的映射关系,内存不足时,JVM会自动回收缓存图片对象的占用空间,有效地避免了OOM问题。
//Map<图片路径,图片对象软引用>,在系统内存不足时value所指向的对象会被回收
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>
3. 弱引用 WeakReference
弱引用需要用java.lang.ref.WeakReference实现,它比软引用的生存期更短,对于弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否够,都会回收该对象的占用内存。
Object a = new Object();
WeakReference<Object> softReference = new WeakReference<>(a);//弱引用
Map中还有一个WeakHashMap,WeakHashMap就是一种弱引用的map,内部的key为弱引用,在GC时如果key指向的对象不存在其它强引用的情况下会被回收掉,而对于value的回收会在下一次操作map时回收掉,所以WeakHashMap适合缓存处理。
4. 虚引用 PhantomReference
虚引用要通过java.lang.ref.PhantomReference类来实现,虚引用不会决定对象的生命周期,如果一个对象只有虚引用,就相当于没有引用,在任何时候都可能会被垃圾回收器回收。它不能单独使用也不能访问对象,虚引用必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。
因此总结如下,在一次GC中,用于可达性分析的GC Roots本身不会被回收,GC Roots引用链不可达对象的必然会被回收,而在引用链可达的情况中:
- 强引用的对象不会被回收
- 仅有软引用的对象在内存不足时会被回收
- 仅有弱引用或虚引用的对象必然会被回收
参考:JVM中如何理解强引用、软引用、弱引用、虚引用?
阿里面试:说说强引用、软引用、弱引用、虚引用吧
四、什么是内存泄漏
严格来说,如果某些对象在程序中不会再被用到了,但是这些对象又无法被垃圾收集器回收(GC Roots以及其引用链可达的强引用对象),那么这些对象所占用的内存就处于平白浪费的状态了,这就的内存泄漏。如果这种情况可以累积,随着内存泄漏的增多,就会导致严重的性能问题甚至OOM。
宽泛地说,实际情况中很多时候一些不太好的实践会导致对象的生命周期变得过长,比如不合理地进入了老年代,在老年代中堆积,等到Full GC时才能被回收,这种情况也可以叫“内存泄漏”。
在实际场景中可大概分为以下几种情况:
1. 类变量中引用短期对象
类变量在垃圾回收时被作为GC Roots,而类变量的生命周期一般和JVM程序一致,只有方法区中的对应类被回收才有可能被回收。如果在类变量对象中引用很多短期内使用的对象,那么由于在GC Roots下被强引用,这些短期对象都得不到回收,就造成了内存泄漏。如下一个静态list的例子:
public class test {
static List list = new ArrayList<>();
//如果object只是短期内需要使用的对象,那么如果这个方法一直被调用,就会造成内存泄漏
public void oomTest() {
Object object = new Object();
list.add(object);
}
}
2. 各类连接泄漏
例如数据库连接。如果在获取数据库连接后没有正确的归还或关闭,导致每次访问都创建一个未关闭的连接,就导致了内存泄漏。
案例:多线程访问数据库导致内存泄露的优化过程
数据库连接池内存泄漏问题的分析和解决方案
3. 改变哈希集合关键字的hash值
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
因为String是不可变类型,我们可以放心地把String 存入HashSet,或者把String当做HashMap的key值。
4. 缓存泄漏
内存泄漏的一个常见来源是缓存,一旦你把对象引用放入到缓存中,就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap(弱引用)代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
参考:https://blog.csdn.net/weixin_43899792/article/details/124304136
五、垃圾收集算法
分代收集理论
现代虚拟机的垃圾收集器大多都遵循了分代收集理论,这个理论建立在三个经验假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构,这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
以下简单讲解下三种垃圾回收方法论
1. 标记 - 清除算法
最早出现也是最基础的垃圾收集算法是标记 - 清除算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收清除掉所有被标记的对象。也可以反过来。
缺点:
执行效率不稳定,如果堆中有大量对象需要回收就必须进行大量的标记与清除动作。
会导致内存空间碎片化,创建对象时需要耗费资源寻找合适的空闲空间,并且创建较大对象时可能找不到足够的连续内存。
2. 标记 - 复制算法
标记 - 复制算法将要回收的空间分为几个区域,注意不要被标记复制的名字忽悠了,标记清除算法中分为标记和清除两个阶段,但是标记复制算法并没有标记阶段,为什么呢?首先要明确判断对象是否存活的核心思想是用根可达算法找出存活对象,由于标记清除算法需要回收垃圾对象,所以需要对存活对象进行标记,然后清除不可用对象。而复制算法是要复制存活对象到另一块区域,所以在根可达算法发现存活对象后是直接复制到另一块区域,即在根可达分析过程中就已经完成了筛选(复制),待复制完成后,直接清理掉另一块区域即可,所以不需要挨个标记然后去清除。
优点:执行效率较高,且不会产生内存碎片
缺点:内存空间不能充分利用,需要保留一块区域用于下一次垃圾回收时复制存活对象。
Hotspot JVM的堆空间中年轻代的垃圾回收就是基于这种算法,年轻代划分为一个Eden区和两个Survivor区,内存比例默认为8:1:1,一个Survivor区保留,其余可使用,垃圾回收时只需要将通过根可达分析确认存活的对象复制到保留的Survivor区。但是不是每次都只有10%的对象存活,因此需要老年代做分配担保,如果Survivor区空间不足,则将一部分对象直接晋升老年代。
3. 标记 - 整理算法
标记 - 复制算法除了不能充分利用空间,且对象存活率高时需要进行较多的复制操作,因此在存活率较高的老年代就不能采用这种算法。
针对老年代存活率高的特性,有人提出了标记 - 整理算法,就是在标记 - 清除算法的基础上,将剩余存活对象向一端进行整理。
六、经典垃圾收集器
如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。
1. Serial / Serial Old / ParNew 收集器
Serial收集器:单线程收集器,并且在进行垃圾收集工作时必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。采用标记复制算法。
Serial Old 收集器:Serial收集器的老年代版本,采用标记整理算法。
ParNew收集器:Serial收集器的多线程版本,除了使用多线程进行垃圾回收外其他与Serial一样
2. Parallel Scavenge / Parallel Old 收集器
Java8的默认垃圾收集器组合
Parallel Scavenge收集器:也是使用标记-复制算法的多线程收集器,关注点是吞吐量(高效率的利用 CPU),提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。
Parallel Old 收集器:Parallel Scavenge的老年代版本。使用多线程和“标记-整理”算法
3. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
并且是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
4. G1 收集器
Java9后的默认垃圾收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。
参考:JavaGuide