java数据类型详解篇
1、8种基本数据类型
数据类型 | 分类 | 字节数 | 内存位数 | 是否最高位为符号位 (0正数1负数) | 取值范围(数值形式) | 取值说明 |
---|---|---|---|---|---|---|
byte | 整数类型 | 1 | 8 | 是 | -128 ~ 127 | -2^7 ~ 2^7 - 1 (幂形式) |
short | 整数类型 | 2 | 16 | 是 | -32,768 ~ 32,767 | -2^15 ~ 2^15 - 1 (幂形式) |
int | 整数类型 | 4 | 32 | 是 | -2,147,483,648 ~ 2,147,483,647 | -2^31 ~ 2^31 - 1 (幂形式) |
long | 整数类型 | 8 | 64 | 是 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 | -2^63 ~ 2^63 -1 (幂形式) |
float | 浮点类型(单精度) | 4 | 32 | 是 | 正数:1.4e-45 ~ 3.402e38 负数:-3.4028235e38 ~ -1.4e-45 | 6~7位有效十进制小数位 |
double | 浮点类型(双精度) | 8 | 64 | 是 | 正数:4.9e-324 ~ 1.797e308 负数:-1.797e308 ~ 4.9e-324 | 15位十进制小数位 |
char | Unicode字符类型 | 2 | 16 | 否 | 0 ~ 65,535 | 0 ~ 2^16 -1 与 Unicode 编码的直接对应,如下 char c = 65; // 输出的不是65而是A字符 System.out.println©; |
boolean | 布尔类型 | 无固定 | 无固定 | 否 | 无固定 | 无固定,实际占用取决于 JVM 实现和使用场景 |
根据JVM的内存模型,基本数据类型存储位置取决于声明位置和使用方式:
- 方法内部声明,则存储在栈里
- 在实例类中声明,则存储在堆里
2、引用数据类型
1、自定义类或java api提供的类
存储对象的引用(内存地址),指向实际堆存储的位置。
Date now = new Date(); // 日期类
Person p = new Person(); // 自定义类实例
class Person {String name;int age;
}
字符串String 特别说明:
String有缓存机制,主要通过 字符串常量池(String Pool) 实现,相同内容的字符串只存储一份,后续重复使用直接引用池中的对象。
字面量赋值(自动入池)
String s1 = "Fly"; // 第一次创建,存入常量池
String s2 = "Fly"; // 直接复用常量池中的对象
System.out.println(s1 == s2); // true(地址相同)
显式调用 intern()(手动入池)
String s3 = new String("Fly"); // 在堆中创建新对象
String s4 = s3.intern(); // 将s3内容加入常量池(若池中已有则返回引用)
System.out.println(s1 == s4); // true(s4指向常量池对象)
new String() 不触发自动缓存
String s5 = new String("Fly"); // 强制在堆中创建新对象
String s6 = new String("Fly"); // 另一个新对象
System.out.println(s5 == s6); // false(地址不同)
System.out.println(s1 == s5); // false(常量池 vs 堆新对象)
注意:运行时拼接(不触发缓存)
// 示例1:编译期优化(字面量拼接)
String a = "Fly" + "Fish"; // 编译后自动合并为 "FlyFish",复用常量池对象// 示例2:运行时拼接(不触发缓存)// 循环内拼接字符串用 StringBuilder,避免生成大量中间对象
String b = "Fly";
String c = b + "Fish"; // 运行时在堆中生成新对象
String d = "FlyFish";
System.out.println(a == d); // true(a、d在常量池)
System.out.println(c == d); // false(c在堆,d在常量池)
使用缓存减少对象创建开销,加速字符串比较(==
比 equals()
快),缓存机制下 ==
有时有效,但非字面量字符串比较必须用 equals()
。
在java8之前内部使用 char[](字符数组)存储数据,java9及之后为节省内存,改为 byte[] + 编码标志(Latin-1 或 UTF-16),但逻辑上仍等价于字符序列。
2、接口类
存储对象的引用(内存地址),指向实际堆存储的位置。
interface Drawable {void draw();
}class Circle implements Drawable {public void draw() {System.out.println("Drawing circle");}
}Drawable d = new Circle(); // 接口引用指向实现类
3、数组类型
数组如果是引用对象存储对象的引用(内存地址),指向实际堆存储的位置。
数组如果是基本数据类型,则按基本数据类型存储在一致。
int[] numbers = {1, 2, 3}; // 基本类型数组
String[] names = new String[5]; // 引用类型数组
int[][] matrix = {{1,2}, {3,4}}; // 多维数组
4、枚举类型
引用对象存储对象的引用(内存地址),指向实际堆存储的位置。
enum Color {RED, GREEN, BLUE
}
Color c = Color.RED; // 枚举引用
5、注解类型
引用对象存储对象的引用(内存地址),指向实际堆存储的位置。
@Retention(RetentionPolicy.RUNTIME)
@interface Author {String name();int version() default 1;
}@Author(name = "John")
class MyClass {...}
6、集合框架
List<String> list = new ArrayList<>(); // 有序列表
Set<Integer> set = new HashSet<>(); // 唯一值集合
Map<String, Integer> map = new HashMap<>(); // 键值对映射
对象存储对象的引用(内存地址),指向实际堆存储的位置。
7、8中基本数据类型对应的包装类
基本类型 | 包装类 | 自动装箱/拆箱 | 值缓存 | 比较陷阱(建议始终用equals比较包装对象) |
---|---|---|---|---|
byte | Byte | 类似 | -128 ~ 127 | Integer x = 100, y = 100; System.out.println(x == y); // true(缓存内) Integer m = 200, n = 200; System.out.println(m == n); // false(缓存外) |
short | Short | 类似 | -128 ~ 127 | 类似Integer |
int | Integer | Integer num = 42; // 自动装箱 (int → Integer) int value = num; // 自动拆箱 (Integer → int) | -128 ~ 127 | 类似Integer |
long | Long | 类似 | -128 ~ 127 | 类似Integer |
float | Float | 自动装箱都创建新对象。 | 无缓存 | 无 |
double | Double | 自动装箱都创建新对象。 | 无缓存 | 无 |
char | Character | 类似 | ** 0 ~ 127**(ASCII字符范围) | 类似Integer |
boolean | Boolean | 仅有两个静态实例Boolean.TRUE 和Boolean.FALSE ,所有自动装箱均复用它们。 | 无 | 无 |
3、四大引用类型
GC回收时机
一、强引用
说明
强引用是 Java 中最常见、最默认的引用类型。只要一个对象被至少一个强引用指向,垃圾收集器 (GC) 就绝对不会回收它。只有当所有指向该对象的强引用都断开(被设置为 null
或超出作用域)后,该对象才会变得可回收。
使用场景举例
public class StrongReferenceExample {public static void main(String[] args) {// 1. 创建一个新的 Person 对象 "Alice"// alice 是一个强引用,指向堆内存中创建的 Person("Alice") 对象Person alice = new Person("Alice"); // 强引用建立!System.out.println("Alice created: " + alice.getName()); // 输出: Alice created: Alice// 2. 让 alice 引用指向一个 *新* 的 Person 对象 "Bob"// 现在,第一个 Person("Alice") 对象失去了它唯一的强引用 (alice 不再指向它)alice = new Person("Bob"); // 强引用指向了新对象,"Alice" 的强引用断开!System.out.println("Now Alice refers to: " + alice.getName()); // 输出: Now Alice refers to: Bob// 3. 显式触发垃圾收集 (注意:这只是建议,GC 不保证立即执行)for (int i = 0; i < 3; i++) {System.gc();}// 4. 给 GC 一点时间运行 (实际应用中通常不需要这样),GC回收后日志即可看到:!!! GC is collecting Person: Alice !!!try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Program finished.");}}class Person {private String name;public Person(String name) {this.name = name;System.out.println("Person constructor: " + name + " created in memory.");}public String getName() {return name;}// 重点:覆写 finalize 方法 (不推荐用于生产,仅用于演示GC行为)@Overrideprotected void finalize() throws Throwable {System.out.println("!!! GC is collecting Person: " + name + " !!!");super.finalize();}}
二、软引用
说明
通过SoftReference
类创建,只有当内存不足时,才会被回收,所以比弱引用的生命周期更长点,适合实现内存敏感的缓存,如图片缓存等。这些缓存对象在内存充足时可以提升性能,但在内存紧张时会被自动回收,为更重要的对象腾出空间,避免 OOM
JVM 不会一次性回收所有软引用对象。回收过程是有策略、渐进式的,具体行为取决于 JVM 的实现和垃圾回收算法。
- 按需回收:只回收足够多的软引用对象来缓解当前内存压力,而非一次性全部回收。
- 策略性回收:JVM 可能根据内部策略(如最近最少使用 LRU)选择性地回收部分软引用对象。如HotSpot 虚拟机使用类似 LRU(Least Recently Used)的策略,在每个软引用对象关联一个“时间戳”,记录最近一次被访问(通过
get()
方法)的时间。内存不足时,优先回收最久未被使用的软引用对象。回收会持续进行,直到释放的内存满足需求或没有更多可回收的软引用。不同 JVM 版本或供应商(如 OpenJDK、Oracle JDK)的实现策略可能不同。 - JVM的垃圾回收算法:软引用对象可能分布在新生代(Young Gen)或老年代(Old Gen)。触发 Full GC(回收整个堆)时,才会全面扫描并回收老年代中的软引用。如果 Minor GC(回收新生代)后内存仍然不足,可能直接触发 Full GC 来回收老年代的软引用。
使用场景举例
适合实现内存敏感的缓存,如图片缓存等。这些缓存对象在内存充足时可以提升性能,但在内存紧张时会被自动回收,为更重要的对象腾出空间,避免 OOM
/*** 软引用例子*/
public class SoftReferenceExample {// 核心缓存结构:文件路径 -> 文件内容的软引用private final Map<String, SoftReference<String>> cache = new HashMap<>();// 引用队列,用于跟踪哪些软引用已被GC回收(内容已被清除)private final ReferenceQueue<String> refQueue = new ReferenceQueue<>();// 从缓存获取文件内容(如果存在且未被回收),否则从磁盘读取并缓存public String getFileContent(String filePath) {// 1. 清理已被GC回收的缓存条目evictCollectedEntries();// 2. 尝试从缓存中获取软引用SoftReference<String> softRef = cache.get(filePath);// 3. 如果软引用存在,尝试获取其引用的实际内容String content = null;if (softRef != null) {content = softRef.get(); // get() 方法获取被引用的对象,如果已被GC则返回null}// 4. 如果从软引用成功获取到内容(content != null),直接返回缓存内容if (content != null) {System.out.println("Retrieved from cache: " + filePath);return content;}// 5. 缓存未命中(软引用不存在,或软引用存在但其内容已被GC回收)System.out.println("Cache miss (or collected). Reading from disk: " + filePath);// 模拟从磁盘读取文件内容 (实际应用中替换为真实IO操作)content = readFileFromDisk(filePath);// 6. 将新读取的内容用软引用包装,并与文件路径关联放入缓存// 同时注册引用队列,以便后续知道该引用何时被回收softRef = new SoftReference<>(content, refQueue);cache.put(filePath, softRef);return content;}// 清理那些已经被垃圾回收器回收了内容的软引用条目private void evictCollectedEntries() {SoftReference<? extends String> clearedRef;// 从引用队列中取出所有已被GC回收的软引用while ((clearedRef = (SoftReference<? extends String>) refQueue.poll()) != null) {// 遍历缓存,找到这个被回收的软引用对应的条目并移除// (注意:这里简单遍历,实际高效实现可能需要反向映射或其他结构)SoftReference<? extends String> finalClearedRef = clearedRef;cache.entrySet().removeIf(entry -> entry.getValue() == finalClearedRef);System.out.println("Evicted collected reference from cache.");}}// 模拟从磁盘读取文件(简单返回一个模拟的大字符串)private String readFileFromDisk(String filePath) {// 模拟读取大文件:创建一个较大的字符串StringBuilder sb = new StringBuilder();sb.append("Content of file: ").append(filePath).append("\n");for (int i = 0; i < 10000; i++) { // 增加字符串大小以模拟大文件sb.append("This is line ").append(i).append(" in the file.\n");}return sb.toString();}// 测试主方法public static void main(String[] args) {SoftReferenceExample fileCache = new SoftReferenceExample();// 第一次读取文件A - 会从磁盘读取并缓存String contentA1 = fileCache.getFileContent("fileA.txt");System.out.println("Content A length: " + contentA1.length());// 第二次读取文件A - 应该从缓存命中String contentA2 = fileCache.getFileContent("fileA.txt");System.out.println("Content A length (again): " + contentA2.length());// 模拟内存压力:尝试缓存多个大文件System.out.println("\nSimulating memory pressure by caching many files...");for (int i = 0; i < 100; i++) {fileCache.getFileContent("large_file_" + i + ".dat");}System.out.println("Cached many large files.");// 尝试再次读取文件A - 可能命中,也可能被回收了需要重新读取System.out.println("\nTrying to access fileA.txt again...");String contentA3 = fileCache.getFileContent("fileA.txt");System.out.println("Content A length (after pressure): " + (contentA3 != null ? contentA3.length() : "null"));}
}
三、弱引用
说明
通过WeakReference
类创建的,下次GC时无论内存是否充足,只要对象仅被弱引用指向(没有强引用或软引用),无论内存是否充足,该对象都会被回收。
使用场景举例
ThreadLocal内部实现的key即ThreadLocal本身就是用的弱引用,具体查看文章 java线程变量ThreadLocal用法篇
通常用在临时缓存的场景使用,WeakHashMap举例如下
static class Config {private final String value;private final byte[] payload; // 增加内存占用public Config(String value) {this.value = value;this.payload = new byte[12048]; // 每个配置对象占用2KB}}/*** 实例本身不可回收,生命周期与类加载器绑定,当键失去所有外部强引用时,GC时即可回收*/private static final WeakHashMap<Object, Config> cache = new WeakHashMap<>();private static final WeakHashMap<Object, Config> cache2 = new WeakHashMap<>();// 内存监控工具private static final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();// 打印内存使用情况private static void printMemoryStats(String label) {// 获取堆内存使用情况MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();long usedHeap = heapUsage.getUsed() / 1024; // KB// 获取非堆内存使用情况MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage();long usedNonHeap = nonHeapUsage.getUsed() / 1024; // KBSystem.out.printf("[%s] 堆内存: %d KB | 非堆内存: %d KB | cache大小: %d | cache2大小: %d%n",label, usedHeap, usedNonHeap, cache.size(), cache2.size());}/*** 这种方式字符串在常量池中,所以key存在强引用,所以key不会被回收*/static void testStringLiteral() {System.out.println("\n===== 开始测试字符串字面量 =====");printMemoryStats("测试前");String key1 = "key1";String key2 = "key2";String key3 = "key3";String key4 = "key4";String key5 = "key5";String key6 = "key6";String key7 = "key7";String key8 = "key8";String key9 = "key9";String key10 = "key10";cache.putIfAbsent(key1, new Config("value_key1")); // 1KBcache.putIfAbsent(key2, new Config("value_key2")); // 1KBprintMemoryStats("添加中 " + 2);cache.putIfAbsent(key3, new Config("value_key3")); // 1KBcache.putIfAbsent(key4, new Config("value_key4")); // 1KBprintMemoryStats("添加中 " + 4);cache.putIfAbsent(key5, new Config("value_key5")); // 1KBcache.putIfAbsent(key6, new Config("value_key6")); // 1KBprintMemoryStats("添加中 " + 6);cache.putIfAbsent(key7, new Config("value_key7")); // 1KBcache.putIfAbsent(key8, new Config("value_key8")); // 1KBprintMemoryStats("添加中 " + 8);cache.putIfAbsent(key9, new Config("value_key9")); // 1KBcache.putIfAbsent(key10, new Config("value_key10")); // 1KBprintMemoryStats("添加中 " + 10);printMemoryStats("添加后");System.out.println("字面量测试完成");}/*** 使用 new String()在堆中创建的新对象,不存在强引用所以key可以回收,value值对象将失去来自 Map 的强引用* 没有引用的value对象即可被GC回收。* 正常业务使用具体对象*/static void testNewString() {System.out.println("\n===== 开始测试 new String() =====");printMemoryStats("测试前");for (int i = 0; i < 10; i++) {// 使用 new String() - 可回收cache2.putIfAbsent(new String("key" + i), new Config("value"+i)); // 1KB// 每1000次打印一次内存if (i % 2 == 0) {printMemoryStats("添加中 " + i);}}printMemoryStats("添加后");System.out.println("new String() 测试完成");}/*** 执行GC并等待清理完成*/private static void forceGCAndWait() {System.out.println("\n触发GC...");printMemoryStats("GC前");// 多次触发GC确保执行for (int i = 0; i < 3; i++) {System.gc();try {Thread.sleep(200); // 给GC时间执行} catch (InterruptedException e) {Thread.currentThread().interrupt();}}printMemoryStats("GC后");}/**** [程序启动] 堆内存: 4096 KB | 非堆内存: 4038 KB | cache大小: 0 | cache2大小: 0** ===== 开始测试字符串字面量 =====* [测试前] 堆内存: 4096 KB | 非堆内存: 4229 KB | cache大小: 0 | cache2大小: 0* [添加中 2] 堆内存: 4096 KB | 非堆内存: 4234 KB | cache大小: 2 | cache2大小: 0* [添加中 4] 堆内存: 4096 KB | 非堆内存: 4238 KB | cache大小: 4 | cache2大小: 0* [添加中 6] 堆内存: 4096 KB | 非堆内存: 4239 KB | cache大小: 6 | cache2大小: 0* [添加中 8] 堆内存: 4096 KB | 非堆内存: 4244 KB | cache大小: 8 | cache2大小: 0* [添加中 10] 堆内存: 4096 KB | 非堆内存: 4246 KB | cache大小: 10 | cache2大小: 0* [添加后] 堆内存: 4096 KB | 非堆内存: 4246 KB | cache大小: 10 | cache2大小: 0* 字面量测试完成** 触发GC...* [GC前] 堆内存: 4096 KB | 非堆内存: 4247 KB | cache大小: 10 | cache2大小: 0* [GC后] 堆内存: 4076 KB | 非堆内存: 4250 KB | cache大小: 10 | cache2大小: 0** ===== 开始测试 new String() =====* [测试前] 堆内存: 4076 KB | 非堆内存: 4254 KB | cache大小: 10 | cache2大小: 0* [添加中 0] 堆内存: 4076 KB | 非堆内存: 4402 KB | cache大小: 10 | cache2大小: 1* [添加中 2] 堆内存: 4076 KB | 非堆内存: 4408 KB | cache大小: 10 | cache2大小: 3* [添加中 4] 堆内存: 4076 KB | 非堆内存: 4410 KB | cache大小: 10 | cache2大小: 5* [添加中 6] 堆内存: 4076 KB | 非堆内存: 4411 KB | cache大小: 10 | cache2大小: 7* [添加中 8] 堆内存: 4076 KB | 非堆内存: 4413 KB | cache大小: 10 | cache2大小: 9* [添加后] 堆内存: 4076 KB | 非堆内存: 4416 KB | cache大小: 10 | cache2大小: 10* new String() 测试完成** 触发GC...* [GC前] 堆内存: 4076 KB | 非堆内存: 4420 KB | cache大小: 10 | cache2大小: 10* [GC后] 堆内存: 4956 KB | 非堆内存: 4899 KB | cache大小: 10 | cache2大小: 0** ===== 最终内存报告 =====* [最终状态] 堆内存: 4956 KB | 非堆内存: 4900 KB | cache大小: 10 | cache2大小: 0* cache 大小: 10* cache2 大小: 0* @param args*/public static void main(String[] args) {printMemoryStats("程序启动");testStringLiteral(); // 内存持续增长forceGCAndWait();testNewString(); // 内存稳定forceGCAndWait();// 最终报告System.out.println("\n===== 最终内存报告 =====");printMemoryStats("最终状态");System.out.println("cache 大小: " + cache.size());System.out.println("cache2 大小: " + cache2.size());}
四、虚引用
说明
虚引用是4个引用中弱最的引用类型,它通过PhantomReference
类创建的,并且必须需要配合ReferenceQueue队列使用
,通过虚引用的get()获取对象总是返回null,在对象被GC回收时ReferenceQueue队列可以收到回收通知,如下
public static void main(String[] args) throws InterruptedException { // 1. 创建引用队列(用于接收被回收对象的虚引用)ReferenceQueue<MyResource> queue = new ReferenceQueue<>();// 2. 创建资源对象MyResource myResource = new MyResource("重要资源");// 3. 创建虚引用,关联资源对象和引用队列PhantomReference<MyResource> phantomRef =new PhantomReference<>(myResource, queue);System.out.println("初始状态:");System.out.println(" 资源对象: " + myResource);System.out.println(" 虚引用是否指向对象: " + (phantomRef.get() != null)); // 虚引用总是返回nullSystem.out.println(" 引用队列是否有数据: " + (queue.poll() != null));System.out.println();// 4. 断开强引用,使资源对象可被回收myResource = null;// 5. 请求垃圾回收(注意:这只是建议,不保证立即执行),多次请求GC(增加成功率)for (int i = 0; i < 3; i++) {System.gc();System.runFinalization();}// 6. 给GC一点时间执行Thread.sleep(500);System.out.println("GC后状态:");System.out.println(" 虚引用是否指向对象: " + (phantomRef.get() != null));// 7. 检查引用队列(虚引用会在对象回收后被加入队列),阻塞方式等待虚引用入队(最多等待2秒)PhantomReference<?> refFromQueue = (PhantomReference<?>) queue.remove(2000);if (refFromQueue != null) {System.out.println("✅ 检测到资源已被回收,虚引用进入队列");System.out.println(" 队列中的引用: " + refFromQueue);System.out.println(" 是否与原始虚引用相同: " + (refFromQueue == phantomRef));// 这里可以执行资源清理操作// resource 普通堆内存对象,JVM 自动回收这个对象占用的堆内存,虚引用入队只是通知你"对象已被回收,不需要手动释放堆内存// System.out.println("执行清理操作:释放资源关联的内存...");} else {System.out.println("❌ 超过等待时间仍未检测到资源回收");System.out.println("可能原因:");System.out.println("1. GC尚未执行完成");System.out.println("2. 对象仍有强引用");System.out.println("3. JVM忽略了System.gc()");}}static class MyResource {private final String name;public MyResource(String name) {this.name = name;System.out.println("创建资源: " + name);}@Overrideprotected void finalize() throws Throwable {System.out.println("🔥 垃圾回收器正在回收资源: " + name);super.finalize();}}
使用场景举例
在精确控制资源释放的场景经常使用,在日常业务开发中较少直接使用,但在基础框架、中间件、高性能库中(如Netty、JDK NIO等)有不可替代的作用
在虚引用机制中,虚引用本身不负责释放内存,它的核心作用是提供对象被回收的通知时机,让开发者有机会执行自定义的清理逻辑。是否需要手动释放内存取决于资源的类型,如下:
内存类型 | 释放责任方 | 虚引用中的作用 |
---|---|---|
堆内存 | JVM自动回收 | 无需处理 |
堆外内存 | 开发者手动释放 | 在清理回调中释放 |
其他资源 | 开发者手动释放 | 在清理回调中关闭/释放 |
例子:
/*** 虚引用例子*/
public class PhantomReferenceExample2 {// 资源清理接口@FunctionalInterfacepublic interface ResourceCleaner {void clean();}// 自定义虚引用(携带清理逻辑)private static class MemoryReference extends PhantomReference<ByteBuffer> {private final ResourceCleaner cleaner;private final Object jdkCleaner; // 改为 Object 类型private final int size;public MemoryReference(ByteBuffer referent,ReferenceQueue<? super ByteBuffer> queue,ResourceCleaner cleaner,Object jdkCleaner, // 改为 Objectint size) {super(referent, queue);this.cleaner = cleaner;this.jdkCleaner = jdkCleaner;this.size = size;}public void clean() {cleaner.clean(); // 执行自定义清理逻辑cleanMemory(); // 实际释放堆外内存System.out.printf("✅ 释放堆外内存: 大小=%,d bytes\n", size);}private void cleanMemory() {try {// 通过反射调用 clean() 方法Method cleanMethod = jdkCleaner.getClass().getMethod("clean");cleanMethod.invoke(jdkCleaner);} catch (Exception e) {throw new RuntimeException("无法调用clean方法", e);}}}// 内存管理器public static class MemoryManager implements AutoCloseable {private final ReferenceQueue<ByteBuffer> queue = new ReferenceQueue<>();private final ConcurrentHashMap<MemoryReference, Boolean> refs = new ConcurrentHashMap<>();private final ExecutorService cleanerThread = Executors.newSingleThreadExecutor();private volatile boolean running = true;public MemoryManager() {// 启动后台清理线程cleanerThread.submit(() -> {while (running || !refs.isEmpty()) {try {MemoryReference ref = (MemoryReference) queue.remove(500);if (ref != null) {ref.clean();refs.remove(ref);}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}});}/*** 分配堆外内存并返回ByteBuffer*/public ByteBuffer allocateDirect(int size) {// 分配堆外内存ByteBuffer buffer = ByteBuffer.allocateDirect(size);// 获取JDK内置的CleanerObject jdkCleaner = getCleaner(buffer);// 创建自定义清理逻辑ResourceCleaner customCleaner = () -> {System.out.println("执行自定义清理操作...");};// 创建虚引用并注册MemoryReference ref = new MemoryReference(buffer, queue, customCleaner, jdkCleaner, size);refs.put(ref, Boolean.TRUE);return buffer;}@Overridepublic void close() throws Exception {running = false;cleanerThread.shutdown();if (!cleanerThread.awaitTermination(5, TimeUnit.SECONDS)) {cleanerThread.shutdownNow();}System.out.println("内存管理器已关闭");}/*** 获取ByteBuffer关联的Cleaner*/private Object getCleaner(ByteBuffer buffer) {try {// 获取ByteBuffer的cleaner()方法Method cleanerMethod = buffer.getClass().getMethod("cleaner");cleanerMethod.setAccessible(true);// 调用cleaner()方法获取Cleaner实例return cleanerMethod.invoke(buffer);} catch (Exception e) {throw new RuntimeException("无法获取Cleaner", e);}}}/**** VM options 中,需要添加两个参数:* --add-opens java.base/java.nio=ALL-UNNAMED* --add-opens java.base/jdk.internal.ref=ALL-UNNAMED** @param args* @throws Exception*/public static void main(String[] args) throws Exception {try (MemoryManager manager = new MemoryManager()) {// 分配堆外内存,但是实例元信息还是在堆中,内存块在堆外ByteBuffer buffer = manager.allocateDirect(1024 * 1024); // 1MB// 使用缓冲区buffer.putInt(0, 42);System.out.println("缓冲区值: " + buffer.getInt(0));// 释放缓冲区,取消强引用,使之实例元信息可以被GC回收// 包含指向堆外内存的指针和其他元数据(注意此处不是元空间的元数据,两者是不同概念)// 只有这个Java对象被回收后,虚引用才会被加入队列,// 虚引用是桥梁:当堆中的 ByteBuffer 对象被回收时,虚引用会触发回调来释放堆外内存buffer = null;// 模拟GC,实际生产环境中,不会使用System.gc(),而是使用JVM的垃圾回收器System.gc();Thread.sleep(1000);}}
}