《深入理解Java虚拟机:JVM高级特性与最佳实践》读书笔记(二)

Java中的对象几乎全部存放在Java堆中,垃圾收集器在回收时首先要确定哪些对象是“存活”的,哪些对象已经“死去”。这篇文章介绍垃圾收集器主要关注的内存区域和在什么情况下垃圾收集器开始进行回收操作。

垃圾回收的内存区域

  • 程序计数器、虚拟机栈和本地方法栈随线程的生灭而生灭,并且栈中的栈帧分配多少内存基本上是在类结构确定下来时就是已知的,即编译器可知,所以这几个区域就不需要考虑垃圾回收的问题了:方法或者线程结束后,内存自然就跟着回收了;
  • Java堆和方法区这部分的内存的分配和回收都是动态的,因为只有在运行时我们才知道会创建哪些对象,所以垃圾回收器关注的是这部分内存的回收,内存分配也是指的这部分;

对象是否被引用的判断方法

  • 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减一;任何时刻计数器为零的对象便是不可使用的对象,这个时候这些内存就要被回收。由于这种算法不能解决引用的“孤岛”问题——循环引用,故Java并未采取这种内存回收算法,而是下面的可达性分析算法;

  • 可达性分析算法:这种判断一个对象是否存活的算法其实是使用了基于引用关系的树遍历,其基本的思路是通过一系列的GC Roots的对象作为起始点,从这些起始点开始向下搜索遍历,搜寻这对象所引用的其他对象,直到找不到被引用的其他对象为止,而搜索所走过的路径成为引用链,被发现的对象会被标记为存活并称之为可达的,当一个对象到GC Roots没有任何引用链时,则该对象是不可用的,成为垃圾回收器的内存回收对象,如图所示:

    对象object5、object6和object7之间虽互有关联,但是他们到GC Roots是不可达的,所以会被判定为回收的对象。在Java中,可作为上文所述的GC Roots的对象包括下面几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
    • 方法区中类静态类型属性、常量等引用的对象;
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象;
  • 对于在上述可达性分析算法中不可达的对象所占用内存空间并不会立即被回收,而是需要经过两次标记过程:对象在经过可达性分析后被发现没有和GC Roots关联则会被标记一次,并通过判断其是否有必要执行finalize()方法(当前对象没有覆盖此方法或者虚拟机已经调用过一次,则会判定为没必要执行),来判断是否还能继续存活;如果这个对象有必要执行finalize()方法,则该对象将会被放入一个称为F-Queue的队列中,在这里对象可能会逃脱被回收的命运:将自己与引用链上任何一个对象建立关联即可,如把自己赋值给某个类变量或者某个对象的成员变量。如果在这里这个对象仍不逃脱,则会再次标记一次,将会真正被回收;

  • Java中将引用分为强引用(Strong Reference)、软引用(Sfot Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),他们的引用强度一次减弱:

    • 当在程序中使用new操作符创建一个新的对象并将其赋值给一个变量的时候,这个变量就成为了指向该对象的一个强引用,只要强引用还存在,垃圾回收器则永远不会回收该对象;
    • 软引用用来描述一些还有用但非必需的对象。对于软引用关联着的对象,只有在系统将要发生内存溢出异常之前,才会将这些对象列进回收范围之内进行第二次回收;
    • 被软引用关联的对象只能生存到下次垃圾收集时,并且无论当时内存是否足够,都会把若引用关联的对象回收掉;
    • 虚引用的存在不会对对象的生存时间构成影响,其存在的唯一目的是其关联的对象在被垃圾收集器回收时收到系统通知。
  • 方法区的垃圾回收主要回收两部分内容:废弃常量和无用类。如果进入常量池的某个常量没有被任何对象引用,也没有任何其他地方使用了此字面量,如果此时发生了必要的内存回收,那这个常量就会被系统清理出常量池。常量池中的其他类(接口)、方法和字段的符号应用也与此类似;判定一个无用类需要同时满足下面3个条件:

    • 该类的所有实例已经被回收,即Java堆中不存在该类的实例;
    • 加载该类的ClassLoader已经被回收;
    • 该类对象的Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。