0
点赞
收藏
分享

微信扫一扫

JVM回收对象都还不会?已经写的很详细了,不可能看不懂吧

尤克乔乔 2021-09-19 阅读 50
日记本

垃圾收集器回收的是什么?当然是"垃圾"。那什么是"垃圾"呢?在生活中当一个东西对我们来说已经没有使用价值,无法利用时,便会作为"垃圾"被我们丢弃掉。同理在JVM中当一个对象不会再被使用时,就成为垃圾回收器回收的对象---"垃圾"。为什么要回收"垃圾"呢?生活中,我们的房间空间是一定的,如果不清理"垃圾",会使得我们可利用的空间会越来越小,JVM中如果没有垃圾回收,也就是所占据的空间将不可回收,这就会造成了内存泄露。

读过上文的应该知道,Java中几乎所有的对象实例都在Java堆中分配内存,方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。所以垃圾收集器进行回收的区域是:堆和方法区中

如何判断是否为"垃圾"

引用计数算法

​ 引用计数算法是最古老的辨别方法。首先在对象中添加一个引用计数器,初始值为零,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;所以任何时刻当一个对象的计数器值为零时,代表此对象没有被任何地方引用,也就是垃圾收集器要回收的"垃圾"。

​ 客观地说,引用计数算法虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是在Java 领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

所谓对象之间的相互引用问题:除了对象a和b相互引用着对方之外,这两个对象之间再无任何引用。但是它们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数器法无法通知GC回收器回收它们。如下图红色区域。


可达性分析算法

​ 基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象可以根据引用链到达GC Root,那么说明这个对象被引用中,反之,则证明此对象是不可能再被使用的。如下图所示。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

如何判断一个常量是废弃常量

运行时常量池主要回收的是废弃的常量。那么,我们怎么判断一个常量时废弃常量呢?

​ 假如在常量池中存在字符串"abc",如果当前没有任何String对象引用该字符串常量的话,就说明常量”abc“就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc“会被系统清理出常量池。

如何判断一个类是无用的类

需要满足以下三个条件:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

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

引用

​ 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。在JDK 1.2版之前,Java里面的引用是很传统的定义: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

​ 在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用、软 引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。

强引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。当内存空间不足时也不回收,将抛出OutOfMemoryError异常,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

软引用

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。在JDK 1.2版之后提供了SoftReference类来实现软引用。

弱引用

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。也就是说软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

虚引用

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。

小结

垃圾回收算法

分代收集算法

​ 当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它是在建立分代假说之上:

​ 1)弱分代假说:绝大多数对象都是朝生夕灭的。

​ 2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

​ 3)跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

​ 1)2)这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

​ 依据3)这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

​ IBM公司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。

​ 在1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍 然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。当然,98%的对象可被回收仅仅是“普通场景”下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。

标记-清除算法

它是最基础的收集算法,之所以说它是最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。这个算法分为两个阶段,“标记”和”清除“。首先标记出所有需要回收的对象,标记过程就是对象是否属于垃圾的判定过程。在标记完成后统一回收所有被标记的对象。也可以反过来,标记存活的对象,统一回收所有未被标记的对象。它有两个不足的地方:

  1. 效率问题,标记和清除两个过程的效率都不高;
  2. 空间问题,标记清除后会产生大量不连续的碎片。

标记-复制算法

​ 标记-复制算法常被简称为复制算法。为了解决面对大量可回收对象时执行效率低的问题,复制算法出现了。它可以把内存分为大小相同的两块,每次只使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块区,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收

​ 如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点。

​ 现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。

标记-整理算法

​ 标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

​ 根据老年代的特点提出的一种标记算法,标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象向一段移动,然后直接清理掉边界以外的内存。

举报

相关推荐

0 条评论