文章目录
JVM学习笔记:垃圾回收
Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。Java中的垃圾收集主要针对堆和方法区进行;程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要堆这三个区域进行垃圾回收。Java自动内存管理最核心的功能是堆内存中对象的分配和回收。Java堆事垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)
判断一个对象是否可以被回收
1.引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器减1。引用计数为0的对象可以被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为0,导致无法对它们进行回收。
由于循环引用的存在,Java虚拟机不使用引用技术算法。
public class ReferenceCountingGC {
//声明一个Object类型的变量instance
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
//把objectA的实例变量引用objectB
objectA.instance = objectB;
//把objectB的实例变量引用objectA
objectB.instance = objectA;
}
}
objectA和objectB形成了循环引用。如果使用引用计数法,objectA和objectB永远不会被垃圾回收
2.可达性分析算法
扫描堆中的对象,看是否能够沿着GC ROOT对象为起点的引用链找到该对象,能够达到的对象都是存活的,不可达的对象可被回收。
Java虚拟机使用该算法来判断对象是否可被回收,在Java中GC ROOT一般包含以下内容:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常亮引用的对象
3.四种引用类型
无论是通过引用计数法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java具有四种强度不同的引用类型。
1强引用
被强引用关联的对象不会被回收。
使用new一个新对象的方式来创建强引用。
Object obj = new Object();
2软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用SoftReference类来创建软引用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
3弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
使用WeekReference类来实现弱引用
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4虚引用
又被称为幽灵引用或者幻影引用。一个对象是否虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
使用PhantomReference来实现虚引用
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
垃圾回收算法
标记清除
将存活的对象进行标记,然后清理掉未被标记的对象。
不足:
- 标记和清除过程效率都不高
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存
标记整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
标记复制
将内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
不足之处:只使用了内存的一半
分代收集
- 对象首先分配再伊甸园区域
- 新时代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from to
- minor gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长
垃圾回收器
以上是HotSpot虚拟机中的7个垃圾收集器,连线表示垃圾收集器可以配合使用
- 单线程与多线程:单线程指的是垃圾收集器只使用了一个线程进行收集,而多线程使用多个线程
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了CMS和G1之外,其他垃圾收集器都是以串行的方式执行。
Serial收集器
Serial(串行)收集器是一个单线程收集器。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(“stop the world”),直到它收集结束
stop the world会带来不良用户体验,但是串行收集器也有优于其他收集器的地方:简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,可以获得高的单线程收集效率。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样
并行地进行垃圾收集
并行和并发概念补充:
- 并行:只多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发:指用户线程与垃圾收集线程同时执行(不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上
新生代采用标记复制算法,老年代使用标记整理算法
Parallel Scavenge收集器
Parallel Scavenge收集器也是使用标记-复制算法的多线程收集器。
与ParNew收集器相比的特别之处:
-XX:+UseParallelGC
使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
使用 Parallel 收集器+ 老年代并行
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU),所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值,即尽可能多运行用户代码
Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
Parallel Old收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS收集器是一种“标记-清除”算法实现的
CMS收集器的运行过程:
- 初始标记:暂停所有其他线程,并记录下直接与root相连的对象,速度很快。
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包并不能保证包含当前所有的可达对象。因为用户线程可能会不断更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段时间一般还会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。
标记的是可达对象
三个明显的缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生
G1收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。
这些Region包括Eden、Survivor、Old
这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
G1收集器的运作大致可以分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
卡表是一个用于加速跟踪对象引用变化的数据结构。在G1的上下文中,卡表主要用于追踪从一个区域指向另一个区域的引用变化。
维护卡表: 对于每个老年代区域,G1维护一个与之关联的卡表。这个卡表是一个位图,用于记录该老年代区域与其他区域之间的引用关系。
卡槽(Card Slot): 卡表被划分为一个个的卡槽,每个卡槽对应一小块内存,通常是512字节。一个卡槽可以看作是堆内存中的一个小块。
卡状态: 每个卡槽有相应的卡状态,用于表示引用的变化状态。常见的卡状态有CLEAN、DIRTY、和被修复的状态。
CLEAN(干净):
- 含义:表示卡槽中的引用没有发生变化。
- 操作:当一个引用变为非空时,卡状态从CLEAN变为DIRTY。
DIRTY(脏): - 含义:表示卡槽中的引用发生了变化,需要在垃圾回收过程中被扫描。
- 操作:在垃圾回收过程中,DIRTY状态的卡槽需要被扫描,以确定是否存在需要回收的引用。
被修复: - 含义:在并发标记阶段,G1收集器可能会在回收的过程中发现某些卡槽的引用已经被回收器清理,此时这些卡槽的状态会被标记为“被修复”。
- 操作:标记为“被修复”的卡槽在后续的垃圾回收迭代中不再需要扫描。