引言
概念和作用
引用是Java中对对象进行操作的主要方式,通过引用,可以在程序中创建、访问和操作对象。
Object obj = new Object();
这里,obj
就是是一个引用,它指向一个刚创建的Object
对象。
在Java中,有着几种不同的引用类型:
- 强引用
- 软引用
- 弱引用
- 虚引用
每种引用类型在内存管理和垃圾回收方面有不同的特性和用途。
引用在Java内存管理中的重要性
在各个编程语言以及其运行框架中,内存管理是及其重要的一个功能。内存管理就会涉及到一个场景,我们怎么能确定占用这个地址的内存是能被框架回收的呢?
引用此时就发挥了重要作用,在Java中,内存管理并非交由开发者管理,而是由JVM
来进行系统性的管理的。虚拟机使用可达性算法来分析对象是否还在被引用。引用则是判断该对象能不能到达的一条路径,没有引用不可达到,则能回收。
简单示例:
public class Main {
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map = new TreeMap<>();
}
}
在第3行代码中,我们实例化了HashMap
对象,那么现在在堆中,就会有一块内存是该对象占用的,然后map
引用了这个对象。
其次在第4行代码中,我们重新赋值了map
。此时,刚刚实例化的HashMap
对象就没有被任何变量以及对象引用,在下一次的垃圾回收中,HashMap
对象就会被回收掉。
强引用(Strong Reference)
定义和特点
在Java中,通常来说只要一个对象被变量或者对象引用的话,那么两者之前的引用关系就被称为强引用。
public class Main {
private static final Object OBJECT = new Object();
public static void main(String[] args) {
Object obj = new Object();
}
}
在上面的代码中,OBJECT
与obj
都存在强引用关系。
内存管理和垃圾回收行为
要是我们没有显式的声明一个对象为null
的话,只要程序还在运行且该对象能被其他对象所使用。那么它就无法被垃圾回收,直到程序退出。
当我们在一个方法内创建一个对象时,只要该对象不会被其他变量引用时,在方法执行完后。可达性分析算法就会认为该对象能被回收,哪怕之前该对象存在强引用。
软引用(Soft Reference)
定义和用途
在SoftReference
类的注释中,提到了该类最主要的作用
Soft references are most often used to implement memory-sensitive caches.
软引用最常用于实现内存敏感缓存
另外,注释中还有着一句话:
All soft references to softly-reachable objects are guaranteed to have been cleared before the virtual machine throws an OutOfMemoryError.
在JVM
抛出OOM之前,会将所有软引用的引用对象给清除。
这句话也侧面说明了它的用途:缓存
如何创建和使用软引用
public class Main {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
SoftReference<Object> softReference = new SoftReference<>(object);
}
}
创建一个SoftReference
对象,将需要引用的对象实例化时传递即可。
软引用在内存不足时的回收机制
示例程序
启动JVM参数:
-Xms512m -Xmx512m
示例代码:
package com.zsk;
import java.lang.ref.SoftReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class Main {
public static void main(String[] args) throws InterruptedException {
int[] array = new int[(int) 1e7];
List<SoftReference<int[]>> data = new ArrayList<>();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
while (true) {
int[] arr = new int[(int) 1e7];
Thread.sleep(1000L);
SoftReference<int[]> softReference = new SoftReference<>(arr);
System.out.println("Time: " + simpleDateFormat.format(new Date()));
data.add(softReference);
}
}
}
上面的代码作用是每隔一秒,创建一个大数组对象以及创建一个软引用对象引用大数组对象,最后将软引用对象放在列表中。
内存变动
当程序启动后,我们就能得到下面这个有规律的图。
由IntelliJ IDEA生成
查看路径:Profiler
-> 选择对应的进程 -> CPU和内存实时图表
每次堆内存在即将使用完时,JVM将进行垃圾回收,此时软引用
所引用的对象就会被回收掉了。
我们也能计算出每次创建大数组对象需要的内存空间:
10000000(size)* 4(int占用字节)= 40000000byte ≈ 38.15MB
该数量与上方内存增长大小与回收后的空间也是一致的。
内存分析
我们也可以MAT内存分析工具来进一步佐证。
首先,我们需要对代码进行一些处理,在垃圾回收前以及垃圾回收后的内存快照(hprof文件)给保存下来
改动后的代码:
public class Main {
public static void main(String[] args) throws InterruptedException {
int[] array = new int[(int) 1e7];
List<SoftReference<int[]>> data = new ArrayList<>();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
int i = 0;
while (true) {
if (i++ == 10) {
Thread.sleep(10000L);
}
if (i > 10) {
Thread.sleep(10000L);
} else {
Thread.sleep(1000L);
}
int[] arr = new int[(int) 1e7];
SoftReference<int[]> softReference = new SoftReference<>(arr);
System.out.println("Time: " + simpleDateFormat.format(new Date()));
data.add(softReference);
}
}
}
垃圾回收变化以及回收机制
垃圾回收前的内存占用情况:
垃圾回收后的内存占用情况:
可以清楚的看到了ArrayList
中的软引用的引用对象(大数组)都被回收掉了,也验证了该类上的注释:在抛出OOM
之前,也就是堆内存使用完之前,将所有的弱引用的引用对象给回收。
题外话
看到对比图,可能有个疑问,为什么ArrayList
中弱引用对象的地址变动了呢?
在JDK进行垃圾回收时,根据不同垃圾回收器使用的垃圾回收算法,会进行内存空间的整理。
我们可以在运行前,添加JVM参数,就能打印出使用的垃圾回收器。
增加JVM启动参数:-Xloggc:.\gc.log
就能在gc.log这个文件中,看到所使用的垃圾回收器类型以及一些启动信息
OpenJDK 64-Bit Server VM (25.392-b08) for windows-amd64 JRE (1.8.0_392-b08), built on Oct 16 2023 22:02:46 by "Administrator" with MS VC++ 15.9 (VS2017)
Memory: 4k page, physical 16726872k(8454788k free), swap 17775448k(3601532k free)
CommandLine flags: -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:+PrintGC
-XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
由于示例代码在运行前没有指定垃圾回收器,就是默认使用UseParallelGC
并行垃圾回收器。
并行垃圾回收器使用的是标记-复制
算法。
该算法会进行三步:
- 标记:标记未被引用对象(不可达算法),其中只被软引用引用的对象也会被标记出来。
- 复制:
ParallelGC
将存活的对象从Eden
区和from
区复制到to
区,内存地址变动也是因为这个复制所作的操作。 - 清理:
ParallelGC
会清理掉年轻代中不再使用的对象。