JVM和GC


Java 虚拟机运行时数据区

  • 线程私有
    • 虚拟机栈(VM Stack),每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 本地方法栈(Native Method Stack),为虚拟机使用到的Native方法服务
    • 程序计数器(Program Counter Register),当前线程所执行的字节码的行号指示器
  • 所有线程共享
    • 堆(Heap),存放对象实例
    • 方法区(Method Area),存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

垃圾收集器(GC)

内存回收需要完成三件事:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

其中线程私有的虚拟机栈、本地方法区栈、程序计数器,在方法或线程结束时,内存也就被回收了。

而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存不一样,一个方法的多个分支需要的内存也可能不一样,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。

哪些内存需要回收


当堆中一个对象没有引用,不可能再被使用时需要进行回收

判断对象是否已死

引用计数法

  • 原理:给对象添加一个引用计数器,每当对象被引用时,计数器加1;当引用失效时,计数器减1;任何时候计数器为0的对象就是不可能再被使用的
  • 优点:实现简单,判定效率高
  • 缺点:很难解决对象之间相互循环引用的问题,即objA = objB, objB = objA,objA和objB相互引用计数器大于0,无法判读已死

可达性分析算法

  • 原理:建立一棵引用树,当对象和GC Roots之间没有任何引用链相通时,判定为可回收对象
  • 在Java语言中,可作为GC Roots的对象有:
    • 虚拟机栈(栈帧中的本地变量表)中的引用对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象

什么时候回收


如果对象在进行可达性分析后发现没有与GC Roots相通的引用链,并不会马上执行回收操作,而是将它进行第一次标记并且进行一次筛选,筛选条件为:此对象是否有必要执行finalize()方法,以下情况不执行finalize()

  1. 该对象没有覆盖finalize()方法
  2. 或者该对象的finalize()方法已经被虚拟机调用过

一个对象真正宣告死亡,至少要经历两次标记。

如果需要执行finalize()方法,那么这个对象将会放置在一个F-Queue的队列之中,并在稍后由虚拟机自动建立的、低优先级的Finalizer线程去执行它。

回收方法区

方法区主要主要存储的是生命周期比较长的类信息、常量、静态变量等信息,相当与堆中的老年代,垃圾收集效率低。主要可回收的内容:

  • 废弃常量

  • 无用的类,同时满足以下3个条件才算无用类:

    1. 该类的实例都已经被回收,也就是Java堆中不存在该类的任何实例

    2. 加载该类的ClassLoader已经被回收

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

如果回收


标记 - 清除算法

最基础的收集算法,后续算法都是基于这种思路并对其不足进行改进而得到的。

不足:

  • 效率问题:标记和清除都需要遍历,效率不高
  • 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要给大内存对象分配内存时,无法找到足够连续的内存而不得不提前触发下一次GC动作

复制算法

  • 原理:将可用的内存按容量划分为大小相等的两块(A、B),每次只使用其中的一块。当A块的内存用完了,就将还存活的对象复制到B块,然后对A已使用内存空间进行一次清理

  • 优点:每次都对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况

  • 缺点:内存缩小为原来的一半

  • 商业运用:

    商业虚拟机采用这种收集算法来回收新生代。根据新生代中的对象98%是”朝生夕死”的特点,将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到空闲的另一块的Survivor空间上,最后清理Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认分配大小 Eden : Survivor = 8 : 1

标记 - 整理算法

  • 原理:标记过程和”标记-清除”算法一样,但后续对象不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
  • 优点:无需复制

分代收集算法

  • 原理:根据对象存活周期的不同将内存划分为几块,一般Java将堆分为新生代和老生代。根据各个年代的特点采用最合适的收集算法:
    • 新生代使用灵活比例的复制算法
    • 老年代采用 标记-清除 或者 标记-整理 算法