文章目录
1、如何判断对象可以回收?
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。
1. 引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
存在的弊端:
A对象引用了B对象,B对象的引用计数为1
B对象也引用了A对象,A对象的引用计数也为1
这两个对象没有谁再引用他们,但是由于他们的引用计数都为1,因此不能被垃圾回收,造成内存泄漏。
2. 可达性分析算法
1、定义:
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为 GC Root ?
2、使用MemoryAnalyze分析哪些对象可以作为GC Root:
public class Demo2_2 {
public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read();
list1 = null;
System.out.println(2);
System.in.read();
System.out.println("end...");
}
}
- 在Terminal终端创建堆内存快照
1.bin
:
jmap -dump:format=b,live,file=1.bin 1063
- 在Terminal终端创建堆内存快照
3.bin
:
jmap -dump:format=b,live,file=3.bin 1063
- 在1.bin中java.Util.ArrayList可以作为GC Root对象:
- 在3.bin中java.Util.ArrayList不可以作为GC Root对象,已经被垃圾回收机制回收:
3. 五种引用
- 强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 - 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象可以配合引用队列来释放软引用自身 - 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象可以配合引用队列来释放弱引用自身
- 虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 - 终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象
3.1 软引用
public class SoftReference<T>extends Reference<T>
软引用对象,在响应内存需要时,由垃圾回收器决定是否清除此对象。
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_3 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
soft();
}
public static void soft() {
//list对SoftReference是强引用,但是SoftReference对byte[]数组是软引用
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
//创建软引用对象
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
//获取软引用对象的指示对象byte数组
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
当循环结束后,遍历软引用指向的对象,发现只保留了最后一个引用对象:
[B@6d6f6e28
1
[B@135fbaa4
2
[B@45ee12a7
3
[B@330bedb4
4
[B@2503dbd3
5
循环结束:5
null
null
null
null
[B@2503dbd3
那么现在想要做的就是如何将这些指示对象为null的软引用给清除掉?使用引用队列
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_3 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
soft();
}
public static void soft() {
//list对SoftReference是强引用,但是SoftReference对byte[]数组是软引用
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
//创建引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
//关联了引用队列,当软引用所关联的byte[]数组被回收时,软引用自己会加入到queue中
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);
//获取软引用对象的指示对象byte数组
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
//从队列中获取无用的软引用对象并释放
Reference<? extends byte[]> poll = queue.poll();
while(poll!=null){
list.remove(poll);
poll = queue.poll();
}
System.out.println("==============");
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
结果:
[B@6d6f6e28
1
[B@135fbaa4
2
[B@45ee12a7
3
[B@330bedb4
4
[B@2503dbd3
5
==============
[B@2503dbd3
3.2 弱引用
public class WeakReference<T>extends Reference<T>
弱引用对象,它们并不禁止其指示对象变得可终结,并被终结,然后被回收。
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_5 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
//每循环一次创建一个ref对象,里面包装了一个4M的byte数组
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
//将弱引用里面的byte数组打印出来
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
//循环结束后,看集合内有多少个弱引用对象
System.out.println("循环结束:" + list.size());
}
}
结果:
[B@6d6f6e28
[B@6d6f6e28 [B@135fbaa4
[B@6d6f6e28 [B@135fbaa4 [B@45ee12a7
[B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 [B@330bedb4
第四次循环时触发了一次垃圾回收
[B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 null [B@2503dbd3
第五次循环时触发了2次垃圾回收
[B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 null null [B@4b67cf4d
[B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 null null null [B@7ea987ac
[B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 null null null null [B@12a3a380
[B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 null null null null null [B@29453f44
触发了一个full Gc
null null null null null null null null null [B@5cad8086
循环结束:10
同样,如果将对象为null的弱引用回收掉,需要配合引用队列来使用。
2、垃圾回收算法
1. 标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
- 执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;需要扫描所有的对象,堆越大GC越慢。
- 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。GC次数越多,碎片越严重。
2. 标记-整理算法
“标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
缺点:
- 如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作。
3. 标记-复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:
- 如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销
- 这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。
4. 结论
标记清除算法:速度快但是会造成内存碎片
标记整理算法:速度慢,没有内存碎片
标记复制算法:没有内存碎片,但是会占用双倍的内存
3、分代垃圾回收
1. 新生代与老年代
一般将java堆划分为新生代和老年代,这样可以根据各个年代的特点采用适当的收集算法,比如新生带每次GC都会由大量的对象死去,只有少量的对象存活,就是用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
1. 新生代:
- 新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
- 当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
- HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90% (80%+10%),只有10%的内存会被“浪费”。
2. 老年代:
存放了经过一次或者多次GC还存活的对象,一般采用标记清除和标记整理算法。
2. 分代垃圾回收的工作原理
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to (因为to总是一块空的内存)
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
3. GC相关参数
3.1 长期存活的对象晋升到老年代
1、主函数什么也不写
/**
* 演示内存的分配策略
*/
public class Demo2_1 {
private static final int _512KB = 512 * 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 -XX:-ScavengeBeforeFullGC
/**
* -Xms20M 堆内存初始大小为20M
* -Xmx20M 最大堆内存为20M
* -Xmn10M 新生代内存大小为10M
* -XX:+UseSerialGC 指定垃圾回收器
* -XX:+PrintGCDetails -verbose:gc 打印GC详情
* -XX:-ScavengeBeforeFullGC
*/
public static void main(String[] args) throws InterruptedException {
}
}
结果:因为主函数中没有代码,因此不会发生垃圾回收
Heap
//新生代
def new generation total 9216K, used 2490K
//Eden区
eden space 8192K, 30% used
//Survivor From区
from space 1024K, 0% used
//Survivor To区
to space 1024K, 0% used
//老年代
tenured generation total 10240K, used 0K
the space 10240K, 0% used
2、创建一ArrayList集合,向里面存放7M内存的数组:
public class Demo2_1 {
private static final int _512KB = 512 * 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;
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
}
结果:Eden的使用率为92%
//GC代表一次Minor GC
[GC (Allocation Failure) [DefNew: 2326K->689K(9216K), 0.0046281 secs] 2326K->689K(19456K), 0.0054018 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 8267K
eden space 8192K, 92% used
from space 1024K, 67% used
to space 1024K, 0% used
tenured generation total 10240K, used 0K
the space 10240K, 0% used
3、继续向ArrayList集合中加入占用512k内存的数组:
list.add(new byte[_512KB]);
结果:Eden区使用率为98%,如果再加数组就会触发第二次垃圾回收
[GC (Allocation Failure) [DefNew: 2326K->686K(9216K), 0.0051965 secs] 2326K->686K(19456K), 0.0059651 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 8776K
eden space 8192K, 98% used
from space 1024K, 67% used
to space 1024K, 0% used
tenured generation total 10240K, used 0K
the space 10240K, 0% used
4、继续向ArrayList集合中加入占用内存为512k的数组:
list.add(new byte[_512KB]);
结果:触发了第二次的垃圾回收
//第一次的垃圾回收
[GC (Allocation Failure) [DefNew: 2158K->681K(9216K), 0.0031508 secs] 2158K->681K(19456K), 0.0038555 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//第二次的垃圾,将新生代内存:从8688K清理至515K
[GC (Allocation Failure) [DefNew: 8688K->515K(9216K), 0.0087407 secs] 8688K->8353K(19456K), 0.0088034 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 1193K
eden space 8192K, 8% used
from space 1024K, 50% used
to space 1024K, 0% used
//将一部分对象晋升到老年代(_7M)
tenured generation total 10240K, used 7837K
the space 10240K, 76% used
3.2 大对象直接晋升到老年代
1、往集合中加入一个内存大小为8M的数组,这个数组 新生代的Eden区和Survivor From区都放不下,此时就会直接晋升到老年代,不会触发垃圾回收
public class Demo2_1 {
private static final int _512KB = 512 * 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 -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
}
结果:没有触发垃圾回收,因为即便触发了垃圾回收,新生代的内存也放不小。
Heap
def new generation total 9216K, used 2490K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 30% used
from space 1024K, 0% used
to space 1024K, 0% used
//老年代的内存使用了80%
tenured generation total 10240K, used 8192K
the space 10240K, 80% used
2、如果我们往集合中放入两个内存大小为8M的数组,就会报错OutOfMemoryError(内存溢出),因为新生代和老年代都放不下:
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
结果:
//Full GC触发新生代Minor GC
[GC (Allocation Failure) [DefNew: 2326K->690K(9216K), 0.0044245 secs][Tenured: 8192K->8881K(10240K), 0.0060387 secs] 10518K->8881K(19456K), [Metaspace: 3447K->3447K(1056768K)], 0.0128145 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
//在内存溢出之前进行了一次自救,Full GC触发了新生代和老年代的垃圾回收
[Full GC (Allocation Failure) [Tenured: 8881K->8863K(10240K), 0.0051081 secs] 8881K->8863K(19456K), [Metaspace: 3447K->3447K(1056768K)], 0.0051629 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 410K)
eden space 8192K, 5% used
from space 1024K, 0% used
to space 1024K, 0% used
tenured generation total 10240K, used 8863K
the space 10240K, 86% used
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at cn.itcast.jvm.t2.Demo2_1.main(Demo2_1.java:37)