JVM如何判断对象是否存活?
在Java堆中存放着所有Java的对象实例 ,在GC执行垃圾收回之前,JVM需要标识出来哪些是对象已经不被引用(垃圾),哪些被引用,而JVM有两种垃圾标识对象是否存活,分别是:引用计数算法和可达性分析算法。
引用计数算法(Reference Counting):
实现比较简单,对每一个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
优点:实现比较简单,很容易判断是否是垃圾,判断效率高,回收没有延迟性。
缺点:
需要单独的字段来存储,增加空间的开销;
每次值的更新需要更新计数器,伴随加法和减法操作,增加一定时延;
引用计数器有一个严重的问题,无法处理循环引用的问题,导致不知道是否是垃圾还是存活类似A->B->A的问题,会造成内存泄漏。
可达性分析算法(Reachability Analysis):
可达性分析算法是java采用来判断对象是否存在的算法,通过判断"GC Roots"是否被直接或间接引用,这种会被可达性分析通过搜索路劲找到,而这个路劲叫引用链(Reference Chain),如果被存在则证明是可达的,若不是被引用则证明对象是可以被回收的。依据只是判断该GC Roots上在的这个对象是否存活,实现稍微比较复杂。
可达性分析算法GC Roots的对象包含有哪些?
虚拟机栈引用的对象
本地方法栈内JNI(本地方法)引用的对象
方法区中常量引用的对象(字符串常量池)
所有被同步锁synchronized持有的对象
Java虚拟机内部的引用
如何判断对象是否存活?
对象有三种状态:可触及、可复活、不可触及,分别代表如下:
可触及:从根节点开始,可以达到这个对象;
可复活:对象的所有引用都被释放,但是对象有可能在finalize()中复活;
不可触及:对象finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
对象被回收前,首先可达性分析到这个对象是否有与GC Roots的引用链,如果没有会被打上第一次标记,然后判断是否有必要执行finalize,如果被执行过或没必要就直接被垃圾回收了。如果有必要,执行后会被入到F-Queue这个队列中,然后逃过一次被GC。等到下一次GC的时候会对F-Queue这个对队列再做一次的标记,如果这次再发现没有引用链就会被直接GC回收了。
Java 中都有哪些引用类型?
强引用:发生 gc 的时候不会被回收。new
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。SoftReference
弱引用:有用但不是必须的对象,在下一次GC时会被回收。WeakReference
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
PhantomReference pr = new PhantomReference (object, queue);
可达性分析算法是JAVA采用判断对象存活的算法。
JVM收集器算法有哪些呢?
标记-清除算法(Mark-Sweep)
标记---清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。标记清除的执行过程是先标记,再清除。
特点:实现简单
缺点:每次清除的时候都需要停机、存在内存空间太强片化问题。
复制算法(Copying)
复制(Copying)算法是为了解决标记-清除算法,的效率和收集的时间空间不连续等问题。主要的实现是将空间分为两份,将存活的对象移到另外一份,标记完后,将原来的空间清除,这样的话空间是连续的,并且效率较高。
特点:空间连续无碎片化、清除高效;
缺点:
压缩一半空间,垃圾清楚的时候一半空间不可用。
对存活对象较多的老年代下,交率较差。
标记-整理算法(Mark-Compact)
由于复制算法的高效性是建立在存活对象少,垃圾对象多的前提下的,对于新生代来说比较适合,但是针对老年代来说,很多对象是一直存活的,所以就不能用复制算法,这样会导致每次回收的垃圾很少,会造成大量的复制。所以标记-整理算法主要是针对老年代来设计的。其原理主要是:分为两个阶段,第一个阶段与标记-清理算法一样,先从根节点标记哪些是被对象引用的,第二阶段将所有存活的对象压缩移动到内存的另一端,按顺序排放,最后清除所有边界以外的空间。
注意:在JDK8默认的配置下使用 新生代,老年代的垃圾回收策略,新生代区域使用标记-复制算法,老年代区域使用标记-整理算法。
三种算法的对比?
对比名称 | 标记-清除 | 标记-整理 | 标记-复制 |
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(会产生碎片) | 少(不会产生碎片) | 需要对象2倍大小 |
移动对象 | 否 | 是 | 是 |
分代收集算法(Generational Collection)
背景:由于每个收集的算法都没办法符合所有的场景,就好比每个对象所在的内存阶段不一样,被回收的概率也不一样,比如在新生代,基本可以说90%以上的都会被回收,而到老年代接近一半以上的对象则是一半存活的,所以针对这两种不同的场景,回收的策略肯定有所不一样,所以引发而出的就是分代收集算法,根据新生代和老年代不同的场景而用不同的算法,比如新生代用复制算法,而老年代则用标记-整理算法。
HotSpot基于分代的概念,GC所使用的内存回收算法是根据新生代和老年代的特点。
新生代(Yong Gen)
年轻代特点:区域相对老年代较小,对象生存周期短,存活率低,回收频繁。所以适合-标记-复制算法;
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不频繁,所以更适合-标记-整理算法;
像CMS、G1这些垃圾收集器都属于这个分代思想演化而来。
增量收集算法(Incremental Collecting)
现有的所有的算法都可能会导致停机-Stop the World的状态,这样会导致所有程序都被挂起,这样会影响用户体验和系统的稳定性。所以增加增量收集算法就是解决每次GC的时候导致停机的问题,其思想是每次垃圾收集中对某一个区域进行收集,再切到用户的线程,直接垃圾收集完成。这样可以很好的避免每次一回收会对整个新生代或老年代进行收集,导致停机场景。
优点:避免停机问题(Stop the World)
缺点:新的垃圾不断产生,会导致线程不断切换上下文,导致回收垃圾成本上升,导致系统吞吐量的下降;
分区收集算法
分区算法主要是通过将整个堆空间划分成连线不同的小区间,每一个区间都可以独立使用,独立回收。这样的话可以通过算法来控制每次对垃圾回收的是哪几个分区,不会因为每次一回收把某个大区都回收了,降低停机概率。
优点:避免停机和合理回收特定分区;
缺点:算法较复杂,需要动态计算每次回收的分区;
最后
垃圾收集算法,是垃圾收集器的核心,年轻代、老年代不同的场景可以针对不同的收集算法,针对JVM来说,对象是有生命周期的,当然JDK8默认的收集器是CMS新生代区域使用标记-复制算法,老年代区域使用标记-整理算法。但随着分代收集算法、分区算法等出现,可以预见不久的将来将会出现很少需要stop the word 或者甚至不停机的垃圾手机器。
参考文献:
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
https://blog.csdn.net/luzhensmart/article/details/81431212
https://blog.csdn.net/weixin_42128166/article/details/80184449
https://blog.csdn.net/weixin_44046437/article/details/99686843
https://blog.csdn.net/baidu_37107022/article/details/89277790
https://blog.csdn.net/wuzhiwei549/article/details/80563134
往期推荐