JVM垃圾回收(GC)
什么是垃圾
- 没有引用的对象
- 相互引用,但没有其他引用的对象
- 环形指向的对象
如何确认垃圾
-
引用计数法(java未使用)
在对象中添加一个应用计数器,有地方应用该对象计数器+1,应用失效计数器-1,为0时回收。优点:实现简单,判断效率高。
缺点:无法处理循环引用的垃圾。 -
正向可达法查找非垃圾对象
-
描述:从roots对象开始,直接或间接关联到的对象就是非垃圾对象,否则,就是垃圾对象。
-
什么样的对象是roots对象?
虚拟机栈中引用的对象;
方法运行时,方法中引用的对象;
类的静态变量引用的对象;
类中常量引用的对象;
Native方法中引用的对象;
被synchronized持有的对象;
基本数据类型的class对象,常驻的异常对象,系统类加载器。 -
使用JProfiler工具查找roots对象
测试代码:public class GCRoot { public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i=0; i<100; i++) { list.add(i+""); } System.out.println("数据添加完毕!"); //用于阻塞程序,便于测试 new Scanner(System.in).next(); } }
安装JProfiler和idea中JProfiler插件(自行百度),执行
由上述步骤可追溯到roots对象。
垃圾回收算法
各类算法
- 标记-清除算法(Mark-Sweep)
-
描述:
最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。 -
图示:
-
优缺点
优点:实现简单,不需要其他额外操作。
缺点:效率不高,清理出的空间是不连续的。 -
注意:清除的时候并不是真正的清除,而是将要清除的空间地址保存到空闲列表,当使用这些空间时才真正清除。
- 复制算法(Copying)
-
描述:
将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。 -
图示:
-
优缺点
优点:速度快,运行效率高。
缺点:浪费空间,总有一半的空间是空闲的,当存活的对象多时,复制的开销大,效率会降低。
- 标记-压缩算法(Mark-Compact)
-
描述:
标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。 -
图示:
-
优缺点
优点:相较标记-清除算法,不会产生碎片化的空间,消除了复制算法中浪费内存的缺点。
缺点:相较复制算法,效率较低,移动过程中,会全程暂停用户应用程序,即STW(stop the world)。
分代算法
jvm根据每个区域的特点在不同的区域采用的不同的垃圾回收算法,新生代存活对象少,回收频率高,占用空间不大,采用复制算法;老年代存活对象多,垃圾少,空间大,标记-清除算法和标记-整理算法的混合使用。
垃圾回收过程
垃圾收集器
评估GC的性能指标
- 吞吐量:运行用户程序的时间占总运行时间(程序运行时间+内存回收时间)的比例,越大越好。
- 暂停时间:垃圾回收时,工作线程暂停的时间(STW),越小越好。
- 内存占用:堆区所占内存大小。
垃圾收集器的组合使用关系
JDK1.8默认使用parallel。
垃圾收集器分类
-
serial垃圾收集器(串行)
最基本、发展最悠久;
单线程垃圾收集器。
单核服务器,最小化内存和并行的开销,可选择此收集器。
-
ParNew收集器(并行)
serial收集器的改版,串行改为并行,多个GC进程同时工作。
-
Parallel Scavenge收集器(并行)
多线程垃圾收集器;
主要控制吞吐量。
-
CMS垃圾收集器(并发标记扫描)
采用标记-清除算法。
优点:并发收集,低延时,用户体验好。
缺点:- 占用大量cpu资源,降低吞吐量;
- 采用标记-清除算法(在清理阶段需要和用户线程并发执行,无法进行内存的移动,所有无法使用标记-压缩算法),会产生空间碎片,提前引起FULL GC,容易导致内存不足,出现Concurrent Mode Failure,此时虚拟机采用后备方案,临时采用serial收集器,单线程收集器,且STW时间长,所以,基本就废了;
- 因为在并发标记阶段是和用户线程并发执行的,边产生垃圾边收集垃圾,此阶段产生的垃圾就无法被标记(重新标记阶段不会标记此并发阶段产生的垃圾),就会产生浮动的垃圾,无法及时被回收。
-
G1收集器
优势:-
并发与并行混合
-
分代收集
将内存划分成小的区域。
-
空间整合
整体使用标记-压缩算法,而每个region内部,使用的则是复制算法,都可以避免空间碎片化。 -
可预测的停顿
可以让使用者明确指定在一个长度为M毫秒的时间端内,垃圾收集的时间不超过N毫秒。
由于分区的原因,缩小了垃圾收集时的范围,使得STW得到较好的控制。
G1会在后台维护一个保存各个region的回收价值(可回收垃圾多的,回收价值就大)的列表,回收时,优先选择收集价值较大的进行回收。
缺点:
因为分代收集,区分了大量的区域,所以产生的额外的内存和负载较多。问题:
分区的设计,会放大一个问题,在垃圾回收时是按region进行回收的,但是这个region中的对象有可能被其他各代中的region所引用,那么是不是回收时,需要遍历所有的堆空间?解决:
无论是G1还是其他垃圾收集器,都使用记忆集
来避免全局扫描,每个region都对应一个记忆集,当引用类型的数据执行写操作时,就会产生一个写屏障
,暂时中断操作,检查要写入的引用是否和当前引用在同一个region(其他收集器检查是否在同一个代),如果不在,则把当前引用的相关信息记录到所指向引用的记忆集里。当垃圾回收时,通过记忆集中的信息进行追溯。
-
-
总结
GC日志分析
-XX:+PrintGCDetails 打印日志分析
使用工具分析GC日志
编写代码:
public class GCLog {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
for (int i=0; i<500; i++) {
list.add(new byte[1024*100]);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
设置jvm参数:-Xms60m -Xmx60m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -Xloggc:./logs/gc.log
-Xloggc表示将gc日志输出到工程根目录/logs文件里,注意,要提前手动创建logs文件夹。
运行完程序,将输出的log文件导入到gceasy在线工具进行分析gceasy。
内存泄漏
- 定义:
狭义上来说,只有对象不被引用到,而且GC又回收不了的情况,才叫内存泄漏,但是广义上来讲,因某些操作,导致对象的生命周期过长,也叫广义上的内存泄露。
- 举例:
- 在单例模式中引用了外部对象,由于单例对象的生命周期较长,导致其引用的对象生命周期也变长,短时间无法被GC,造成了广义上的内存泄露。
- 一些提供close的资源未收到关闭,如数据库、socket和IO链接,也会造成广义上的内存泄露。
STW(Stop The World)
所有的垃圾收集器在执行GC时,都会造成STW,暂停用户应用进程,为了在垃圾回收时保证数据的一致性,所以开发中尽量减少STW。
调用System.gc()会触发full GC,导致STW。
4中引用状态
- 强引用
代码中普遍存在的类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 - 软引用
有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用。 - 弱引用
非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。 - 虚引用
这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。