在JVM专题七:JVM垃圾回收机制中提到JVM的垃圾回收机制是一个自动化的后台进程,它通过周期性地检查和回收不可达的对象(垃圾),帮助管理内存资源,确保应用程序的高效运行。今天就让我们来看看JVM到底是怎么定义“垃圾”对象的。
JVM垃圾对象判断方法
JVM在垃圾回收(Garbage Collection简称GC)过程中定义“垃圾”对象,主要是根据对象是否还可达来判断。以下是一些判断对象是否成为垃圾的常见标准:
-
引用计数法:这是一种简单的垃圾回收方法,每个对象都有一个引用计数器,每当有引用指向该对象时,计数器加一;每当引用离开作用域或被赋值为null时,计数器减一。当引用计数器为零时,对象被认为是垃圾。但这种方法无法处理循环引用的问题。
-
可达性分析:这是现代JVM中常用的垃圾回收方法。从一系列的“GC Roots”开始,所有能够通过引用链到达的对象都被认为是存活的,而那些无法到达的对象则被认为是垃圾。GC Roots通常包括:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 运行时常量池中引用的对象
- JNI(Java Native Interface)的引用对象
-
不可达的对象:即使对象不可达,JVM也不会立即回收它们。JVM会将这些对象标记为“即将回收”的状态。在下一次垃圾回收周期中,这些对象会被真正回收。
-
对象的finalize()方法:如果对象在被标记为垃圾之前定义了
finalize()
方法,JVM会给予对象最后一次“自救”的机会。在下一次垃圾回收时,JVM会尝试调用这个对象的finalize()
方法。如果对象在finalize()
方法中重新与GC Roots建立了连接,它将被移出即将回收的列表。 -
分代收集:JVM通常采用分代收集算法,将对象分为新生代和老年代。新生代中的对象通常存活时间较短,而老年代中的对象存活时间较长。JVM会根据对象的年龄来决定它们应该在哪个区域。
垃圾回收是一个复杂的过程,JVM会根据当前的内存使用情况和对象的生命周期来决定何时以及如何回收对象。
JVM回收对象示例
局部变量作为GC Roots
public class App {
public static void main(String[] args) {
runApp(args);
System.out.println("====");
}
private static void runApp(String[] args) {
SpringApplication sApp = new SpringApplication();
sApp.run(App.class, args);
// sApp.getRunListeners(args);
}
}
public class SpringApplication {
public String run(Class appClass, String[] args) {
System.out.println("my Spring Application ");
return "run args";
}
public String getRunListeners( String[] args) {
System.out.println("My getRunListeners ");
String myRunListenersVar = "myRunListenersVar";
this.getSpringFactoriesInstances(args);
return "run args";
}
public String getSpringFactoriesInstances( String[] args) {
System.out.println("My getSpringFactoriesInstances ");
String mySpringFactoriesInstancesVar = "mySpringFactoriesInstances";
return "run args";
}
}
上述代码中,最关键的一段是 SpringApplication sApp = new SpringApplication();代码分析如下图所示:
JVM垃圾回收线程扫描到Spring Appliedcation发现它有一个GC Roots,此时就不能回收了。
静态变量作为GC Roots
除了上述代码外,咱们在看一个常见的代码。
public class App {
public static SpringApplication sApp = new SpringApplication();
public static void main(String[] args) {
runApp(args);
System.out.println("====");
}
private static void runApp(String[] args) {
SpringApplication sApp = new SpringApplication();
sApp.run(App.class, args);
}
}
上述 public static SpringApplication sApp = new SpringApplication() 代码分析如下图所示:
JVM垃圾回收线程扫描到Spring Appliedcation发现它有一个GC Roots,此时就不能回收了。相信有细心的小伙伴注意到这块代码与上述代码在示意图上的区别了,他们GC Roots 所在区域不一样,一个是在JVM栈中,一个是在JVM的方法区且特意把颜色换成同堆内存一样的颜色,其实就是想说这块区域是共享内存,而局部变量是线程内存。
因此这里留一个小小的思考问题:大家直观感受下这两个引用谁引用持有的时间会更长一点?
简单总结,只要对象被局部变量或静态变量引用就不会被回收。
Java中不同对象类型
我们在进行可达性分析的时候常常提到局部变量持有某个对象的引用,或者静态变量持有某个对象引用,但是我们脑海里需要清楚一个概率那就是java中有不同的引用类型,他们分别是强引用、软引用、弱引用和虚引用。
强引用
强引用是最常见的引用类型,它使得对象在任何情况下都不会被垃圾回收,除非显式地将引用设置为null
或超出作用域。如下代码所示:
public class App {
public static SpringApplication app = new SpringApplication();
}
软引用
软引用提供了一种在内存不足时可以被回收的机制。它们通常用于缓存,当内存足够时,对象不会被回收,但如果内存不足,垃圾回收器会尝试回收这些对象。使用软引用可以提高应用的内存效率,但需要权衡对象被回收的概率.
import java.lang.ref.SoftReference;
// ...
public class ConfigurationPropertyUtils {
// ...
private static final SoftReference<ConfigurationPropertyCache> cache =
new SoftReference<>(null);
public static <T> T getCached(BeanFactory beanFactory, Class<T> cacheType) {
// ...
SoftReference<ConfigurationPropertyCache> ref = cache;
ConfigurationPropertyCache cpCache = (ref != null ? ref.get() : null);
if (cpCache == null) {
cpCache = new ConfigurationPropertyCache(beanFactory, cacheType);
cache = new SoftReference<>(cpCache);
}
// ...
return cpCache.getProperty();
}
// ...
}
在这段代码中,ConfigurationPropertyUtils
类使用了一个静态的软引用cache
来存储配置属性的缓存。当调用getCached
方法时,如果缓存不存在或已被垃圾回收,就会创建一个新的ConfigurationPropertyCache
实例,并用软引用包装后存储起来。由于软引用对象在内存不足时会被回收,这有助于减少垃圾回收器的压力,避免长时间的GC停顿,提高应用的响应性。
所以在软引用放到缓存这个场景就非常实用,想想内存充足我就不回收,内存不够了我回收而缓存基本上都是为了提升性能的,现在内存都不足了还有啥自行车,能运行就不错啦 !
弱引用
弱引用不会阻止垃圾回收器回收对象,即使内存充足也会被回收。它们通常用于监听或观察对象的状态,而不阻止对象的回收。弱引用在内存不足时更有可能被回收,因为它们对对象的生命周期没有任何控制。
public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
private final Object poolSizeMonitor = new Object();
private int corePoolSize = 1;
private int maxPoolSize = Integer.MAX_VALUE;
private int keepAliveSeconds = 60;
private int queueCapacity = Integer.MAX_VALUE;
private boolean allowCoreThreadTimeOut = false;
@Nullable
private TaskDecorator taskDecorator;
@Nullable
private ThreadPoolExecutor threadPoolExecutor;
// Runnable decorator to user-level FutureTask, if different
// 聚焦这里哈
private final Map<Runnable, Object> decoratedTaskMap =
new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
}
上述代码是Spring的ThreadPoolTaskExecutor节选部分,注意ThreadPoolTaskExecutor
本身并不直接使用弱引用来引用任务对象。它主要使用弱引用来缓存线程池中的Future
任务,以便在内存不足时可以回收这些不再需要的Future
对象。这样做可以避免因缓存大量已完成或取消的任务的Future
引用而导致的内存泄漏问题。
一个对象被线程池持有强引用,那么即使这个对象不再被其他地方使用,它也不会被垃圾回收器回收,因为线程池中的引用阻止了垃圾回收。使用弱引用可以确保当没有其他强引用存在时,线程池中的任务可以被垃圾回收。
虚引用
虚引用是最弱的引用类型,它们几乎不提供任何保护,对象可以随时被回收。虚引用的主要作用是跟踪对象被回收的状态,通过ReferenceQueue
来接收通知。
因为虚引用接触比较少,暂时就不讨论了,下面对他们四个应用简单总结下:
- 强引用:只要强引用存在,对象就不会被垃圾回收。
- 软引用:内存不足时,软引用对象会被垃圾回收,适用于缓存等场景。
- 弱引用:弱引用对象在下一次垃圾回收时会被回收,适合用于监听或观察对象状态。
- 虚引用:虚引用几乎不阻止对象回收,主要用于跟踪对象被垃圾回收的状态
到这里,我们已经Java介绍完垃圾对象的判断方法,并举出例子演示给大家看;同样提出几个问题:我们在介绍完GC Roots的时候分为不同类型的时候提出问题,他们引用时间的长短对于引用时间长短不同可以等价不同的对象实例存活时间。不同对象回收的机制一样嘛?如果不一样根据什么特点区分呢?回收以后内存空间要怎么整理呢?本章结束:有时间小伙伴可以回头看看,前面的文章回忆下各个区域的特点和整个分配的过程。从下一章我们将开始慢慢介绍GC算法,需要前面的铺垫。
专题汇总
JVM专题一:深入分析Java工作机制
JVM专题二:Java如何进行编译的
JVM专题三:Java代码如何运行
JVM专题四:JVM的类加载机制
JVM专题五:类加载器与双亲委派机制
JVM专题六:JVM的内存模型
JVM专题七:JVM垃圾回收机制
JVM专题八:JVM如何判断可回收对象
JVM专题九:JVM分代知识点梳理
JVM专题十:JVM中的垃圾回收机制
JVM专题十一:JVM 中的收集器一
JVM专题十二:JVM 中的收集器二
JVM专题十三:总结与整理(面试常用)