当一个对象没有人再引用他的时候,他就可以被回收了
垃圾回收算法有如下几种:
Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。 因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。
任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。
那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。
引用计数法有一个大BUG,就是当存在循环引用现象的时候,就会导致双方引用计数始终无法归零,内存没办法被释放
GC Root
对象 为起点的引用链找到该对象,找不到,表示可以回收GC Root
?在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
一般来说,如下情况的对象可以作为GC Roots:
常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。
一个对象可以属于多个root,GC root有几下种:
1
.Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.2
.Thread -活着的线程3
.Stack Local - Java方法的local变量或参数4
.JNI Local - JNI方法的local变量或参数 JNI5
.Global - 全局JNI引用 Monitor Used - 用于同步的监控对象6
.Held by JVM -用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。
分析哪些对象可以作为GC Root
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler线程调用虚引用相关方法释放直接内存
对象重写了终结方法,并且没有对象强引用他时,对象会被释放,那么终结方法什么时候,如何被调用呢?
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
处理引用队列的线程优先级比较低,这样会导致对象的finalize 方法迟迟不被调用,导致对象占用内存迟迟不能被是方法
先介绍三个虚拟机参数:
/**
* <p>
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
* </p>
* @author 大忽悠
* @create 2022/1/21 21:20
*/
public class Main
{
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();
}
}
对于上面的情况来说,如果数据是不太重要的,可以丢弃一些,那么可以采用软引用形式,即第一次垃圾回收后,如果内存还是不够,那么会回收软引用对象
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list=new ArrayList<>();
for(int i=0;i<5;i++)
{
SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB]);
System.out.println("第"+i+"次创建软引用对象:"+ref.get());
list.add(ref);
}
System.out.println("循环结束:"+list.size());
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
//阻塞
System.in.read();
}
软引用可以在内存敏感的情况下,用于关联一些不重要的资源
当软引用中的对象被回收后,对应的软引用对象也就没有意义了,需要被回收,那么怎么回收软引用对象呢? —>引用队列
List<SoftReference<byte[]>> list=new ArrayList<>();
//引用队列
ReferenceQueue<byte[]> queue=new ReferenceQueue<>();
for(int i=0;i<5;i++)
{
//管理了引用队列,当软引用关联的byte数组被回收时,软引用对象会被加入引用队列中
SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB],queue);
System.out.println("第"+i+"次创建软引用对象:"+ref.get());
list.add(ref);
}
//从队列中获取无用的软引用对象,并移除
Reference<? extends byte[]> reference = queue.poll();
while(reference!=null)
{
//解除强引用关系
list.remove(reference);
reference=queue.poll();
}
System.out.println("循环结束:"+list.size());
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
//阻塞
System.in.read();
先不管引用队列,只看弱引用:
List<WeakReference<byte[]>> list=new ArrayList<>();
//引用队列
// ReferenceQueue<byte[]> queue=new ReferenceQueue<>();
for(int i=0;i<10;i++)
{
//管理了引用队列,当软引用关联的byte数组被回收时,软引用对象会被加入引用队列中
WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB]);
System.out.println("第"+(i+1)+"次创建弱引用对象:"+ref.get());
list.add(ref);
System.out.println("当前剩余的弱引用对象列表: ");
for (WeakReference<byte[]> weakReference : list) {
System.out.print(weakReference.get()+" ");
}
System.out.println();
}
//从队列中获取无用的软引用对象,并移除
// Reference<? extends byte[]> reference = queue.poll();
// while(reference!=null)
// {
// //解除强引用关系
// list.remove(reference);
// reference=queue.poll();
// }
System.out.println("循环结束:"+list.size());
for (WeakReference<byte[]> weakReference : list) {
System.out.println(weakReference.get());
}
//阻塞
System.in.read();
输出分析
分析之前首先明确一点,我们每次往list集合中增加了一个弱引用对象,不仅弱引用关联的对象会占用内存,弱引用对象本身也会占用内存,即使关联对象被回收了,弱引用对象依然存在
加上引用队列
List<WeakReference<byte[]>> list=new ArrayList<>();
//引用队列
ReferenceQueue<byte[]> queue=new ReferenceQueue<>();
for(int i=0;i<10;i++)
{
//管理了引用队列,当弱引用关联的byte数组被回收时,软引用对象会被加入引用队列中
WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB],queue);
System.out.println("第"+(i+1)+"次创建弱引用对象:"+ref.get());
list.add(ref);
System.out.println("当前剩余的弱引用对象列表: ");
for (WeakReference<byte[]> weakReference : list) {
System.out.print(weakReference.get()+" ");
}
System.out.println();
}
//从队列中获取无用的软引用对象,并移除
Reference<? extends byte[]> reference = queue.poll();
while(reference!=null)
{
//解除强引用关系
list.remove(reference);
reference=queue.poll();
}
System.out.println("循环结束:"+list.size());
for (WeakReference<byte[]> weakReference : list) {
System.out.println(weakReference.get());
}
//阻塞
System.in.read();
软引用是一次FULL GC还不够的时候才回去回收,弱引用一次FULL GC后就会回收
定义: Mark Sweep
这里清除不是真正的清零,而是将被回收的内存区域标记为了空闲内存
定义:Mark Compact
由于整理牵扯到对象的移动,需要考虑内存的拷贝,还有引用对象地址的改变
定义:Copy
参数设置:
-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
-Xms20M---堆初始化大小 -Xmx20M---堆最大大小 -Xmn10M--->新生代大小 -XX:+UseSerialGC --->防止幸存区大小动态调整 -XX:+PrintGCDetails -verbose:gc
实例演示:
/**
* @author 大忽悠
* @create 2022/1/21 21:20
*/
public class Main
{
private static final int _4MB=4*1024*1024;
//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
// -Xms20M---堆初始化大小 -Xmx20M---堆最大大小 -Xmn10M--->新生代大小 -XX:+UseSerialGC --->防止幸存区大小动态调整 -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
}
}
垃圾日志分析
实例分析:
public class Main
{
private static final int _521KB=521*1024;
private static final int _1MB=1024*1024;
private static final int _6MB=6*1024*1024;
private static final int _7MB=7*1024*1024;
private static final int _8MB=8*1024*1024;
//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
List<byte[]> bytes = Arrays.asList(new byte[_7MB], new byte[_521KB], new byte[_521KB]);
}
}
最后可以看出,并不是一定要到达阈值时,才会将对象移动到老年代,当新生代内存不足时,会将新生代存活对象移动到老年代,但是如果此时老年代内存也不足,会触发Full GC
大对象在老年代空间足够,新生代空间不足时,会直接晋升到老年代
OOM分析
当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,从而不影响其他线程的运行
STW是垃圾回收时,所有线程处垃圾回收线程外的暂停时间
开启串行垃圾回收器的VM参数:
-XX:+UseSerialGC = Serial + SerialOld
串行垃圾回收器分为两个部分:新生代垃圾回收,采用复制算法 + 老年代垃圾回收,采用标记加整理算法
垃圾回收线程执行垃圾回收时,会阻塞其他线程,因为垃圾回收会涉及到对象地址的改变
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
新生代垃圾回收器,采用复制算法 老年代垃圾回收,采用标记加整理算法
这两个垃圾回收器是多线程的,并且只要开启其中一个,另一个就会跟着开启
并且线程数取决于CPU核心数
jdk1.8默认使用的垃圾回收器
-XX:+UseAdaptiveSizePolicy
自适应动态调整新生代区域比例,伊甸园和幸存区比例,还有阈值
-XX:GCTimeRatio=ratio : 调整垃圾回收和总时间的占比,公式: 1/(1+radio),radio默认值为99,即垃圾回收的时间不能超过总时间的百分之一,如果你工作了一百分钟,那么最多只能有一分钟用于垃圾回收,如果超过这个时间,便会动态调整堆的大小达到这个目标,一般是增大堆内存
-XX:MaxGCPauseMillis=ms : 每一次垃圾回收的暂停时间限制,这个配置和上面的互斥,因为radio越小,一般堆会分配的更大,那么单次垃圾回收的时间会变长
-XX:ParallelGCThreads=n :控制运行时的线程数
开启响应时间优先的垃圾回收期的VM参数:
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
基于并发标记清除的老年代垃圾回收器 新生代使用的还是标记整理算法
如果并发失败了,会采取补救措施,从老年代的并发垃圾回收器退化为单线程的标记整理垃圾回收器
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
并行执行的垃圾回收器线程数 并发执行的垃圾回收器线程数,一般设置为并行数的1/4
CMS在最后执行并发清理的过程中,其他用户线程可能会产生新的垃圾,这些垃圾只有等待下一次垃圾清理的时候,才能被回收,这些垃圾我们称为浮动垃圾
垃圾回收过程中会产生新垃圾,这些垃圾无法等待堆内存不足了再进行回收,这些新垃圾没地放了, 因此我们需要提前预留一些空间存放这些浮动垃圾
设置当老年代的内存占用达到多少百分比时,执行垃圾回收,为的是预留空间存放浮动垃圾,百分比越小,触发CMS垃圾回收的时机越早
-XX:CMSInitiatingOccupancyFraction=percent
进行重新标记的时候,新生代的对象可能会引用老年代的对象,因此需要重新扫描整个堆,但是新生代中的垃圾对象比较多,如果从新生代的对象引用去做可达性分析找到老年代对象,本来这些新生代对象就要被回收掉,会造成额外扫描资源浪费
在进行重新标记之前,先进行一次新生代的垃圾回收操作
-XX:+CMSScavengeBeforeRemark
因为CMS基于标记加清除算法,会产生内存碎片,如果老年代产生了较多的内存碎片,同时新生代也产生了很多内存碎片,此时新创建的对象无法放入新生代,也无法放入老年代,这样就造成了并发失败。
此时CMS垃圾回收器无法正常工作,会退化为串行的垃圾回收器,会对碎片进行整理,碎片减少了,才能恢复工作,但是这样垃圾回收的时间会飙升
定义:Garbage First
适用场景
相关 JVM 参数
G1开创了面向局部收集的设计思路和基于Region的布局设计,并且还提供并发的类卸载支持,被称为全能垃圾收集器
会 STW
对于G1来说,他会把堆内存划分为大小相等的一个个区域,每个区域都可以独立作为伊甸园,新生代和老年区,程序运行时会加载一些类对象到伊甸园区域中保存,当伊甸园区域逐渐被占满,会触发一次新生代的垃圾回收,会触发一个STW
G1是以Region作为单次回收的最小单元,并且每个Region都有自己回收优先级,每次先回收哪些优先级大的region区域
新生代垃圾回收,会将幸存的对象以复制的算法放入幸存区中
幸存区的对象比较多或者幸存区对象存回年龄超过一定时间,再下一次新生代内存分配失败触发Minor GC后,会将幸存区中满足条件的对象移动到老年代; 不满足年龄的幸存区对象会再次拷贝到另一个幸存区中
在 Young GC 时会进行 GC Root 的初始标记
初始标记仅仅是标记一下GC ROOTS能直接关联到的对象,并且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿
老年代占用堆空间比例达到阈值时
,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
会对 E、S、O 进行全面垃圾回收
最终标记(Remark)会 STW
拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms
G1可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收效益最大,这就是G1收集器的Mixed GC模式
G1虽然仍然保留新生代和老年代的概念,但新生代和老年代不再固定,他们都是一系列区域的动态集合,G1收集器能够建立可预测的停顿时间模型,是因为他将Region作为单词回收的最小单元,即每次回收到的内存空间都是Region大小的整数倍,这样可以避免在整个java堆中进行全区域的垃圾收集。
G1会去跟踪各个region里面的垃圾堆积的价值大小,价值即回收所获得的空间大小及回收需要的时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定的收集停顿时间来优先处理回收价值收益最大的那些region
在决定进行回收的时候,g1会对region按照回收价值和成本排序,根据用户期望的停顿时间来指定回收计划,可以自由选择任意多个region组成回收集,然后把决定回收的那一部分region的存活对象复制到空的region中,再清理掉整个旧的region的全部空间
FULL GC是整堆收集,收集整个java堆和方法区的垃圾收集
新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc
新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc
新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足
会触发cms的垃圾回收,如果在cms的并发清理过程中,垃圾回收的速度赶不上新对象分配的速度,会导致内存分配失败,进而触发STW,临时启用serial old单线程垃圾收集器来重新进行老年代的垃圾收集,进行的是FULL GC
新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足
对于G1来说,如果老年代堆空间(所有是老年代的region占用总控)占比默认达到百分之45时,会触发并发标记阶段,然后是最终标记,最后进行筛选回收,即混合收集 G1为每一个region设计了两个名为TAMS(Top at Mark Start)的指针,把region中的一部分空间划分出来用户并发回收过程中的新对象的分配,并发回收时新对象的分配必须在这两个指针位置以上。 G1默认在这个地址以上的对象是被隐式标记过的,即默认存活,不纳入回收范围 如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程,导致FULL GC而产生长时间的STW,这一点和CMS中FULL GC类似
新生代回收的跨代引用(老年代引用新生代)问题
g1中每个region都维护了一个属于自己的记忆集,这些记忆集会记录下别的region指向自己的指针并标记这些指针分别在哪些卡页范围内。
g1的记忆集本质是一个哈希表,key是别的region的起始地址,value是一个集合,里面存放的元素是卡表的索引号,这种双向的卡表结构(卡表是我指向谁,这种数据结构还记录了谁指向我)
卡表和记忆集的关系,参考hashMap与map的关系
卡表对应的字节数组,每一个元素都对应内存区域中的一个特定的内存块,这个内存块被称为卡页。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个或对个对象的字段存在跨代指针,那就将对应卡表的数组元素标识为1,称这个元素变脏了。
在垃圾回收时,只要筛选出卡表中变脏的元素,就可以得出哪些卡页中的内存卡中包含跨代指针,把他们加入GC ROOTS中扫描
pre-write barrier + satb_mark_queue
Remark是重新标记阶段
我们知道现在主流的垃圾回收器都是依靠可达性分析算法来判定对象是否存活,但是可达性分析算法要求全过程必须基于一个能够保障一致性快照中才能进行分析,这意味着必须全程冻结用户线程进行。
在GC ROOTS枚举过程中,由于GC ROOTS的对象数量相对较少,并且在OopMap等技术的加持下,枚举GC ROOTS的时间相对短暂且固定。
但是从GC ROOTS玩下继续遍历对象图,这个耗费时间就和java堆容量成正比例关系了,堆越大,存储对象越多,对象图结构越复杂,表示对象产生停顿时间就越长。
我们知道GC ROOTS的枚举必须是STW的,但是如果从GC ROOTS往下继续标记的过程也STW,未免时间也太长了,必须想办法让这个过程和用户线程并发执行,但是如果在进行标记过程中,用户线程改变了某个已经被标记过的对象引入,就会造成不一致性,我们需要解决这个问题,这就引入了三色标记法:
如果在标记过程中用户线程改变了某个对象的引用,就将该对象标记为灰色,并将其加入stab队列中,等待重新标记阶段,垃圾收集器会去访问该队列里面所有对象,判断是否存活,即重新扫描一遍这些灰色对象
**如何在用户线程改变了某个对象的引用,就将该对象标记为灰色的呢? **
这是通过写屏障实现的,即对某个对象进行修改的时候,会在修改前加入一个写屏障,将当前对象标记为灰色,即放入stab队列中去
优点:节省大量内存 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
JDK9已经对字符串底层的char数组做了改变,底层换成了byte[]数组
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
判定一个类型是否属于不在被使用的类的条件比较苛刻:
一般来说,只有在大量使用反射,动态代理,CGLIB等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通过需要java虚拟机具备类卸载的能力,保证不会对方法区造成大的内存压力
方法区的垃圾回收主要涉及废弃的常量和不再使用的类型
一个对象大于 region 的一半时,称之为巨型对象
G1 不会对巨型对象进行拷贝
回收时被优先考虑
G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
并发标记必须在堆空间占满前完成,否则退化为 FullGC
JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
JDK 9 可以动态调整