一、如何判断对象可以回收
1.1 引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一,当引用失效时,计数器减一,当该对象的引用计数器为 0时,我们认为该对象就不能被使用了。
效率高,但是无法解决循环引用的问题(即A 引用 B ,B 引用 A)。
1.2 可达性分析算法
以一个 GC Roots 为根节点,从这个节点往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,就证明此对象是不可用的。
可作为GC Roots的对象有:虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中类的静态属性引用的对象、方法区中常量引用的对象。
1.3 四种引用
1.3.1 强引用
被 GC ROOT 引用的对象,比如 Object o = new Object() ,只要强引用还在,JVM 就不会回收该对象。当强引用消失了,JVM才会回收该对象。
1.3.2 软引用
还有用但非必须的对象,在 JVM发生内存溢出之前,会对这些对象进行回收。
还可以配合引用队列一起来使用,即当软引用的对象被回收之后,那么软引用将被放到引用队列里面,因为软引用也是本身也是个对象,他们自身也要占用一部分内存,如果想对它占用的内存做进一步的释放那么就需要引用队列来找到它俩,然后做进一步的操作。
1.3.3 弱引用
不管 JVM内存是否存在溢出的情况,只要扫描到它都会被回收。
还可以配合引用队列一起来使用,即当虚引用的对象被回收之后,那么虚引用将被放到引用队列里面,因为虚引用也是本身也是个对象,他们自身也要占用一部分内存,如果想对它占用的内存做进一步的释放那么就需要引用队列来找到它俩,然后做进一步的操作。
1.3.4 虚引用
必须配合引用队列来使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler线程调用虚引用相关方法释放直接内存。
1.3.5 终结器引用
必须配合引用队列来使用,无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC时才能回收被引用对象。
1.3.6 强引用示例
首先设置堆内存大小为20 M ,我们下面的程序循环 5 次,每次是 4M 的内存,应该会超出 jvm内存,会报错,如下所示:
强引用是不会被回收的,内存直接就溢出了。
java
public class Test {
// 设置堆内存大小 -Xmx20m
public 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]);
}
}
}
1.3.7 软引用示例
首先设置堆内存大小为20 M ,List 不直接引用 byte 数组,而是在他们之间加了一个 SoftReference 软引用的对象,软引用的对象再间接的引用 byte数组,代码如下所示:
java
public class Test {
// 设置堆内存大小 -Xmx20m
public static final int _4MB = 4*1024*1024;
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(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());
}
}
}
可以看到程序是正常结束了,在第一次 for 循环中调用 get 方法是可以正常输出的,但是等循环结束了,我们再去循环一遍发现,数组的前 4 个元素都变成了 null ,只剩下了最后一个,添加 GC日志再次执行一遍,看下日志的输出:
bash
-Xmx20m -XX:+PrintGCDetails -verbose:gc
从日志可以看到,从第四次开始首先触发了minor gc ,发现没回收多少内存,然后又触发了 full gc,以此类推。
1.3.8 软引用配合引用队列
在1.3.2 的时候我们说过,软引用还可以搭配引用队列一起使用,来清理没有用的软引用,比如下面打印的那四个 null,其实就是没有用但还占用内存的软引用对象本身,可以通过引用队列来清除它,如下所示:
java
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class Test {
// 设置堆内存大小 -Xmx20m
public static final int _4MB = 4*1024*1024;
public static void main(String[] args) throws IOException {
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(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 通过 queue 获取无用的软引用对象,通过 queue.poll() 方法来获取,这个方法是获取最先放入队列的那个元素
Reference<? extends byte[]> poll = queue.poll();
// 只要是在这个 poll 里面的都是要回收的软引用对象
while(poll != null) {
// 在 list 里面移除掉无用的软引用对象
list.remove(poll);
// 继续在队列里获取下一个无用的对象
poll = queue.poll();
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
1.3.9 弱引用示例
首先设置堆内存大小为20 M ,List 不直接引用 byte 数组,而是在他们之间加了一个 WeakReference 软引用的对象,软引用的对象再间接的引用 byte数组,代码如下所示:
java
public class Test{
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
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[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
二、垃圾回收算法
2.1 标记清除算法 Mark Sweep
首先标记处所有需要回收的对象,然后统一回收所有标记的对象,速度快,但是会造成内存碎片。
把可用的内存占用的起始和结束的地址记录到空闲的地址列表里面就可以了,下次再分配内存的时候,就到空闲列表中去找,看看有没有一块内存可以存放这个对象。
2.2 标记整理算法 Mark Compact
把存活的对象往一端压缩(替换位置),然后清理掉可回收的对象,速度慢,但是没有内存碎片。
2.3 复制算法 Copying
将内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块用完,就把还存活的对象复制到另外一块上,再把上一块内存清理干净,大多数的 JVM都采用这种算法回收新生代。
将内存分为一块较大的 Eden 区和两块较小的 Survivor 区,每次使用 Eden 和其中一块 Survivor ,HotSpot 默认的 Eden 和 Survivor 大小比例是 8:1 ,即每次只浪费10%
三、分代垃圾回收
3.1 分代回收
JVM采用分代的垃圾回收算法,即把内存分为新生代和老年代,新生代采取复制算法,老年代采用标记压缩或标记算法。特点如下:
1、对象首先分配在 Eden区域。
2、新生代空间不足时,触发minor gc ,Eden 和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1 并且交换from to。
3、minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
4、当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。
5、当老年代空间不足,会先尝试触发minor gc ,如果之后空间仍不足,那么触发full gc ,STW的时间更长。
3.2 JVM 相关参数
|------------------|-------------------------------------------------------------|---|
| 含义 | 参数 | |
| 堆初始大小 | -Xms | |
| 堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size | |
| 新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) | |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy | |
| 幸存区比例 | -XX:SurvivorRatio=ratio | |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold | |
| 晋升详情 | -XX:+PrintTenuringDistribution | |
| GC详情 | -XX:+PrintGCDetails -verbose:gc | |
| FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC | |
3.3 GC 测试
首先运行一个空的 main 方法,看下内存的使用情况,记得需要配置 jvm 的相关参数,如下所示:
java
public class Test3 {
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
// -Xms20M -Xmx20M:初始和最大的堆内存为 20M
// -Xmn10M:新生代设置内存为 10M
// -XX:+UseSerialGC:使用 SerialGC 垃圾回收器,因为 jdk1.8 下默认的垃圾回收器不是它
// -verbose:gc -XX:-ScavengeBeforeFullGC:打印 gc 的详情
public static void main(String[] args) {
}
}
bash
Heap
# 新生代的相关信息,总共有9M多,我们明明设置了 10M,因为幸存区 to 一直要空着,是不能用的
# used 1147K 表示新生代使用了1M多
def new generation total 9216K, used 1147K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
# 分给 eden 区 8M 多,为什么用了 14%,是因为 JVM 要加载一些类
eden space 8192K, 14% used [0x00000000fec00000, 0x00000000fed1ef20, 0x00000000ff400000)
# 分给 from 1M 多
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
# 分给 to 1M 多
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
# 老年代的相关信息,还没有被使用
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
# 元空间的相关信息
Metaspace used 2767K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 299K, capacity 386K, committed 512K, reserved 1048576K
3.4 大对象直接进入老年代
java
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public class Test3 {
private static final int _8MB = 8 * 1024 * 1024;
// 放入一个 8M 的对象,他已经超过了我们 eden 区的总容量
// 直接就进入了老年代
// 不会触发垃圾回收
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
}
bash
# 没有触发 gc
Heap
# 新生代的内存容量没有发生变化
def new generation total 9216K, used 1147K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 14% used [0x00000000fec00000, 0x00000000fed1ef20, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
# 直接占用了老年代的内存空间
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 2768K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 299K, capacity 386K, committed 512K, reserved 1048576K
如果放入两个 8M的对象会发生什么?直接就内存溢出了,因为新生代和老年代都放不下了。
java
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public class Test3 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
}
如果子线程发生内存溢出,会不会影响我的主线程正常执行呢?答案是不会的,如下所示,程序目前还是没有结束。
java
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public class Test3 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep");
Thread.sleep(10000000L);
}
}
四、垃圾回收器
4.1 串行
单线程运行,适合堆内存较小的个人电脑,cpu核数多了也没用,因为就有一个线程。
4.2 吞吐量优先
多线程运行,适合堆内存较大,需要多核 cpu 支持,让单位时间内,STW的时间最短。垃圾回收时间占比最低,这样就称吞吐量高。
4.3 响应时间优先
多线程运行,适合堆内存较大,需要多核 cpu 支持,尽可能让单次 STW的时间最短。