一、垃圾判断
引用计数算法(Reference Counting):
- 在对象中添加一个引用计数器
- 每当有一个地方引用它的时候,计数器就加
+1
- 每当有一个引用失效的时候,计数器就减-1
- 当计数器的值为
0
的时候,那么该对象就是可被GC回收的垃圾对象
引用计数算法存在的问题:对象循环引用
a对象引用了b对象,b对象也引用了a对象,a、b对象却没有再被其他对象所引用了,其实正常来说这两个对象已经是垃圾了,因为没有其他对象在使用了,但是计数器内的数值却不是 0
,所以引用计数算法就无法回收它们。
可达性分析算法(Reachability Analysis):
可达性分析算法也是JVM 默认使用的寻找垃圾算法。通过定义了一系列称为“GC Roots”的根对象作为起始节点集,从 GC Roots 开始,根据引用关系往下进行搜索,查找的路径我们把它称为 "引用链" 。当一个对象到 GC Roots之间没有任何引用链相连时(对象与GC Roots之间不可达),那么该对象就是可被GC回收的垃圾对象。
二、Java 中的四种引用类型
强引用(Strong Reference)
强引用是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足时,JVM
宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
如果强引用对象不使用时,需要弱化从而使GC
能够回收。
弱化方式:
- 显式地设置
strongReference
对象为null
,则gc
认为该对象不存在引用,这时就可以回收这个对象。但是,具体什么时候收集这要取决于GC
算法。例如,strongReference
是全局变量时,就需要在不用这个对象时赋值为null
,因为强引用不会被垃圾回收。 - 让对象超出作用域范围。
String str = new String("abc");//强引用
SoftReference<String> softReference = new SoftReference<>(str);//弱化
软引用(Soft Reference)
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。所以,软引用可用来实现内存敏感的高速缓存。
public class Test1 {
public static void main(String[] args) {
String str = new String("abc");//强引用
SoftReference<String> softReference = new SoftReference<>(str);//弱化
str = null;
System.gc();
try {
byte[] buff1 = new byte[900000000]; // 内存充沛
byte[] buff2 = new byte[900000000];
byte[] buff3 = new byte[900000000];
byte[] buff4 = new byte[900000000];
byte[] buff5 = new byte[900000000]; // 内存不足
} catch (Error e) {
e.printStackTrace();
}
System.out.println(softReference.get()); // 内存充沛abc,内存不足null
}
}
弱引用(Weak Reference)
只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;
ystem.gc();
// 一旦发生GC,弱引用一定会被回收
System.out.println(weakReference.get());
虚引用(Phantom Reference)
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,完全不会对其生存时间构成影响,它就和没有任何引用一样,随时可能会被回收。
public class PhantomReference<T> extends Reference<T> {
public T get() {
return null;
}
}
虚引用,主要用来跟踪对象被垃圾回收的活动,可以在垃圾收集时收到一个系统通知。
三、垃圾收集算法
分代收集理论
收集器应该将Java堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历过垃圾收集过程的次数)分配到不同的区域存储。
分代存储
如果一个区域中大多数对象都是朝生夕灭(新生代),难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,只关注如何保留少量存活对象,而不是去标记大量将要回收的对象,就能以较低代价回收到大量的空间。
如果一个区域中大多数对象都是难以回收(老年代),那么把它们集中放在一起,JVM
虚拟机就可以使用较低的频率,来对这个区域进行回收。
这样设计的好处是,兼顾垃圾收集的时间开销和内存空间的有效利用。
分代收集
堆区按照分代存储的好处:
在Java堆区划分成不同区域后,垃圾收集器才可以每次只回收其中某一个或者某些区域,所以才有MinorGC
、MajorGC
、FullGC
等垃圾收集类型划分。
在Java堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾收集算法:标记-复制算法、标记-清除算法、标记-整理算法等
垃圾收集类型划分:
部分收集(Partial GC):没有完整收集整个Java
堆的垃圾收集,其中又分为:
- 新生代收集(
Minor GC
/Young GC
) - 老年代收集(
Major GC
/Old GC
) - 混合收集(
Mixed GC
):收集整个新生代和部分老年代的垃圾收集。 - 整堆收集(
Full GC
):收集整个Java
堆的垃圾收集。
垃圾收集算法
垃圾收集算法是指用于管理和回收内存中无用对象的算法。在程序运行过程中,不断创建对象,而有些对象可能成为垃圾,即程序不再需要使用它们,但是仍占用着内存空间。垃圾收集算法就是为了识别和回收这些无用对象,从而释放内存空间,以提高程序可用内存的利用率和程序的性能。
目前主流JVM
虚拟机中的垃圾收集器,都遵循分代收集理论:
- 弱分代:绝大多数对象都是朝生夕灭
- 强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡
标记-清除算法 ( Mark-Sweep )
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
- 执行效率不稳定问题:如果执行垃圾收集的区域,大部分对象是需要被回收的,则需要大量的标记和清除动作,导致效率变低。
- 内存空间碎片化问题:标记清除后会产生大量不连续的碎片,空间碎片太多,会导致分配较大对象时,无法找到足够的连续空间,从而会触发新的垃圾收集动作。
标记-复制算法 ( Copying )
“标记-复制”收集算法简称“复制算法”,为了解决“标记-清除”面对大量可回收对象时执行效率低下的问题。
该算法将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把已使用的空间一次清理掉。
- 对象存活率较高,需要进行较多的内存间复制,效率降低
- 浪费过多的内存,使现有的可用空间变为原先的一半
标记-整理算法 ( Mark-Compact )
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向内存空间一端移动, 然后直接清理边界以外的内存,这样清理的机制,不会像标记-整理那样留下大量的内存碎片。
前虚拟机的垃圾收集都基于分代收集思想,根据对象存活周期的不同,将内存分为几个不同的区域,在不同的区域使用不同的垃圾收集算法。
四、垃圾收集器
Serial 收集器(新生代)
Serial (串行)收集器是最基本、历史最悠久的垃圾收集器,采用“标记-复制”算法负责新生代的垃圾收集。它是Hotspot虚拟机运行在客户端模式下的默认新生代收集器。
它是一个单线程收集器。它会使用一条垃圾收集线程去完成垃圾收集工作,并且它在进行垃圾收集工作的时候,必须暂停其他所有的工作线程( "Stop The World" ),直到收集结束。
这样的设计,带来的好处就是:简单高效。对于内存资源受限制的环境,它是所有收集器中额外内存消耗最小的收集器。适合单核处理器或处理器核心数较少的环境,每次收集几十MB
甚至一两百MB
的新生代内存,垃圾收集的停顿时间完全可以控制在十几毫秒或几十毫秒,最多一百多毫秒。
Serial Old 收集器(老年代)
Serial Old收集器同样是一个单线程收集器,采用“标记-整理”算法负责老年代的垃圾收集,主要用于客户端模式下的Hotspot虚拟机使用。
如果在服务器端使用,它主要有两种用途:
- 在JDK5及以前版本,与Parallel Scavenge收集器搭配使用;
- 作为CMS收集器发生失败时的后备预案;
ParNew 收集器(新生代)
ParNew 收集器是一个多线程的垃圾收集器。它是运行在 Server 模式下的虚拟机的首要选择,可以与 Serial Old,CMS垃圾收集器一起搭配工作,采用“标记-复制”算法。
Parallel Scavenge 收集器(新生代)
Parallel Scavenge收集器是也是一款新生代收集器,使用“标记-复制”算法实现的多线程收集器。
Parallel Scavenge收集器预其它收集器的目标不同,CMS等其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间。但是Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
Parallel Old 收集器(老年代)
Parallel Old收集器是一个多线程的垃圾收集器,使用“标记-整理”算法,是Parallel Scavenge收集器的老年代版本。
在注重吞吐量或者处理器资源较为稀缺的应用场景,都可以优先考虑 Parallel Scavenge 收集器 + Parallel Old收集器这个收集器组合。
CMS 收集器(老年代)
CMS 收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现,是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
工作流程:
- 初始标记
(
CMS Initial Mark)
:标记一下GC Roots
能直接关联到的对象,速度很快; - 并发标记
(
CMS Concurrent Mark):从GC Roots
的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行; - 重新标记
(
CMS remark)
:重新标记阶段,是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间长,远远比并发标记阶段时间短 - 并发清除
(
CMS concurrent sweep)
:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
主要优点:并发收集、低停顿。
主要缺点:
- 影响用户线程的执行效率:并发标记和并发清除时,是和用户线程一起运行的,收集过程中肯定占用了用户程序的
CPU
资源。CMS
默认启动的回收线程数是(CPU数量+3)/4
,当CPU
数量在4
个以上时,垃圾回收线程占用不少于25%的CPU
资源,势必影响用户线程的执行效率。 - 无法处理浮动垃圾:在并发清除阶段,用户线程并没有停止,所以还会继续产生新的垃圾,只能等待下一次收集时才能进行回收,这部分垃圾被称为“浮动垃圾”。
- 产生大量空间碎片:因为CMS收集器是基于“标记-清除”算法实现的,所以在进行大量的垃圾回收时,会产生很多不连续的内存空间。这是使用“标记-清除”算法都会有的缺点。
G1 收集器(老年代)
G1 收集器是一款面向服务端应用的垃圾收集器,它的设计目标是在保证低停顿时间的同时,尽可能地获得高吞吐量。
G1 收集器采用了分代收集的思想,但是它将堆划分为一组可变大小的区域,而不是固定大小的新生代和老年代。G1 收集器的垃圾收集过程分为以下几个阶段:
- 初始标记 (Initial Mark):标记 GC Roots 直接关联的对象。
- 并发标记 (Concurrent Mark):标记所有存活对象,与应用程序并发执行。
- 最终标记 (Final Mark):标记存活对象的对象图。
- 筛选回收 (Live Data Counting and Evacuation):计算每个区域的存活对象数量并进行回收,同时将剩余的对象移动到空闲的区域。
G1 收集器的优点包括:
- 高可预测的 GC 暂停时间:通过智能地决定哪些区域需要被收集、收集的顺序和回收的时间,使得 GC 暂停时间在可控范围内。
- 不会产生大量内存碎片:将堆划分为多个区域后,可以避免因为对象大小不同而产生的内存碎片问题。
- 高吞吐量:与传统的 CMS 垃圾收集器相比,G1 收集器在吞吐量上有显著提升。