垃圾回收机制

垃圾回收Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。有效的使用可以使用的内存,对内存中已经死亡的或者长时间没有使用的对象进行清除和回收

死亡对象判断方法

JVM(Java虚拟机)垃圾回收机制 —— 内存分配和回收规则

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)

引用计数法

给对象中添加一个引用计数器

  • 每当有一个地方引用它,计数器就加 1;
  • 当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。

![[Pasted image 20231011222409.png]]

所谓对象之间的相互引用问题:除了对象 objAobjB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 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 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

![[Pasted image 20231011222616.png]]

图片来源:JavaGuide

Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象

可以作为 GC Roots 的对象
  • 虚拟机栈(栈帧中的局部变量表)中引用的对象

  • 本地方法栈(Native 方法)中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

  • 所有被同步锁持有的对象

  • JNI(Java Native Interface)引用的对象

判断对象被回收需要经历的过程

可达性算法是一种通过判断对象是否可达来确定是否回收的方法。对象被判定为不可达时,并非意味着它们会立即被回收,而是在垃圾回收器执行回收操作时才会被释放。具体的回收时机和策略由垃圾回收器的实现决定

可达性算法中,判断对象是否被回收通常经历以下过程:

  1. 根对象标记:垃圾回收器会从一组称为"根对象(GC Roots)"(如全局变量、活动线程的栈等)开始,标记所有从根对象可直接或间接访问到的对象。这些被标记的对象被认为是"可达"的,应该保留不被回收。

  2. 遍历标记:垃圾回收器会逐个遍历可达对象,标记它们所引用的其他对象。这个过程是递归的:在找到一个对象并标记它后,继续查找并标记被该对象引用的其他对象,直到不再有新对象可访问。

  3. 清理非标记对象:在标记阶段完成后,垃圾回收器会遍历堆内存中的所有对象。未被标记的对象被视为不可达,它们不再被任何可达对象引用或访问。垃圾回收器将这些非标记对象标记为垃圾,并将它们的内存释放出来。

  4. 内存回收:垃圾回收器会执行垃圾回收操作,将被标记为垃圾的对象所占用的内存空间进行回收释放。这个过程通常是自动的,由垃圾回收器负责管理和执行。

引用类型

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

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),可以通过以下步骤进行

  1. 查看文档或注释:首先,查看常量的文档或注释,看是否有明确的说明该常量已被废弃。通常,废弃的常量会在文档或注释中标记为废弃,并给出了推荐的替代方案。

  2. 阅读源代码:如果没有明确的文档或注释,可以查看常量所在类的源代码。在常量的定义处,通常使用 @Deprecated 注解将废弃常量标记为废弃。在 Java 中,使用了 @Deprecated 注解的常量会在编译时发出警告。

  3. IDE 提示:在使用现代集成开发环境(IDE)时,常量的废弃状态通常会得到特殊标记或提示。IDE 可能会在代码中给出警告,提示开发者一个常量已被废弃

如果经过以上步骤确认一个常量被标记为废弃,那么开发者应该遵循推荐的做法,并尽量避免使用该废弃常量。替代方案往往会在文档或注释中提到,开发者可以根据推荐的替代方案进行调整和更新代码。

废弃原因

标记常量为废弃的原因可以包括

  • 常量已不再符合设计或功能要求。
  • 常量存在安全风险或潜在问题,不推荐使用。
  • 常量已被更好的替代方案取代,推荐使用新的常量。
遵循原则

开发者在使用废弃常量时应该考虑遵循

  • 尽量避免使用废弃常量,推荐使用替代的、非废弃的常量。
  • 如果必须使用废弃常量,开发者应该了解其存在的问题和风险,并确保理解和处理这些问题。
  • 废弃常量可能在未来的版本中被移除,因此开发者应该及时更新代码,以避免依赖于已移除的常量。

无用的类

无用的类(Unused Classes) 是指在代码中存在但没有被使用或引用的类。这些类可能是代码重构、功能删除或其他变更导致的残留代码。

所需条件

类需要同时满足下面 3 个条件才能算是 无用的类

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是可以,而并不是和对象一样不使用了就会必然被回收

造成的问题

在开发过程中,存在无用的类可能会造成一些问题

  1. 内存占用:无用的类会占用内存空间,增加应用程序的内存消耗。

  2. 代码冗余:无用的类会增加代码库的大小,导致代码冗余。

  3. 可读性下降:存在无用的类会使代码库变得混乱和不易维护。

解决步骤

解决无用类的问题,可以采取以下几个步骤

  1. 代码评审:定期进行代码评审,识别和删除未使用的类。
  2. 搜索引擎工具:使用搜索引擎工具(如IDE中的Find Usages)来检查类的引用情况,找出未被引用的类。
  3. 构建工具插件:使用一些构建工具插件(如ProGuard、Unused、UCDetector等),它们可以帮助 自动检测和删除未使用的类
  4. 清理过程:在代码库中进行定期的清理过程,包括删除未使用的类和其他无效代码。

在删除无用的类之前,应该先进行彻底的测试确保没有任何功能受到影响。一些类可能在特定的场景或条件下使用到,并且可能不容易通过简单的搜索来识别。

修复和删除无用的类有助于提高代码质量、减少资源浪费和简化维护工作。但同时也要谨慎操作,确保不会意外删除有用的类。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐