点击围观!腾讯 TAPD 助力金融行业研发提效、敏捷转型最佳实践! 了解详情
写点什么

Java 深度历险(四)——Java 垃圾回收机制与引用类型

  • 2011-01-31
  • 本文字数:4877 字

    阅读完需:约 16 分钟

Java 语言的一个重要特性是引入了自动的内存管理机制,使得开发人员不用自己来管理应用中的内存。C/C++ 开发人员需要通过 malloc / free new / delete 等函数来显式的分配和释放内存。这对开发人员提出了比较高的要求,容易造成内存访问错误和内存泄露等问题。一个常见的问题是会产生“悬挂引用(dangling references)”,即一个对象引用所指向的内存区块已经被错误的回收并重新分配给新的对象了,程序如果继续使用这个引用的话会造成不可预期的结果。开发人员有可能忘记显式的调用释放内存的函数而造成内存泄露。而自动的内存管理则是把管理内存的任务交给编程语言的运行环境来完成。开发人员并不需要关心内存的分配和回收的底层细节。Java 平台通过垃圾回收器来进行自动的内存管理。

Java 垃圾回收机制

Java 的垃圾回收器要负责完成 3 件任务:分配内存、确保被引用的对象的内存不被错误回收以及回收不再被引用的对象的内存空间。垃圾回收是一个复杂而且耗时的操作。如果 JVM 花费过多的时间在垃圾回收上,则势必会影响应用的运行性能。一般情况下,当垃圾回收器在进行回收操作的时候,整个应用的执行是被暂时中止(stop-the-world)的。这是因为垃圾回收器需要更新应用中所有对象引用的实际内存地址。不同的硬件平台所能支持的垃圾回收方式也不同。比如在多 CPU 的平台上,就可以通过并行的方式来回收垃圾。而单 CPU 平台则只能串行进行。不同的应用所期望的垃圾回收方式也会有所不同。服务器端应用可能希望在应用的整个运行时间中,花在垃圾回收上的时间总数越小越好。而对于与用户交互的应用来说,则可能希望所垃圾回收所带来的应用停顿的时间间隔越小越好。对于这种情况,JVM 中提供了多种垃圾回收方法以及对应的性能调优参数,应用可以根据需要来进行定制。

Java 垃圾回收机制最基本的做法是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成 3 个世代:年轻、年老和永久。内存的分配是发生在年轻世代中的。当一个对象存活时间足够长的时候,它就会被复制到年老世代中。对于不同的世代可以使用不同的垃圾回收算法。进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。一般来说,一个应用中的大部分对象的存活时间都很短。比如局部变量的存活时间就只在方法的执行过程中。基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性。

年轻世代的内存区域被进一步划分成伊甸园(Eden)和两个存活区(survivor space)。伊甸园是进行内存分配的地方,是一块连续的空闲内存区域。在上面进行内存分配速度非常快,因为不需要进行可用内存块的查找。两个存活区中始终有一个是空白的。在进行垃圾回收的时候,伊甸园和其中一个非空存活区中还存活的对象根据其存活时间被复制到当前空白的存活区或年老世代中。经过这一次的复制之后,之前非空的存活区中包含了当前还存活的对象,而伊甸园和另一个存活区中的内容已经不再需要了,只需要简单地把这两个区域清空即可。下一次垃圾回收的时候,这两个存活区的角色就发生了交换。一般来说,年轻世代区域较小,而且大部分对象都已经不再存活,因此在其中查找存活对象的效率较高。

而对于年老和永久世代的内存区域,则采用的是不同的回收算法,称为“标记- 清除- 压缩(Mark-Sweep-Compact)”。标记的过程是找出当前还存活的对象,并进行标记;清除则遍历整个内存区域,找出其中需要进行回收的区域;而压缩则把存活对象的内存移动到整个内存区域的一端,使得另一端是一块连续的空闲区域,方便进行内存分配和复制。

JDK 5 中提供了 4 种不同的垃圾回收机制。最常用的是串行回收方式,即使用单个 CPU 回收年轻和年老世代的内存。在回收的过程中,应用程序被暂时中止。回收方式使用的是上面提到的最基本的分代回收。串行回收方式适合于一般的单 CPU 桌面平台。如果是多 CPU 的平台,则适合的是并行回收方式。这种方式在对年轻世代进行回收的时候,会使用多个 CPU 来并行处理,可以提升回收的性能。并发标记 - 清除回收方式适合于对应用的响应时间要求比较 高的情况,即需要减少垃圾回收所带来的应用暂时中止的时间。这种做法的优点在于可以在应用运行的同时标记存活对象与回收垃圾,而只需要暂时中止应用比较短的时间。

通过 JDK 中提供的 JConsole 可以很容易的查看当前应用的内存使用情况。在 JVM 启动的时候添加参数 -verbose:gc 可以查看垃圾回收器的运行结果。

Java 引用类型

如果一个内存中的对象没有任何引用的话,就说明这个对象已经不再被使用了,从而可以成为被垃圾回收的候选。不过由于垃圾回收器的运行时间不确定,可被垃圾回收的对象的实际被回收时间是不确定的。对于一个对象来说,只要有引用的存在,它就会一直存在于内存中。如果这样的对象越来越多,超出了 JVM 中的内存总数,JVM 就会抛出 OutOfMemory 错误。虽然垃圾回收的具体运行是由 JVM 来控制的,但是开发人员仍然可以在一定程度上与垃圾回收器进行交互,其目的在于更好的帮助垃圾回收器管理好应用的内存。这种交互方式就是使用 JDK 1.2 引入的 java.lang.ref 包。

强引用

在一般的 Java 程序中,见到最多的就是强引用(strong reference)。如 Date date = new Date(),date 就是一个对象的强引用。对象的强引用可以在程序中到处传递。很多情况下,会同时有多个引用指向同一个对象。强引用的存在限制了对象在内存中的存活时间。假如对象 A 中包含了一个对象 B 的强引用,那么一般情况下,对象 B 的存活时间就不会短于对象 A。如果对象 A 没有显式的把对象 B 的引用设为 null 的话,就只有当对象 A 被垃圾回收之后,对象 B 才不再有引用指向它,才可能获得被垃圾回收的机会。

除了强引用之外,java.lang.ref 包中提供了对一个对象的不同的引用方式。JVM 的垃圾回收器对于不同类型的引用有不同的处理方式。

软引用

软引用(soft reference)在强度上弱于强引用,通过类 SoftReference 来表示。它的作用是告诉垃圾回收器,程序中的哪些对象是不那么重要,当内存不足的时候是可以被暂时回收的。当 JVM 中的内存不足的时候,垃圾回收器会释放那些只被软引用所指向的对象。如果全部释放完这些对象之后,内存还不足,才会抛出 OutOfMemory 错误。软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。比如考虑一个图像编辑器的程序。该程序会把图像文件的全部内容都读取到内存中,以方便进行处理。而用户也可以同时打开多个文件。当同时打开的文件过多的时候,就可能造成内存不足。如果使用软引用来指向图像文件内容的话,垃圾回收器就可以在必要的时候回收掉这些内存。

复制代码
public class ImageData {
private String path;
private SoftReference<byte[]> dataRef;
public ImageData(String path) {
this.path = path;
dataRef = new SoftReference<byte[]>(new byte[0]);
}
private byte[] readImage() {
return new byte[1024 * 1024]; // 省略了读取文件的操作
}
public byte[] getData() {
byte[] dataArray = dataRef.get();
if (dataArray == null || dataArray.length == 0) {
dataArray = readImage();
dataRef = new SoftReference<byte[]>(dataArray);
}
return dataArray;
}
}

在运行上面程序的时候,可以使用 -Xmx 参数来限制 JVM 可用的内存。由于软引用所指向的对象可能被回收掉,在通过 get 方法来获取软引用所实际指向的对象的时候,总是要检查该对象是否还存活。

弱引用

弱引用(weak reference)在强度上弱于软引用,通过类 WeakReference 来表示。它的作用是引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收。弱引用的作用在于解决强引用所带来的对象之间在存活时间上的耦合关系。弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何 Java 对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉 JVM 中全部的内存。

对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。Java 中提供了 WeakHashMap 来满足这一常见需求。

幽灵引用

在介绍幽灵引用之前,要先介绍 Java 提供的对象终止化机制(finalization)。在Object 类里面有个 finalize 方法,其设计的初衷是在一个对象被真正回收之前,可以用来执行一些清理的工作。因为 Java 并没有提供类似 C++ 的析构函数一样的机制,就通过 finalize 方法来实现。但是问题在于垃圾回收器的运行时间是不固定的,所以这些清理工作的实际运行时间也是不能预知的。幽灵引用(phantom reference)可以解决这个问题。在创建幽灵引用 PhantomReference 的时候必须要指定一个引用队列。当一个对象的 finalize 方法已经被调用了之后,这个对象的幽灵引用会被加入到队列中。通过检查该队列里面的内容就知道一个对象是不是已经准备要被回收了。

幽灵引用及其队列的使用情况并不多见,主要用来实现比较精细的内存使用控制,这对于移动设备来说是很有意义的。程序可以在确定一个对象要被回收之后,再申请内存创建新的对象。通过这种方式可以使得程序所消耗的内存维持在一个相对较低的数量。比如下面的代码给出了一个缓冲区的实现示例。

复制代码
public class PhantomBuffer {
private byte[] data = new byte[0];
private ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
private PhantomReference<byte[]> ref = new PhantomReference<byte[]>(data, queue);
public byte[] get(int size) {
if (size <= 0) {
throw new IllegalArgumentException("Wrong buffer size");
}
if (data.length < size) {
data = null;
System.gc(); // 强制运行垃圾回收器
try {
queue.remove(); // 该方法会阻塞直到队列非空
ref.clear(); // 幽灵引用不会自动清空,要手动运行
ref = null;
data = new byte[size];
ref = new PhantomReference<byte[]>(data, queue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return data;
}
}

在上面的代码中,每次申请新的缓冲区的时候,都首先确保之前的缓冲区的字节数组已经被成功回收。引用队列的 remove 方法会阻塞直到新的幽灵引用被加入到队列中。不过需要注意的是,这种做法会导致垃圾回收器被运行的次数过多,可能会造成程序的吞吐量过低。

引用队列

在有些情况下,程序会需要在一个对象的可达到性发生变化的时候得到通知。比如某个对象的强引用都已经不存在了,只剩下软引用或是弱引用。但是还需要对引用本身做一些的处理。典型的情景是在哈希表中。引用对象是作为 WeakHashMap 中的键对象的,当其引用的实际对象被垃圾回收之后,就需要把该键值对从哈希表中删除。有了引用队列( ReferenceQueue ),就可以方便的获取到这些弱引用对象,将它们从表中删除。在软引用和弱引用对象被添加到队列之前,其对实际对象的引用会被自动清空。通过引用队列的 poll / remove 方法就可以分别以非阻塞和阻塞的方式获取队列中的引用对象。

参考资料


感谢张凯峰对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011-01-31 00:0025536

评论 1 条评论

发布
用户头像
写得非常棒!
2019-06-03 22:27
回复
没有更多了
发现更多内容

一种基于事件驱动架构的 SAP 产品集成方案介绍

Jerry Wang

Kubernetes 云原生 SAP Kyma 5月月更

vue响应式原理简述

达摩

Vue vue源码解读

查看Docker容器的信息

程序员欣宸

Java 5月月更

数据预处理利器 Amazon Glue DataBrew

亚马逊云科技 (Amazon Web Services)

数据 处理器

开发者玩转机器学习不能错过的15篇深度文章!

阿里云大数据AI技术

算法 数据处理 算法框架/工具 机器学习/深度学习 搜索推荐

小程序怎样一键转化成APP

Geek_99967b

小程序生态 小程序容器 小程序转app

做一个能对标阿里云的前端APM工具

光毅

阿里云 性能优化 前端

什么!Sentinel流控规则可以这样玩?

牧小农

sentinel SpringCloud Alibaba

小程序如何实现一键转换成App

Geek_99967b

小程序容器 小程序转app

软件开发的核心原则

宇宙之一粟

软件设计原则 5月月更

模块2-微信朋友圈高性能复杂度分析

Fan

架构实战营

flask框架学习总结(四)【 文件上传,实现最小应用,路径变量,模板继承】

恒山其若陋兮

5月月更

跨平台应用开发进阶(十四) :uni-app 实现IOS原生APP-本地打包集成极光推送(JG-JPUSH)详细教程

No Silver Bullet

uni-app App 5月月更 IOS原生 JG-JPUSH

vue + electronの文件读写

空城机

Electron 5月月更

linux之curl命令

入门小站

Linux

Kernel SIG直播:关于 Plugsched 调度器热升级 | 第 18 期

OpenAnolis小助手

Linux 直播 内核 sig 龙蜥大讲堂

在线HTML转SQL工具

入门小站

工具

P2P传输模式

秋名山码民

计算机网络 5月月更

微信视频号严查教育招生类账号:应该抵制恶意网络营销

石头IT视角

金融街资本携手索信达:助推金融行业数智化转型

索信达控股

Java Core「4」java.util.concurrent 包简介

Samson

学习笔记 5月月更 Java core

在线文本删除空行工具

入门小站

工具

互联网公司实行目标管理(OKR)五点原则和基础

laofo

互联网 OKR 研发效能 快手 绩效考核

虚谷未来CEO 唐佳娴:让虚拟人“灵” 动起来

阿里云弹性计算

XR 虚拟人

Linux环境编译动态库

Loken

音视频 5月月更

土地市场分析,基于Python,基于Javascript,包含核心参数

梦想橡皮擦

5月月更

1.2架构的定义(一)

凌晞

架构 架构设计

发布订阅模式和观察者模式【vue】

达摩

Vue 观察者模式 发布订阅

通用池化框架实践之GenericKeyedObjectPool

FunTester

开发者的福音 提前为你揭秘2022鲲鹏开发者创享日武汉站

Geek_2d6073

Java深度历险(四)——Java垃圾回收机制与引用类型_Java_成富_InfoQ精选文章