一、System.gc()方法理解
1. 实现
public final class System {
public static void gc() {
Runtime.getRuntime().gc();
}
}
2. 使用
- 该方法提醒JVM执行GC,无法保证对垃圾收集器的调用。
- 一般不用手动调用。对于特殊场景,比如正在编写一个性能基准,可以手动调用。
/**
* {@link System#gc()}方法测试
*
* @author Jinhua
* @date 2021/5/1 21:18
*/
public class SystemGc {
public static void main(String[] args) {
new SystemGc();
// 提醒JVM执行gc方法
System.gc();
// 加上该方法,一定会执行gc
// System.runFinalization();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("重写了finalize方法");
}
}
3. 对局部变量调用GC的理解
/**
* 局部变量的gc:观察buf数组的空间回收情况<p>
* VM参数:<p> 
* -XX:+PrintGCDetails
*
* @author Jinhua
* @date 2021/5/1 21:50
*/
public class LocalVarGc {
public static void main(String[] args) {
LocalVarGc gc = new LocalVarGc();
gc.gc1();
// gc.gc2();
// gc.gc3();
// gc.gc4();
// gc.gc5();
}
public void gc1() {
byte[] buf = new byte[10 * 1024 * 1024];
// 不执行GC,在该方法栈,buf还在引用
System.gc();
}
public void gc2() {
byte[] buf = new byte[10 * 1024 * 1024];
buf = null;
// 执行buf空间回收,对象无引用
System.gc();
}
public void gc3() {
{
byte[] buf = new byte[10 * 1024 * 1024];
}
// 不执行回收,buf为第二个局部变量
System.gc();
}
public void gc4() {
{
byte[] buf = new byte[10 * 1024 * 1024];
}
int value = 10;
// 执行buf空间回收。局部变量slot复用,占用索引为1的局部变量
System.gc();
}
public void gc5() {
gc1();
// 执行了buf空间回收,方法栈桢弹出,作用域失效
System.gc();
}
}
二、内存溢出与内存泄漏
1. 内存溢出
1) 必须满足的条件
- 无空闲空间;
- 垃圾收集器无法提供更多内存。
2) 相关说明
- GC一直在发展,除非占用速度大于回收速度,否则不太容易造成OOM
- 报OOM之前极大概率会执行一次独占式的Full GC(最后的挣扎)
- 在引用机制分析后,JVM会尝试回收软引用指向对象;
- java.nio.Bits.reserveMemory()方法,会调用System.gc()。
- GC不触发的例子:分配一个超过堆大小的对象,JVM可直接判断GC无法解决该问题,直接抛出OOM。
3) 可能原因
- 堆内存设置太小。
- 代码中创建大量大对象,并且长时间不能被垃圾收集器收集。
2. 内存泄漏(Memory Leak)
1) 定义
- 对象不会再被程序使用,GC却无法执行回收。
2) 相关说明
- 不是程序崩溃的直接原因。但会导致内存逐步被蚕食殆尽,最终导致OOM。
- Java的内存泄漏,存储空间不是物理内存,而是虚拟机内存,取决于磁盘交换区设定的大小。
3) 常见场景
- 单例设计模式
生命周期与应用程序一样长。
举例:Runtime类实例对象 - 一些提供close的资源未关闭
- 数据库连接
- 网络连接
- io连接
三、Stop The World——停止所有用户线程,进行垃圾回收
1. 定义
GC事件发生时候,停止用户线程,无任何响应。
2. 说明
- GC完成后恢复,频繁中断会让用户感觉是网速问题,影响体验,所以需要减少STW事件发生。
- 所有GC都有整个事件发生。垃圾回收器越优秀,则STW时间会更短。
- STW是JVM后台自动发起和自动完成的。在用户不可见的情况下挂起用户线程。
- 开发过程中不要用System.gc(),会导致Stop the world。
可达性分析算法场景
- 分析工作必须在一个保证一致性的快照中进行。
- 分析过程若引用关系还在不断变化,分析结果准确性无法保证。
四、垃圾回收的并行与并发
1. 并发(Concurrent)——同一时段内,多个应用或线程切换执行
2. 并行(Parallel)——同一时刻,多个应用都在运行
五、安全点与安全域
1. 安全点(Safe Point)
1) 概述
- 程序执行时,在特定的位置停顿下来开始GC。
- 选取很重要。
- 数量太少会导致GC等待时间过长。
- 数量太多会导致运行时性能问题。
2) 划分依据
指令是否能让程序长时间执行的特性。
- 方法调用点。
- 循环跳转。
- 异常跳转。
3) 如何到达最近安全点
- 抢先式中断(目前JVM采用了该方式)
- 中断所有线程。
- 检查每个线程是否在安全点。
- 对未在安全点的线程,恢复运行到安全点后中断。
- 主动式中断。
- 设置一个中断标志。
- 各个线程运行到安全点时主动轮询标志,
- 标志为真,则将自己进行中断挂起。
2. 安全域(Safe Region)
1) 定义
- 一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始执行GC都是安全的。
2) 问题场景
- 线程处于Sleep或Blocked状态,无法响应JVM的中断请求,不太可能被唤醒并走到安全点。
3) 实际过程
- 线程运行到Safe Region时,首先改变标识,已经入安全区域。这段时间内发生GC,JVM会忽略标识为Safe Region的线程。
- 线程离开Safe Region时,会检查JVM是否已经完成GC。
- 已完成,则继续运行。
- 否则等待,直到收到可以安全离开Safe Region的信号为止。
六、对象引用关系
希望描述这类对象:
- 内存空间足够时,能保留在内存中;
- 进行垃圾回收后,空间还是很紧张,则可以抛弃这些对象。
1. 强引用(Strong Reference)
1) 概念
普遍的代码引用赋值。
public class Main {
public static void main(String[] args) {
// 强引用
Object obj = new Object();
}
}
2) 说明
- 强引用的对象是可触及的。
- 无论什么情况下,只要强引用关系还存在,垃圾收集器就不会回收。
- 是造成Java内存泄漏的主要原因之一。
2. 软引用(Soft Reference)与 弱引用(Weak Reference)
1) 概念
- 软引用:内存不够时,将这些对象纳入回收范围进行第二次回收。回收后还没有足够内存,再抛出OOM。
- 内存不足才回收。
- 弱引用:被弱引用关联的对象,只能生存到下次垃圾回收之前。
- 与内存情况无关,发现即回收。
2) 使用场景
- 实现内存敏感的缓存。
- 例:Mybatis的一些内部类。
3) 实现
- java.lang.ref.SoftReference类来实现软引用;
- java.lang.ref.WeakReference类来实现虚引用**。
/**
* 软引用与弱引用测试:<p> 
* 1) 软引用:内存不足才回收<p> 
* 2) 弱引用:发现即回收<p>
* -Xms10M -Xmx10M
*
* @author Jinhua
* @date 2021/5/2 15:36
*/
public class SoftWeakRef {
public static void main(String[] args) {
// 方式一:
// SoftReference<User> sUser = new SoftReference<>(new User(1, "name1"));
// 方式二:
User user = new User(1, "name1");
SoftReference<User> sUser = new SoftReference<>(user);
user = null;
// gc后,能获取到
System.gc();
System.out.println("sUser.get() = " + sUser.get());
// 尝试让内存紧张
try {
byte[] b = new byte[7 * 1024 * 1024];
} catch (Throwable t) {
t.printStackTrace();
} finally {
// 内存紧张时候被回收,输出null
System.out.println(sUser.get());
}
WeakReference<User> wUser = new WeakReference<>(new User(2, "name2"));
System.out.println("wUser.get() = " + wUser.get());
System.gc();
// 无论内存是否充足,执行过GC后都会回收
System.out.println("After GC: ");
System.out.println("wUser.get() = " + wUser.get());
}
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class User {
private final int id;
private final String name;
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
}
3. 虚引用(Phantom Reference)
- 完全不影响对象的生存时间,也无法通过虚引用获得对象实例。
- 唯一目的是,对象回收跟踪。在对象被回收时,收到系统通知。
/**
* 虚引用测试
*
* @author Jinhua
* @date 2021/5/2 16:25
*/
public class PhantomRefObj {
/**
* 当前类对象
*/
public static PhantomRefObj pRefObj;
/**
* 引用队列
*/
static ReferenceQueue<PhantomRefObj> pRefQueue = null;
/**
* 检查队列的线程
*/
public static class CheckQueue extends Thread {
@Override
@SuppressWarnings("all")
public void run() {
while (true) {
if (pRefQueue != null) {
PhantomReference<PhantomRefObj> pObj = null;
try {
pObj = (PhantomReference<PhantomRefObj>) pRefQueue.remove();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
if (pObj != null) {
System.out.println("追踪垃圾回收过程成功!");
}
}
}
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("finalize方法被调用");
pRefObj = this;
}
@SneakyThrows
public static void main(String[] args) {
Thread check = new CheckQueue();
// 设置守护线程,仅剩守护线程时候,守护线程也会结束
check.setDaemon(true);
check.start();
pRefObj = new PhantomRefObj();
pRefQueue = new ReferenceQueue<>();
PhantomReference<PhantomRefObj> phantomRef = new PhantomReference<>(pRefObj, pRefQueue);
// 不可获取虚引用中的对象
System.out.println("phantomRef.get() = " + phantomRef.get());
// 去除强引用
pRefObj = null;
// 第一次gc,对象可以复活,无法回收该对象
System.gc();
Thread.sleep(1_000L);
String msg = pRefObj == null ? "pRefObj is null." : "pRefObj is available.";
System.out.println(msg);
// 去除强引用
pRefObj = null;
// 第二次gc,可以执行回收
System.gc();
Thread.sleep(1_000L);
msg = pRefObj == null ? "pRefObj is null." : "pRefObj is available.";
System.out.println(msg);
}
}
4. 终结器引用(Final Reference)
- 用于实现对象的finalize()方法
- 无需手动编码,其内部配合引用队列使用。
- GC时候,终结器引用入队。由finalizer线程通过终结器引用找到被引用对象,并且调用它的finalize()方法,第二次回收时候,才能回收被引用对象。