【JVM】垃圾回收
1. 如何判断对象可以回收
1.1 引用计数法

A对象引用了B对象,B对象的引用计数为1;B对象引用了A,所以A对象的引用计数也为1。它们俩循环引用。
但是这两个对象没有其他对象引用它们,虽然它们俩都不会被使用了,但是它们的引用计数不能归零,所以不能被垃圾回收。这样就可能导致内存泄露。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
1.2 可达性分析算法
在垃圾回收前,对堆内存中的所有对象进行一遍扫描,看有没有对象被“根对象”(肯定不能当作垃圾被回收的对象)直接或间接地引用。
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着
GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
哪些对象可以作为 GC Root ?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
1.3 四种引用
- 强引用
- 软引用
- 弱引用
- 虚引用
- 终结器引用

1.3.1 强引用(StrongReference)
我们使用的大部分引用都是强引用,这是使用最普遍的引用。一个对象具有强引用,就像必不可少的生活用品那么就不会被垃圾回收。当内存空间不足时,java虚拟机宁愿抛出 OutOfMemoryError 错误使程序异常终止,也不会回收具有强引用的对象来解决内存不足的问题。
1.3.2 软引用(SoftReference)
如果一个对象只具有软引用,就像可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
1.3.3 弱引用(WeakReference)
如果一个对象只具有弱引用,就像可有可无的生活用品。
弱引用和软引用的区别在于:
- 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 不过,垃圾回收器是一个优先级很低的线程,因此不一定会很快发现哪些只具有弱引用的对象。
- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
1.3.4 虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
1.3.5 终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
1.4 应用
1.4.1演示强引用
演示强引用 -Xmx20m -XX:+PrintGCDetails -verbose:gc
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
//soft();
}
设置堆内存大小为20M,运行程序,报了一个 OutOfMemoryError 堆内存空间不足的错误。这是因为使用了强引用,导致对象不能被垃圾回收。
1.4.2 演示软引用
演示软引用 -Xmx20m -XX:+PrintGCDetails -verbose:gc
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
soft();
}
public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u1Y63IbS-1677686677517)(https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/17403/image-20230228000725553.png)]

2. 垃圾回收算法
2.1 标记-清除算法(Mark Sweep)
该算法分为”标记“和”清除“两个阶段。首先标记处所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
它是最基础的回收算法,他有两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
优点:速度快
缺点:造成空间不连续,产生内存碎片。

2.2 标记-整理算法(Mark Compact)
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
优点:没有内存碎片
缺点:速度慢

2.3 标记-复制算法(Copy)
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
优点:不会有内存碎片
缺点:需要占用双倍内存空间
复制前:

复制后:

将FROM清空并交换FROM和TO:

3. 分代垃圾回收
堆内存划分为两块,一个叫新生代,一个叫老年代。这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
新生代:用完即丢的对象放在新生代
老年代:长时间使用的对象放在老年代
比如在新生代中,每次回收都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发
minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加1并且交换from和to minor gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足时,会先尝试触发
minor gc,如果只会空间仍不足,那么触发full gc,stw的时间更长,如果还是空间不足,那么就会触发堆内存溢出错误。
3.1 相关 VM 参数
| 含义 | 参数 |
|---|---|
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
| 新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
| 幸存区比例 | -XX:SurvivorRatio=ratio |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | XX:+PrintTenuringDistribution |
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
4. 垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,STW 的时间最短,垃圾回收时间占比最低,这样就称吞吐量高
- 响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次 STW 的时间最短
4.1 串行(Serial)回收器
Serial(串行)回收器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾回收工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它回收结束。
开启参数:-XX:+UseSerialGC = Serial + SerialOld
新生代采用标记-复制算法,老年代采用标记-整理算法。

4.2 吞吐量优先(Parallel Scavenge)回收器
这是java8默认的回收器。
关注点是吞吐量(高效率的利用 CPU)。 CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。 所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC **新生代采用标记-复制算法,老年代采用标记-整理算法。**上面的只要开启其中一个,另一个就会自动开启。
-XX:+UseAdaptiveSizePolicy 采用自适应调整新生代大小的策略
-XX:GCTimeRatio=ratio 调整吞吐量目标,没达到就会想方法靠近这个目标(提高吞吐量就要扩大堆内存)
-XX:MaxGCPauseMillis=ms 最大暂停毫秒数,和上面的吞吐量目标冲突(减少最大暂停毫秒数就要减小堆内存),取一个折中
-XX:ParallelGCThreads=n控制ParalleGC线程的线程数
文章介绍了JVM中垃圾回收的基本原理,包括如何判断对象可以回收(引用计数法和可达性分析)、四种引用类型(强引用、软引用、弱引用、虚引用)以及垃圾回收算法(标记-清除、标记-整理、标记-复制)。此外,还讨论了分代垃圾回收的概念和不同垃圾回收器(如串行回收器和吞吐量优先回收器)的特点。
1147

被折叠的 条评论
为什么被折叠?



