垃圾回收机制
简单的说就是内存中已经不再被使用到的内存空间就是垃圾
垃圾判定算法
引用计数法:给对象添加一个引用计数器,有人引用就加1,引用失效就减1
- 优点:实现简单,效率高
- 缺点:不能解决对象间循环引用(循环引用对象可以看作一组垃圾)
可达性分析法(根搜索算法):从根(GC Roots)阶段向下搜索对象节点,搜索走过的路径称为引用链,当一个对象到根节点之间没有连通的话,则该对象视为垃圾
- 可作为GCRoots对象的包括:虚拟机栈(栈帧局部变量)中的引用对象,方法区类静态属性引用对象,方法区中常量引用对象,本地方法栈中JNI引用对象,被synchronized关键字持有的对象,反应JVM内部情况的对象JMSbean,以及本地代码缓存中持有对象
- 由于在实际中程序中的对象很多,若虚拟机在运行期间都按照跟搜索从根向下遍历判定对象是否是垃圾的话,性能很低,所以使用了一组用来描述对象之间引用关系,OopMap的数据结构达到准确式GC的目的;在类加载完成时JVM就会计算出当前对象在那个偏移位置上有什么样的引用,将这些信息记录在OopMap中,这样就不必从根节点开始遍历,而之间扫描OopMap
- 在OopMap的协助下,JVM可以很快做完GCRoots的遍历,但是JVM并没有为每条指令生成OopMap,在能记录OopMap的指令的特定位置被称为安全点,当前线程执行到安全点后才运行暂停进行GC,若一段代码中对象引用关系不会发生变化,则该区域中任何地方开始GC都是安全的,该区域被称为安全区
判断是否是垃圾的步骤
- 根搜索算法判断不可用
- 看是否有必要执行
finalize()
方法(该方法会在对象第一次回收时调用,该方法只能被调用一次,所以该方法可实现对象自救,只能自救一次) - 两个步骤走完hou对象任然没有人使用,那就是垃圾
public class HelpSelf {
private static HelpSelf helpSelf = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("发生垃圾回收,执行自救");
helpSelf = this; //让该对象重新被引用即可达到自救目的
}
public static void main(String[] args) {
helpSelf = new HelpSelf();
helpSelf = null;
System.gc(); //建议GC
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第一次helpSelf对象是否被回收" + helpSelf);
helpSelf = null;
System.gc(); //建议GC
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第二次helpSelf对象是否被回收" + helpSelf);
}
}
判断类无用条件
满足以下任意条件时,说明类已经没有用了,该类就会从内存中卸载
- JVM中该类的所有实例都被回收
- 加载该类的ClassLoader已经被回收
- 没有任何地方引用该类Class对象
- 无法在任何地方通过反射访问该类
引用分类
无论是哪种垃圾判定算法中其实都是在判断对象是否还有人引用,但在Java中并非只有有引用或无引用两种状态
- 强引用:手动new出来的对象,不会被回收
- 软引用:还有用但并不是必须的对象,当垃圾回收后内存还是不够时才会被回收掉,使用
SoftReference
来实现 - 弱引用:非必须对象,比软引用弱,垃圾回收时会回收掉,使用
WeakReference
来实现 - 虚引用:也叫幽灵引用或幻影引用,最弱的引用,垃圾回收时会回收掉,使用
PhantomReference
来实现
测试前最好将JVM内存调小点,方便看到效果,我使用的参数是-Xms3m -Xmx3m
,使用下面代码进行测试
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
class ReferenceObject {
private byte[] bytes = new byte[1024]; //消耗内存,自行调节
private String objectID;
public ReferenceObject(String objectID) {
this.objectID = objectID;
}
@Override
public String toString() {
return "ReferenceObject{" +
"objectID='" + objectID + '\'' +
'}';
}
@Override
protected void finalize() throws Throwable { //当对象不被引用时,GC会调用该对象的finalize()方法
super.finalize();
System.out.println(this.toString() + "被回收");
}
}
public class Main {
private static ReferenceQueue<ReferenceObject> referenceQueue = new ReferenceQueue<>();
//Reference对象所引用的对象被GC回收后,该Reference对象将会被加入引用队列中
private static void printQueue() {
Reference<? extends ReferenceObject> reference = referenceQueue.poll();
if (reference != null) {
System.out.println("队列中存在有被GC回收的引用" + reference.get());
} else {
System.out.println("队列中不存在有被GC回收的引用");
}
}
private static void softReference() {
List<SoftReference<ReferenceObject>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SoftReference<ReferenceObject> softReference = new SoftReference<>(new ReferenceObject("Soft" + i), referenceQueue);
//创建一个软引用,存放引用对象,当该对象被回收后会加入到引用队列中
System.out.println("创建软引用对象: " + softReference.get());
list.add(softReference);
}
System.gc(); //建议垃圾回收
try {
Thread.sleep(500); //程序暂停一下在进行验证,不容易出错
} catch (InterruptedException e) {
e.printStackTrace();
}
printQueue(); //验证队列中是否存有引用对象被回收的引用
}
private static void weakReference() {
List<WeakReference<ReferenceObject>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<ReferenceObject> weakReference = new WeakReference<>(new ReferenceObject("Weak" + i), referenceQueue);
//创建一个弱引用,存放引用对象,当该对象被回收后会加入到引用队列中
System.out.println("创建弱引用对象: " + weakReference.get());
list.add(weakReference);
}
System.gc(); //建议垃圾回收
try {
Thread.sleep(500); //程序暂停一下在进行验证,不容易出错
} catch (InterruptedException e) {
e.printStackTrace();
}
printQueue(); //验证队列中是否存有引用对象被回收的引用
}
private static void phantomReference() {
List<PhantomReference<ReferenceObject>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
PhantomReference<ReferenceObject> phantomReference = new PhantomReference<>(new ReferenceObject("Phantom" + i), referenceQueue);
//创建一个虚引用,存放引用对象,当该对象被回收后会加入到引用队列中
System.out.println("创建虚引用对象: " + phantomReference.get());
list.add(phantomReference);
}
System.gc(); //建议垃圾回收
try {
Thread.sleep(500); //程序暂停一下在进行验证,不容易出错
} catch (InterruptedException e) {
e.printStackTrace();
}
printQueue(); //验证队列中是否存有引用对象被回收的引用
}
public static void main(String[] args) {
softReference();
// weakReference();
// phantomReference();
}
}
垃圾回收算法
垃圾回收中需要解决的问题
跨代引用:一个代中的对象引用另一个代中的对象,跨代引用相对于同代引用来说只是极少数的,并且相互引用关系的两个对象是应该倾向于同时生存和消亡的,由于存在跨代引用,当扫描新生代时也要同时扫描老年代,这样就会极大影响垃圾回收性能,为了解决该问题,可使用记忆集
记忆集(Remembered Set):一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,简单来说就是建立一个全局数据结构,将新生代和老年代划分成若干个小块,标识出那一块内存存在跨代引用,也就是利用另一块内存记录跨代引用的情况,记忆集有以下几种实现精度
- 字长精度:每个记录精确到一个机器字长,该字包括跨代指针
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针
卡表(Card Table):是记忆集的一种具体实现,定义了记忆级的记录精度和与堆内存的映射关系,卡表的每个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块称为卡页(Card Page)
写屏障:可看作JVM对引用类型字段赋值这个动作的AOP,通过写屏障来实现当对象状态改变后,维护卡表状态
垃圾回收算法中的相关概念
GC类型
- MinorGC/YoungGC:发生在新生代的收集动作
- MajorGC/OldGC:发生在老年代的GC,目前只有CMS收集器会有单独收集老年代的行为
- MixedGC:手机整个新生代以及部分老年代,目前只有G1收集器会有这种行为
- FullGC:收集整个Java堆和方法区的GC
STW
STW(Stop The World)是Java中一种全局暂停现象,多半由GC引起,所谓全局暂停就是所有Java代码停止运行,native代码可以执行但不能与JVM进行交互,其危害是长时间服务停止,无响应,对于HA系统可能引起主备切换,严重危害生产环境
垃圾收集类型
- 串行收集:GC单线程内存回收,会暂停所有用户线程,比如Serial收集器
- 并行收集:多个GC线程并发工作,同样用户线程会暂停,比如Parallel收集器
- 并发收集:用户线程和GC线程同时执行(不一定是并行,可能交替执行),无需停顿用户线程,比如CMS收集器
标记清除法
标记清除法(Mark-Sweep)算法分成标记和清除两个阶段
- 标记阶段:先标记出要回收的对象
- 回收阶段:然后统一回收这些对象
优点:就是简单
缺点:效率不高,标记和清除的效率都不高;标记清除过后会产生大量不连续的内存碎片,从而导致在分派大对象时触发GC
复制算法
复制算法(Copying):把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完啦,就把这块上还存活的对象拷贝到另一块,然后把这块清除掉
优点:实现简单,运行高效,无需考虑内存碎片问题
缺点:内存浪费较大,在存活对象较多时,效率较低,因此老年代一般不会选用复制算法,JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的存活区,每次使用Eden和单独一块存活区,回收时将存活对象放入另一个单独的存活区
HotSpot默认的Eden区和存活区比是8:1,也就是每次能用90%的新生代空间,若存活区空间不够,就需要依赖老年代进行分配担保,将放不下的对象直接进入老年代
分配担保:当新生代进行垃圾回收后,新生代的存活区放不下时,就需要将这些对象放到老年代的策略,也就是老年代为新生代的GC做空间分配担保,步骤如下:
- 在发生MinorGC前,JVM会检查老年代的最大可用连续空间,是否大于新生代所有对象的总空间
- 若大于可确保MinorGC是安全的
- 若小于,JVM会检查是否设置了允许担保失败,若允许失败,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小
- 若大于,则尝试进行一次MinorGC
- 若不大于,则改做一次FullGC
标记整理法
标记整理法(Mark-Compact):标记过程跟标记清除法一样,但后续不是直接清除可回收对象,而是让所有存活对象都想一端移动,然后直接清除边界以外的内存
垃圾收集器
垃圾收集器是垃圾收集算法的具体实现,所以不同厂商、不同版本的虚拟机实现差别较大,HotSpot中包含垃圾收集器组合有如下几组:
- Serial负责新生代,Serial old负责老年代
- ParNew负责新生代,CMS负责老年代,并且老年代使用Serial old备用
- Parallel Scavenge负责新生代、Parallel old负责老年代
- Parallel Scavenge负责新生代、Serial old负责老年代
- G1同时支持新生代和老年代
串行收集器
Serial / Serial old:串行收集器,GC单线程;垃圾收集时会STW,新生代采用复制算法,老年代采用标记整理法
- 优点:简单,对于单CPU,由于没有多线程的交互开销,可能更高效,Serial是Client模式下默认的新生代收集器
- 使用
-XX:+UseSerialGC
参数开启,会同时使用Serial和Serial old收集器组合
并行收集器
ParNew :并行收集器,GC多线程;垃圾收集时会STW,新生代采用复制算法
- 优点:在并发能力好的CPU环境中,STW时间要比串行收集器短;但对于单CPU或并发能力差的CPU,由于多线程交互开销,可能比串行收集器更差,是Server模式下首选的新生代收集器
- 不在使用
-XX:+UseParNewGC
单独开启,开启CMS收集器即可开启该新生代收集器 - 使用
-XX:ParNewGCThreads
参数指定线程数量,最好与CPU内核数量一致
Parallel Scavenge / Parallel old:并行收集器,垃圾收集时会STW,新生代采用复制算法,老年代采用标记整理法
- 优点:跟ParNew类似,但更关注吞吐量,能最高效的利用CPU,适合后台应用
- 使用
-XX:+UseParallelGC
或-XX:+UseParallelOld
参数开启,会同时使用Parallel Scavenge和Parallel old收集器组合 - 使用
-XX:MaxGCPauseMillis
参数设置GC最大停顿时间,若设置的很小会导致GC频率加快
CMS收集器
CMS(Concurrent Mark and Sweep):并发标记清除收集器,使用的是标记清除法,GC线程和用户线程同时运行,收集器过程分为下面几个阶段,部分阶段与用户线程同时运行:
- 初始标记:只标记GCRoots能直接关联到的对象,相对于从根节点向下找一层能直接关联到的对象,会STW
- 并发标记:进行GCRootsTracing过程,从一级节点向下查找,直到叶子节点,找出垃圾,与用户线程同时执行
- 重新标记:修正并发标记期间,因为程序运行导致标记发生变化的那一部分对象,会STW
- 并发清除:并发回收垃圾对象,与用户线程同时执行
- 重置线程:清空跟收集相关的数据并重置,等待下一次垃圾回收,与用户线程同时执行
优点:低停顿、并发执行
缺点:并发执行对CPU资源压力大;无法处理在处理过程中产生的垃圾,可能导致FullGC;标记清除法会导致大量内存碎片,从而在分配大对象时间可能触发FullGC
使用-XX:+UseConcMarkSweepGC
参数开启,会同时使用ParNew和CMS以及Serial old备用收集器组合,常用配置参数如下:
参数名 | 描述 |
---|---|
-XX:CMSInitiatingOccupancyFraction |
设置CMS收集器在老年代空间被使用多少后触发回收,默认是80% |
-XX:MaxTenuringThreshold=n |
设置新生代到老年代的岁数,默认是6 |
G1收集器
G1(Garbage-First):是一款面向服务端应用的收集器,与其他收集器相比具有如下特点:
- G1将内存划分成多个独立的区域,叫做Region,仅保留了分代思想,但是不再需要物理隔离,而是一部分Region的集合,也就是说无需Region是连续的,当一个大对象(超过Region一半)可能会横跨多个Region区域
- G1能充分利用多CPU多核环境硬件优势,尽量缩短STW
- G1整体上采用标记整理法,局部通过复制算法,不会产生内存碎片
- G1停顿可预测,能明确指定在一个时间段内,消耗在垃圾收集器上的时间不能超过多长时间
- G1跟踪各个Region中垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的时间来回收较值最大的区域,从而保证在优先时间内的高效收集
跟CMS类似,分为下面几个阶段:
- 初始标记:只标记GCRoots能直接关联到的对象,会STW
- 并发标记:进行GCRootsTracing过程,与用户线程同时执行
- 最终标记:修正并发标记期间因为程序运行导致标记发生变化的那一部分对象,会STW
- 筛选回收:根据限制的回收时间来进行价值最大回收,会STW
使用-XX:+UseG1GC
参数开启G1,JDK11默认就是G1,常用参数如下:
参数名 | 描述 |
---|---|
-XX:MaxGCPauseMillis=n |
设置最大GC停顿时间,JVM尽可能的停顿小于这个时间,但不保证 |
-XX:InitiatingHeapOccupancyPercent=n |
设置当堆占用了多少时就触发GC,默认为45 |
-XX:NewRatio=n |
设置老年代与新生代的比值,默认是2 |
-XX:SurvivorRatio=n |
设置Ened区和存活区的比值,默认为8 |
-XX:MaxTenuringThreshold=n |
设置新生代到老年代的岁数,默认是15 |
-XX:ParallelGCthreads=n |
设置GC线程数,默认值根据平台不同而不同 |
-XX:ConcGCThreads=n |
设置并发GC使用线程数 |
-XX:G1ReservePercent=n |
设置作为空闲空间的预留内存百分比,以降低目标空间溢出风险,默认值是10% |
-XX:G1HeapRegionSize=n |
设置G1区域大小,值是2的幂次方,范围是1MB到32MB,目标根据最小的Java堆大小划分出约2048个区域 |
Comments NOTHING