文章目录
- 垃圾回收机制
- 死亡对象判断方法
- 引用计数法
- 可达性分析算法
- 可以作为 GC Roots 的对象
- 判断对象被回收需要经历的过程
- 引用类型
- 引用汇总
- 引用队列
- 废弃常量
- 判定废弃常量
- 废弃原因
- 遵循原则
- 无用的类
- 所需条件
- 造成的问题
- 解决步骤
垃圾回收机制
垃圾回收(Garbage Collection,GC
),顾名思义就是释放垃圾占用的空间,当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
死亡对象判断方法
JVM(Java虚拟机)垃圾回收机制 —— 内存分配和回收规则
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)
引用计数法
给对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。
所谓对象之间的相互引用问题:除了对象 objA
和 objB
相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
可达性分析算法
该算法通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots
没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
图片来源:JavaGuide
Object 6 ~ Object 10
之间虽有引用关系,但它们到 GC Roots
不可达,因此为需要被回收的对象
可以作为 GC Roots 的对象
-
虚拟机栈(栈帧中的局部变量表)中引用的对象
-
本地方法栈(Native 方法)中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
所有被同步锁持有的对象
-
JNI(Java Native Interface)引用的对象
判断对象被回收需要经历的过程
可达性算法是一种通过判断对象是否可达来确定是否回收的方法。对象被判定为不可达时,并非意味着它们会立即被回收,而是在垃圾回收器执行回收操作时才会被释放。具体的回收时机和策略由垃圾回收器的实现决定
可达性算法中,判断对象是否被回收通常经历以下过程:
-
根对象标记:垃圾回收器会从一组称为"根对象(GC Roots)"(如全局变量、活动线程的栈等)开始,标记所有从根对象可直接或间接访问到的对象。这些被标记的对象被认为是"可达"的,应该保留不被回收。
-
遍历标记:垃圾回收器会逐个遍历可达对象,标记它们所引用的其他对象。这个过程是递归的:在找到一个对象并标记它后,继续查找并标记被该对象引用的其他对象,直到不再有新对象可访问。
-
清理非标记对象:在标记阶段完成后,垃圾回收器会遍历堆内存中的所有对象。未被标记的对象被视为不可达,它们不再被任何可达对象引用或访问。垃圾回收器将这些非标记对象标记为垃圾,并将它们的内存释放出来。
-
内存回收:垃圾回收器会执行垃圾回收操作,将被标记为垃圾的对象所占用的内存空间进行回收释放。这个过程通常是自动的,由垃圾回收器负责管理和执行。
引用类型
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用 四种(引用强度逐渐减弱)
引用汇总
1.强引用(Strong Reference)
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。
当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.软引用(Soft Reference)
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存
只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue
)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(Weak Reference)
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue) 联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4.虚引用(Phantom Reference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用
使用软引用的情况较多,软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
引用队列
引用队列(Reference Queue) 是 Java 中用于管理对象引用的特殊队列。它与垃圾回收(Garbage Collection)机制密切相关,用于跟踪和处理被垃圾回收器回收的对象。
当对象被垃圾回收器标记为可回收时,它可能会被加入到引用队列中。通过监视引用队列,我们可以了解对象何时被回收,并进行相应的处理操作。
当一个对象的引用类型存在于引用队列中时,我们可以使用 poll()
方法或 remove()
方法从引用队列中获取对应的引用对象。这样,我们就可以得知某个对象何时被垃圾回收器回收了,并进行一些相关的处理工作,比如资源释放、记录日志等。
引用队列在某些场景下非常有用,比如内存敏感的缓存系统、对象终结(Finalization)等。但是需要注意,使用引用队列需要小心处理,避免导致内存泄漏或引起性能问题。
废弃常量
运行时常量池主要回收的是废弃的常量。
-
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区,此时 hotspot 虚拟机对方法区的实现为永久代
-
JDK1.7 字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是 hotspot 中的永久代 。
-
JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)
相关知识补充:Java 入门指南:JVM(Java虚拟机) —— Java 内存运行时的数据区域
废弃常量(Deprecated Constants)是指在编程语言或库的API中标记为过时或不推荐使用的常量。它们通常是为了提醒开发者不要再依赖或使用这些常量,并鼓励使用新的替代方案。
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。
在Java中,使用@Deprecated
注解可以将常量标记为废弃。
public class Constants {
@Deprecated
public static final int OLD_CONSTANT = 100;
public static final int NEW_CONSTANT = 200;
// ...
}
// 当其他代码中引用此废弃常量时,会收到编译器的警告,以提醒开发者不再使用该常量。
判定废弃常量
要判定一个常量是否被标记为废弃(Deprecated),可以通过以下步骤进行:
-
查看文档或注释:首先,查看常量的文档或注释,看是否有明确的说明该常量已被废弃。通常,废弃的常量会在文档或注释中标记为废弃,并给出了推荐的替代方案。
-
阅读源代码:如果没有明确的文档或注释,可以查看常量所在类的源代码。在常量的定义处,通常使用
@Deprecated
注解将废弃常量标记为废弃。在 Java 中,使用了@Deprecated
注解的常量会在编译时发出警告。 -
IDE 提示:在使用现代集成开发环境(IDE)时,常量的废弃状态通常会得到特殊标记或提示。IDE 可能会在代码中给出警告,提示开发者一个常量已被废弃
如果经过以上步骤确认一个常量被标记为废弃,那么开发者应该遵循推荐的做法,并尽量避免使用该废弃常量。替代方案往往会在文档或注释中提到,开发者可以根据推荐的替代方案进行调整和更新代码。
废弃原因
标记常量为废弃的原因可以包括:
- 常量已不再符合设计或功能要求。
- 常量存在安全风险或潜在问题,不推荐使用。
- 常量已被更好的替代方案取代,推荐使用新的常量。
遵循原则
开发者在使用废弃常量时应该考虑遵循:
- 尽量避免使用废弃常量,推荐使用替代的、非废弃的常量。
- 如果必须使用废弃常量,开发者应该了解其存在的问题和风险,并确保理解和处理这些问题。
- 废弃常量可能在未来的版本中被移除,因此开发者应该及时更新代码,以避免依赖于已移除的常量。
无用的类
无用的类(Unused Classes) 是指在代码中存在但没有被使用或引用的类。这些类可能是代码重构、功能删除或其他变更导致的残留代码。
所需条件
类需要同时满足下面 3 个条件才能算是 无用的类:
-
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
-
加载该类的
ClassLoader
已经被回收。 -
该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是可以,而并不是和对象一样不使用了就会必然被回收
造成的问题
在开发过程中,存在无用的类可能会造成一些问题:
-
内存占用:无用的类会占用内存空间,增加应用程序的内存消耗。
-
代码冗余:无用的类会增加代码库的大小,导致代码冗余。
-
可读性下降:存在无用的类会使代码库变得混乱和不易维护。
解决步骤
解决无用类的问题,可以采取以下几个步骤:
- 代码评审:定期进行代码评审,识别和删除未使用的类。
- 搜索引擎工具:使用搜索引擎工具(如IDE中的Find Usages)来检查类的引用情况,找出未被引用的类。
- 构建工具插件:使用一些构建工具插件(如ProGuard、Unused、UCDetector等),它们可以帮助 自动检测和删除未使用的类。
- 清理过程:在代码库中进行定期的清理过程,包括删除未使用的类和其他无效代码。
在删除无用的类之前,应该先进行彻底的测试确保没有任何功能受到影响。一些类可能在特定的场景或条件下使用到,并且可能不容易通过简单的搜索来识别。
修复和删除无用的类有助于提高代码质量、减少资源浪费和简化维护工作。但同时也要谨慎操作,确保不会意外删除有用的类。