JVM垃圾回收
🤚我的博客
- 欢迎光临我的博客:
https://blog.youkuaiyun.com/qq_52434217?type=blog
📖JVM垃圾回收
Java运行时的对象都存放在JVM的堆内存中,JVM的自动内存管理就是对堆中的对象进行内存分配和内存回收。由于堆内存是垃圾收集器管理的主要区域,所以也被成为GC堆。
为了对堆内存进行高效的垃圾回收,垃圾收集器采用了分代垃圾收集的算法。在新生代就有Minor GC,老生代有Major GC。还有一个对整个堆内存(新生代、老生代、元空间)进行垃圾收集的Full GC。这里回忆一个知识点,JDK1.8后元空间取代了以前的永久代,且元空间使用的是直接内存。
💠判断对象是否能回收
引用计数法
所谓引用计数法,就是在对象被引用时,其引用数就加一,如果引用失效就减一。当计数器为0时就回收这个对象。
但是这个方法无法解决对象循环引用问题,即A对象调用了B,B对象调用了A。此时两个对象的计数器由于互相调用都无法变成0,无法被GC回收。只能手动释放内存。
可达性分析
可达性分析的基本思想是通过一个GC Root
对象作为起点,从这个根节点向下搜索,节点所走过的路径被称为引用链。如果对象能够通过GC Root的引用链到达,则说明该对象可达而不用被回收。反之则需要被GC回收。
在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
一般来说可以作为GC Root的对象有栈中正在引用的对象,静态属性应用的对象和方法区常量引用的对象。
可以作为GC Roots的对象有:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
💠引用类型
java中有5种引用类型,分别为:强引用、软引用、弱引用、虚引用和终结器引用,引用强度逐渐减弱。本文在说明时,特意区分了被引用对象和引用变量。但被引用对象与引用变量的关系就是说过的5种关系,如果说X引用被垃圾回收,就是指被引用对象被回收。因为前面章节我们知道,被引用对象在堆区,变量在栈的局部变量表中,而GC只会对堆区内存进行。
强引用:就是常用的声明变量的方式的一种引用,即
Person p = new Person();
被GC Roots强引用的对象不会被垃圾回收掉,即使JVM会抛出OutOfMemory
错误也不会回收掉强引用对象。如果想要中断强引用和对象之间的关联,可以显示的将引用赋值为null
,即p = null;
。
软引用:被软引用的对象由SoftReference
类创建。即
SoftReference<bety[]> ref = new SoftReference<>(new bety[1024*1024*10])
- 这里的
new bety[1024*1024*10]
是被软引用的对象。被软引用的对象只会在内存不足时被回收,也就是当内存足够时不会对软引用类型进行回收。软引用可用来实现内存敏感的高速缓存。软引用可以和引用队列联合使用,如果被软引用的对象被垃圾回收,JVM会把这个软引用变量ref
加入到与之关联的引用队列中,然后扫描引用队列中变量的状态,如果没有被再次使用,则会被GC。
弱引用:被软引用的对象使用WeakReference
类创建。即
WeakReference<bety[]> ref = new WeakReference<>(new bety[1024*1024*10])
- 被
WeakReference
对象引用的对象被称为弱引用对象。与软引用不同的是,当JVM进行垃圾回收时,无论内存是否足够,弱引用都会被回收掉。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - 弱引用常用于实现缓存,特别是当希望缓存项能够在内存压力下自动释放时。
- 弱引用可以当做对象池。弱引用常用于实现缓存,特别是当希望缓存项能够在内存压力下自动释放时。
- 弱引用可以防止该对象被意外地保留,从而避免潜在的内存泄露。
虚引用:被虚引用的对象使用WeakReference
类创建。即
//引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
//创建虚引用对象的同时把该对象放入引用队列
PhantomReference<byte[]> m1 = new PhantomReference<>(new byte[1024*1024*10],queue);
虚引用必须和引用队列(ReferenceQueue)联合使用。虚引用唯一的作用就是对垃圾回收过程进行跟踪,如果一个对象要被垃圾回收了,会向jvm发送一个通知,让jvm做其他事情。
💠如何判断一个类是无用的类
- 该类的所有实例都被回收
- 加载该类的
ClassLoader
已经被回收 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射的方式访问该类的方法
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
💠内存泄漏和内存溢出的理解
原因
内存泄露:内存泄漏是指在程序运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收。如下面的obj
对象。
class MemoryLeakExample {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
Object obj = new Object();
list.add(obj);
// 让对象obj被其他对象引用,导致无法被GC回收
obj = list.get(0);
}
}
}
- 使用静态数据结构如
HashMap
或ArrayList
存储对象,且未清理(静态数据的生命周期随整个应用的生命周期)。 - 未及时取消的事件源的监听,导致对象持续被引用
- 未停止的线程可能持有对象引用而未被收回(线程不安全)
内存溢出:内存溢出是指在JVM申请内存时无法找到足够的内存空间,最终导致OutOfMemoryError
错误,一般出现在堆内存不足以创建新的对象时。
- 大量创建对象可能会超过JVM堆内存大小而导致内存溢出
- 大型数据结构长时间持有对象引用而没有导致内存堆积而造成内存溢出
- 无限递归调用导致栈内存溢出
- 元空间或永久代的内存溢出
- 直接内存的内存溢出
内存泄露解决方法
- 尽量减少静态变量的使用,因为静态变量的生命周期伴随着整个应用的生命周期,除非该类为无用类。
- 如果使用单例,尽量采用懒加载
- 打开资源后要在
finally
语句中关闭资源,如文件流、数据库链接、session对象等。或者尽量使用try-with-resources
的方式打开(类似于python的with
语句)
try(
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
){
channel.queueDeclare(QUEUE_NAME,true,false,false);
}catch(Exception e){
e.printStackTrace();
}
- 使用ThreadLocal保证线程安全,使用
ThreadLocal#remove
函数关闭资源。
try {
threadLocal.set(System.nanoTime());
}finally{
threadLocal.remove();
}
💠垃圾回收算法
标记清除
标记清除算法分为标记和清除两阶段。JVM首先经过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。
- 效率问题:标记和清除两个过程效率都不高。
- 空间问题:会造成大量的碎片化内存空间,使得后续需要连续空间(数组内存)时无法申请到内存而引发一次GC。
复制
复制是为了解决标记-清除算法的效率和碎片化内存问题而出现的。它将内存分为两个大小相同的内存区域,每次使用一块区域。当from
区域使用完之后,就将存活的对象复制到to
区域中,然后再把原来的from
区域中的内存清理掉。这样就只对内存区域的一半进行回收。
- 可用内存变小:可用内存缩小为原来的一半。
- 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
标记整理
标记整理基于标记清除算法,整理是指将所有存活的对象都移动到内存连续的一段从而去除碎片化的内存空间。标记之后,JVM会将对象移动到一段,然后清理端边界之外的内存。
- 效率比整理标记更低,所以比较适合老年代这种频率垃圾收集频率不高的场景。
分代回收算法
- 对象首先分配在Eden区
- 新生代空间不足时,触发一次Minor GC,Eden和from存活的对象会由JVM使用复制算法复制到to中,如果幸存区无法放下则提前进入到老年代中。存活下来的对象年龄加一。
- Minor GC会引发一次stop the world,即暂停其他用户线程,等待垃圾回收结束,用户线程恢复运行。
- 当对象寿命超过阈值时,会晋升到老年代。最大寿命为15(4bit)。
- 当老年代空间不足,会尝试触发一次Minor GC。如果空间仍不足,会触发Full GC,STW时间会变得更长。由于老年代没有多余的空间进行分配担保,所以老年代的垃圾收集只能是“标记清除”或者“标记整理”。
💠垃圾收集器
Serial收集器与Serial Old收集器
Serial(串行)收集器是单线程收集器,是最基本、历史最悠久的垃圾收集器。串行收集器采用单线程进行垃圾收集,并且它在收集垃圾的过程中会暂停其他线程的工作(Stop The World),直到它收集结束。
Serial Old收集器主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
新生代的串行收集器采用复制算法,老年代采用标记-整理算法
- 串行收集器简单高效,没有与线程交互的开销,可以获得很高的单线程收集效率
- 但是串行收集器在垃圾收集时停顿时间会给用户造成不良的体验
ParNew收集器
新生代并行收集器,该收集器是串行收集器的多线程版本,其余行为与串行收集器完全一样。
Parallel Scavenge收集器与Parallel Old 收集器
Parallel Scavenge 收集器称为吞吐量优先收集器,因为它的关注点是吞吐量,其中吞吐量的计算公式如下。
吞吐量
=
用户线程时间
用户线程时间
+
G
C
线程时间
吞吐量 = \frac{用户线程时间}{用户线程时间+GC线程时间}
吞吐量=用户线程时间+GC线程时间用户线程时间
吞吐量和最大停顿时间都可以通过命令行参数进行设置。设置吞吐量时,系统会调整堆内存的大小以适应垃圾收集所消耗的时间。
Parallel Scavenge 收集器是新生代收集器,是使用复制算法的多线程收集器。Parallel Scavenge收集器提供了很多参数给用户寻找最合适的停顿时间或者最大吞吐量。Parallel Old收集器是老年代收集器,采用标记-整理算法进行收集。
CMS收集器
CMS(Concurrent Mark Sweep)收集器关注于响应时间,是一种以获取最短回收停顿时间为目标的收集器。适合注重用户体验的应用场景。CMS 收集器基于 “标记-清除”算法 实现,同时CMS是一个并发收集器,实现了垃圾收集线程与用户线程并发工作。CMS一般工作在老年代内存区,可以配合新生代的Serial和ParNew收集器一起使用。
CMS主要分为4个步骤。
- 初始标记:初始标记时值标记GC Root对象,速度相对较快。标记时,会暂停所有用户线程的运行。
- 并发标记:并发标记时开启GC线程和用户线程,用一个闭包结构去记录可达对象。但是这个阶段结束后并无法保证闭包包含当前所有的可达对象。因为用户线程可能会不断更新引用域。所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记:重新标记是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。发生变动的那一部分垃圾被称为浮动垃圾。
- 并发清除:并发清除时会开启用户线程,同时GC线程开始对未标记的区域做清扫。
- 该收集器的优点是并发收集和低停顿
- 但是对CPU资源敏感
- 无法处理浮动垃圾
- 使用标记-清除算法,会产生大量碎片。
- 如果在清理过程中预留给用户线程的内存不足就会出现’Concurrent Mode Failure’一旦出现此错误时便会切换到Serial Old收集方式。
G1收集器
Garbage First又被称为 G-One收集器,是一个面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以及高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特点。G1收集器基于标记-整理算法实现,不会产生内存碎片。另外,G1的回收范围是Java整个堆内存,而前几种收集器的收集范围仅限于新生代或者老生代。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记:标记GC Roots对象,暂停用户线程
- 并发标记:利用可达性算法无法达到的垃圾对象
- 最终标记:暂停用户线程,在设定收集之间内标记垃圾对象,并挑出回收价值较高的区域
- 筛选回收:对回收价值较高的区域进行垃圾回收。
- 并行与并发:G1 能充分利用 多CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 STW 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
💠常用指令总结
堆内存
- 显示指定堆内存
-Xms512k # 指定堆内存大小最小为512KB
-Xmx5G # 指定堆内存大小最大为5GB
- 显示指定新生代内存
-XX:NewSize=512m # 指定新生代大小最小为512MB
-XX:NewMaxSize=1024m # 指定新生代大小最大为1024MB
-Xmn256m # 指定新生代大小最小和最大都为256MB
-XX:NewRatio=1 # 指定新生代与老生代的大小比例为1:1
- 显示指定永久代大小
-XX:PermSize=N # 方法区 (永久代) 初始大小
-XX:MaxPermSize=N # 方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError
- 显示指定元空间大小
-XX:MetaspaceSize=N # 设置 Metaspace 的初始大小(是一个常见的误区,后面会解释)
-XX:MaxMetaspaceSize=N # 设置 Metaspace 的最大大小
垃圾收集器
- 指定垃圾收集器的类型
-XX:+UseSerialGC # 指定收集器为串行垃圾收集
-XX:+UseParallelGC # 指定收集器为并行垃圾收集
-XX:+UseConcMarkSweepGC # 指定收集器为CMS收集器
-XX:+UseG1GC # 指定收集器为G1收集器
- GC日志记录
-XX:+PrintGCDetails # 打印基本 GC 信息
-XX:+PrintGCDateStamps # 打印基本 GC 时间戳
-XX:+PrintTenuringDistribution # 打印对象分布
-XX:+PrintHeapAtGC # 打印堆数据
-XX:+PrintReferenceGC #打印Reference处理信息
-XX:+PrintGCApplicationStoppedTime # 打印STW时间
-XX:+PrintSafepointStatistics # 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:PrintSafepointStatisticsCount=1
-Xloggc:/path/to/gc-%t.log # GC日志输出的文件路径
-XX:+UseGCLogFileRotation # 开启日志文件分割
-XX:NumberOfGCLogFiles=14 # 最多分割几个文件,超过之后从头文件开始写
-XX:GCLogFileSize=50M # 每个文件上限大小,超过就触发分割
📖参考
JVM垃圾回收详解(重点) | https://javaguide.cn/java/jvm/jvm-garbage-collection.html
JVM | https://www.javalearn.cn/#/doc/JVM/%E9%9D%A2%E8%AF%95%E9%A2%98
🔷END
公众号
欢迎关注小夜的公众号,一个什么都想学的研究生。