《深入理解Java虚拟机》(第三版)读书笔记(二):第三章 垃圾收集器与内存分配策略(上)

《深入理解Java虚拟机》(第三版)读书笔记(二):第三章 垃圾收集器与内存分配策略(下)

如何判断对象已死

​ ​ ​ 引用计数法是面试中常听到的回答,引用计数法是怎么判断的呢?在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数值+1,引用失效的时候,计数值-1,任何时刻计数器为0的对象就是不可能在被使用的,也就是对象已死。引用计数法占用了额外的内存空间进行计数,但是原理简单,效率高。但是主流的JVM里并没有选择引用计数法来管理内存,因为如果使用这个算法,那么必须要考虑很多例外情况。

​ ​ ​ ​ 目前主流的商用程序语言,比如Java的内存管理子系统,是通过可达性分析算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为GC Roots的根对象最为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链项链,那么这个对象是不可能在被使用的。

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

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

​ 引用可以分为强引用、软引用、弱引用、虚引用,这4中强度依次逐渐减弱。

  • 强引用:在程序代码之中普遍存在的引用复制,类似于“Object obj=new Object()”,无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象
  • 软引用是描述一些还有用但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,就会抛出内存溢出异常。
  • 弱引用也是描述非必须对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用也被称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例,为一个对象设置虚引用关联的目的就是为了能在这个对象被收集器回收的时候收到一个系统通知
如何判断一个对象真正死亡

​ ​ ​ 即使是可达性分析算法中判定为不可达对象,也不是非死不可,这时候它们暂时处于缓刑状态,要真正宣告一个对象死亡,至少经历两次标记过程:如果对象在进行可达性分析后发现没有鱼GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,加入对象没有覆盖此方法或者此方法已经被虚拟机调用过,那么虚拟机讲视这两种情况为没必要执行。如果这个对象被判定为有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列中,并在稍后由一条虚拟机自动建立、低调度优先级的Finalizer线程去执行finalize()方法。这里的执行指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。如果finalize()方法执行慢,将可能导致F-Queue队列中的其他对象永久处于等待甚至导致整个内存回收子系统的崩溃,所以这儿的执行指的是虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束的原因。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

​ ​ ​ ​ 任何一个对象的finalize()方法都只会被系统自动调用一次,如果兑现面临下一次回收,它的finalize()方法不会被再次执行,所以它的自救行动也没戏了。不过finalize()方法能做的try…finaly也可以甚至更好更及时,书里是不建议使用finalize()方法的。

垃圾收集算法

​ ​ ​ ​ 从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(ReferenceCounting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。Tracing GC在主流Java虚拟机中比较常见,因此主要讲该算法。

标记-清除算法

​ ​ ​ ​ 先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

​ ​ ​ ​ 主要缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制算法

​ ​ ​ ​ 解决了标记-清除算法面对大量可回收对象时执行效率低的问题。将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。

​ ​ ​ ​ 每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。但是如果内存中多数对象都是村好的,这种算法就会产生大量的内存间复制的开销。

​ ​ ​ ​ 需要说的是,新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。

标记-整理算法

​ ​ ​ ​ 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

发布了58 篇原创文章 · 获赞 5 · 访问量 6261

猜你喜欢

转载自blog.csdn.net/weixin_40992982/article/details/104039324