内容:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为零的对象都是不可能在被使用的
缺点:无法解决对象之间的循环引用问题。如下图所示对象A和对象B,他们之间相互引用,除此之外再无任何引用,则他们的引用计数器值都为1,但实际上这两个对象都不可能在被访问了,而且无法被回收。
内容:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots之间没有任何的引用链相连(或者用图论的话来说就是从GC Root到这个对象不可达),则证明这个对象是不可能再被使用的
Java虚拟机中的垃圾回收器采用的可达性分析算法探索所有存活对象
GC Roots包含哪些对象?
如何使用工具查看GC Roots:
jmap -dump:format=b,live,file=1.bin 进程id
强引用:
软引用:
弱引用:
虚引用:
终结引用(FinalReference):
软引用使用示例:
当使用容量大但是不那么重要的对象时(如图片资源),可以使用软引用
代码:
/**
* 演示软引用的使用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class SoftReferenceTest {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) {
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
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
运行结果:
清除软引用对象:
清除掉没有引用对象的软引用对象(ref),主要使用引用队列ReferenceQueue进行实现。
弱引用使用示例:
代码:
/**
* 演示软引用的使用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class SoftReferenceTest {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) {
// list ---------->WeakReference---------->byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++){
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> wr : list) {
System.out.print(wr.get() + " ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
内容:首先根据对象到GC Roots之间是否有可达路径来标记需要回收的对象,再根据标记的结果对内存进行清除(将清除的内存的起始地址放入可分配内存表中)
优点:速度快,简单
缺点:
内容:首先根据对象到GC Roots之间是否有可达路径来标记需要回收的对象,之后让所有存活的对象都向内存空间的一端移动,然后清理掉边界以外的内存。
优点:不会出现内存碎片化的问题
缺点:垃圾回收效率没有标记清除高,因为需要移动存活对象,还要更新对存活对象引用(一般需要全部暂停用户应用程序才能进行)
内容:它将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另外一块,然后把已使用过的内存空间一次清理掉
优点:标记-复制算法优点也是不会产生内存碎片化的问题,分配内存时只需要移动堆顶指针按顺序分配即可
缺点:它的缺点也十分明显,就是可用内存缩小到了原来的一半
分代收集理论的理论基础:
分代:
GC分类:
分代流程:
垃圾回收相关参数:
含义 | 参数 |
---|---|
堆初始容量 | -Xms |
堆最大容量 | -Xmx或-XX:MaxHeapSize=size |
新生代容量 | -Xmn或者-XX:NewSize=szie 和-XX:MaxNewSize=size |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio和-XX:UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阙值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
Full GC前Minor GC | -XX+ScavengeBeforeFullGC |
经典垃圾回收器的种类以及搭配关系如下图所示:
名词介绍(垃圾回收场景):
特点:
1)、Serial收集器
Serial收集器是最基础、历史最悠久的垃圾收集器。
2)、Serial Old收集器
Serial Old是Serial的老年代版本
老年代
采用标记-清除算法
搭配
使用VM指令 -XX:+UseSerialGC=Serial+SerialOld
开启Serial和Serial Old搭配
Serial和Serial Old运行示意图如下图所示,不能并发运行,会产生STW:
特点:
1)、Parallel Scavenge收集器(JDK8默认新生代收集器)
新生代、多线程并行
标记-复制算法
目标是达到一个可控的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾回收时间)
,吞吐量和响应时间一般是相互矛盾的)
具有自适应调节策略
VM参数设置
-XX:+UseAdaptiveSizePolicy
该参数会自动设置新生代Eden和Surivor区的比例,虚拟机会根据当前系统运行情况手机性能监控信息,动态的调整参数以提供最合适的停顿时间或者最大吞吐量-XX:ParallelGCThreads=n
设置Parallel Scavenge收集器使用的线程数目,最好和CPU核数一致-XX:MaxGCPauseMillis=ms
设置一个大于0的毫秒值,垃圾回收器会尽力保证内存回收花费的时间不超过用户设置的值。而且垃圾回收停顿时间缩短是以牺牲吞吐量和新生代空间为代价的,所以这个指不是越小越好-XXGCTimeRatio=ratio
设置一个ratio,ratio代表用户线程运行时间占比,也就是设置该参数之后,那允许的最大垃圾收集时间占总时间比率1/(1+ratio)
。同理ratio也不是越大越好,一般设为19,这时最大垃圾收集时间占总时间比率为5%。2)、Parallel Old收集器(JDK8默认老年代收集器)
Parallel Old收集器是Parallel Scavenge的老年代版本
Parallel Old和Parallel Scavenge运行示意图如下图所示,
特点
1)、CMS收集器
CMS(Concurrent Mark Sweep)是一款以获得最短回收停顿时间为目标的收集器,它运行在老年代,是基于标记-清除算法的。整个回收过程包括四个步骤★:
Concurrent Mark Sweep收集器运行示意图如下图所示:
CMS的缺点:
CMS通常搭配ParNew新生代垃圾回收器使用。ParNew实际上是Serial收集器的多线程并行版本。
CMS常用VM参数:
-XX:+UseConcMarkSweepGC~-XX:+UseParNewGC~SerialOld
:使用CMS搭配ParNew,并且使用Serial Old作为并发失败的后备收集器-XX:ParallelGCThreads=n
:设置ParNew并行执行时使用的线程数,通常为CPU核数-XX:ConcGCThreads=n
:设置CMS并发执行时使用的线程数,同为CPU核数/4
-XX:CMSInitiatingOccupancyFraction=precent
:设置CMS在老年代内存使用多少时进行垃圾回收(由于并发清理阶段会产生浮动垃圾,所以不能等100%时在清理),通常为80%左右。太低会导致频繁的垃圾回收,太高会导致并发失败。-XX:+CMSScavengeBeforeRemark
:设置重新标记前进行Minor GC,减少标记时跨代引用的查找时间G1(Garbage First)收集器开创了面向局部收集的设计思路和基于Region的内存布局形式。G1的特点包括★:
-XX:MaxGCPauseMills
设定,默认值为200毫秒)优先处理回收价值收益最大的那些Region。这也是“Garbage First”名称的由来G1收集器运作过程包括下面四个步骤★:
G1收集器的不足之处:G1无论是为了垃圾收集产生的**内存占用(Footprint)还是程序运行时的额外执行负载(Overload)**都要比CMS高。
CMS常用VM参数:
-XX:+UseG1GC
:设置使用G1垃圾回收器,JDK9默认是G1-XX:G1HeapRegionSize=size
:设置G1中每个Region的大小,取值范围为1MB~32MB,且应为2的N次幂-XX:MaxGCPauseMills=time
:设置允许的垃圾回收停顿时间,默认是200毫秒。G1根绝此值选定需要收集那些Region和收集的个数1)、Shenandoah回收器
2)、ZGC回收器
(待补充)
private static final int _1MB = 1024 * 1024;
/**
* 新生代 Minor GC测试
* VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8
*
* -XX:+UseSerialGC 设置使用Serial+Serial Old, JDK8默认时Parallel Scavenge+Parallel Old
* -XX:+PrintGCDetails 设置输出每次GC的详细信息,并在程序结束后输出内存各个区域的分配情况
* -Xms20m -Xmx20m -Xmn10m 设置内存区域大小为20M,其中分给新生代10M
* -XX:SurvivorRatio=8 设置新生代中Eden区和Survivor区的空间比率为8:1,
* 也就是Eden区8192K,Survivor to和from都是1024K,新生代总共可用9216K(Eden区+1个Survivor区)
*/
public static void testAllocation(){
byte[] a1,a2,a3,a4; //注意是byte[],不是Byte[]
//先手动执行一次Full GC用于清除其他垃圾,防止影响结果
System.gc();
//依次分别给a1,a2,a3,a4分配2MB,2MB,2MB,4MB的空间
//a1,a2,a3直接分配到新生代Eden区
System.out.println("a1");//分配前输出将要分配的编号
a1 = new byte[2*_1MB];
System.out.println("a2");
a2 = new byte[2*_1MB];
System.out.println("a3");
a3 = new byte[2*_1MB];
//a4分配时Eden区没有足够的内存发生了一次GC
//而a1,a2,a3分配的区域仍具有引用无法回收,而且Survivor区from空间也不够
//所以直接把a1,a2,a3移动了老年代,而在eden区为a4分配了内存空间
System.out.println("a4");
a4 = new byte[4* _1MB];
}
private static final int _1MB = 1024 * 1024;
/**
* 测试大对象直接在老年代分配
* VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
*
* -XX:PretenureSizeThreshold=3145728 设置大对象的内存阙值为3M(不能直接写3M)
* -XX:+UseSerialGC 设置使用Serial+Serial Old, 只有Serial和ParNew支持PretenureSizeThreshold参数
*/
public static void testPretenureSizeThreshold() {
//当设置大对象的阙值时3M时,6M的alloc引用的对象会直接分配到老年代
byte[] alloc = new byte[6 * _1MB];
}
/**
* 测试长期存活的对象会进入老年代
* VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
*
* -XX:MaxTenuringThreshold 设置对象经过多少次GC进入老年代
*/
public static void testTenuringThreshold(){
byte[] a1,a2,a3;
//先手动执行一次Full GC用于清除其他垃圾,防止影响结果
System.gc();
//为a1分配256K大小
System.out.println("a1");
a1 = new byte[_1MB / 4];
System.out.println("a2");
a2 = new byte[4 * _1MB];
//a3第一次分配会导致GC,此时a1的生命值从0变为1,但仍在新生代中
System.out.println("a3");
a3 = new byte[4 * _1MB];
a3 = null;
//a3第二次分配也会导致GC,此时a1的生命值为1
//当TenuringThreshold=1时,a1会进入到老年代
//当TenuringThreshold=15时,a1生命值为1,仍然会在新生代
System.out.println("a3");
a3 = new byte[4 * _1MB];
}
当TenuringThreshold=1的情况:
当TenuringThreshold=15的情况:
关于上面的这个案例,细心的同学可能会发现,a2不管是当TenuringThreshold为1或者15都进入了老年代,这是为什么呢?
原因是GC后Eden区幸存的会进入Survivor To,而Survivor To只有1M空间放不下有4M的a2,所以a2直接进入了老年代。
其实不光是对象大于Survivor区域时会直接进入老年代,只要满足下面条件都会直接进入老年带:
如果Survivor空间中相同年龄的对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象都会直接进入老年代,无需等到-XX:MaxTenuringThreshold设置的年龄
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。
JDK1.6下的情况:如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure); 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次Minor GC是有风险的:如果小于,或者XHadePomotio alure设置不允许冒险,那这时就要改为进行一次Full GC。
JDK1.8下的情况(JDK1.6之后的情况):-XX:HandlePromotionFailure参数被废弃,只要老年代的连续空间大于新生代对象总大小或者大约历次晋升的平均大小都会先执行Mionr GC,空间不足情况下在执行Full GC。
中相同年龄的对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象都会直接进入老年代,无需等到-XX:MaxTenuringThreshold设置的年龄**
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。
JDK1.6下的情况:如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure); 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次Minor GC是有风险的:如果小于,或者XHadePomotio alure设置不允许冒险,那这时就要改为进行一次Full GC。
JDK1.8下的情况(JDK1.6之后的情况):-XX:HandlePromotionFailure参数被废弃,只要老年代的连续空间大于新生代对象总大小或者大约历次晋升的平均大小都会先执行Mionr GC,空间不足情况下在执行Full GC。