JVM与垃圾收集讲解

213 阅读6分钟

JVM运行时数据区

JVM内存分为虚拟机栈 (VM Stack)、堆 (Heap)、方法区 (Method Area)、程序计数器 (Program Counter Register)、 本地方法栈 (Native Method Stack)。其中,

  • 线程共享,堆、方法区
  • 线程独享,虚拟机栈、程序计数器、本地方法栈

虚拟机栈

每个线程有一个私有的栈,跟随线程创建而创建。

栈中存放栈帧 (frame),每个方法运行时会创建一个栈帧,存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从被调用到执行完毕,对应栈帧在虚拟机栈中的入栈和出栈

通常栈指的是虚拟机栈中的局部变量表部分,局部变量表所需要的内存在编译时就被分配好了。栈的大小可动态扩展,

  1. 当扩展到无法再申请到足够内存时,抛出一个错误OutOfMemoryError
  2. 当栈调用深度大于JVM允许的栈深度(非确定值)时,抛出一个错误StackOverflowError

本地方法栈

和虚拟机栈类似,主要为JVM使用到的native方法服务。

程序计数器

每个线程有自己的程序计数器。

  1. 如果现在执行的是native方法,则PC寄存器值为空
  2. 如果现在执行的是JVM方法,则PC寄存器值为当前执行指令的地址

堆内存是JVM所有线程共享的,JVM启动时已经创建好了堆。JAVA中所有对象和数组都在堆中分配。可以通过GC回收掉这部分内存。当申请不到更多的内存空间时,会抛出OutOfMemoryError

方法区

所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。 这个区域的内存回收目标主要针对常量池的回收和对类型的卸载。 当方法区无法满足内存分配需求时,则抛出OutOfMemoryError异常。

以下是栈、堆和方法区之间的关系,

垃圾收集 (GC相关)

判断一个对象是否可被回收

引用计数法 (ReferenceCounting)

给对象添加一个引用计数器,当对象增加一个引用时计数器+1,减少一个引用时,计数器-1。当对象的计数器为0时,表明对象可以被回收掉了。

这种方法看似没有什么问题,但是当它遇到循环引用的情况就不行了,如下,

public class MyObject {
    public Object ref = null;
    public static void main(String[] args) {
        MyObject myObject1 = new MyObject();
        MyObject myObject2 = new MyObject();
        myObject1.ref = myObject2;
        myObject2.ref = myObject1;
        myObject1 = null;
        myObject2 = null;
    }
}

此时myObject1与myObject2互为引用,引用计数器永远不会为0,导致GC无法回收掉他们。正是因为循环引用的存在,Java虚拟机不使用引用计数法。

可达性分析 (Readchablility Analysis)

以GC Roots为起始点开始向下搜索,当一个对象到GC Roots不可达时,证明此对象不可用了。

在Java语言中,常作为GC Roots的对象为:

  • 虚拟机栈 (栈帧中的局部变量表)中引用的对象
  • 本地方法栈中JNI (即native方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

这些对象能作为GC Roots对象的原因是:GC (Garbage Collector)是用来管理JVM中的堆,而虚拟机栈、本地方法栈、方法区和常量池并不被GC管理。

可达性分析算法中,上述提到的不可达的对象并不是立即死亡的,对象有一次自我拯救的机会 (finalize方法)。在JVM中对象死亡至少需要经历两次标记过程,

  1. 第一次是在可达性分析时被标记为不可达
  2. 第二次是在虚拟机简历的Finalizer队列中判断对象是否需要执行finalize方法。

finalize方法

finalize()方法如果没有被复写或调用,则判定为不需要执行,对象死亡。finalize()方法如果在Finalizer队列中缓慢执行还没有结束或者发生了死循环,也会被宣告死亡。拯救自己的方法就是在finalize()方法建立起引用,那么第二次标记时对象就会被移除出“即将回收”的集合。

但因为每个对象只能触发一次finalize()方法,并且其运行代价高昂,有很高的不确定性,所以不推荐使用。

引用类型

判定对象是否可以被回收都与引用类型有关。

强引用

被强引用关联的对象不会被回收 。可通过 new 来创建强引用。

Object obj = new Object();

软引用 (SoftReference)

被软引用关联的对象只有在内存不够的情况下才会被回收。 使用 SoftReference 类来创建软引用。

Object obj = new Object(); 
SoftReference<Object> sf = new SoftReference<Object>(obj); 
obj = null; // 使对象只被软引用关联

弱引用 (WeakReference)

被弱引用关联的对象一定会被回收,它只能存活到下一次GC发生之前。 使用 WeakReference 类来创建弱引用。

Object obj = new Object(); 
WeakReference<Object> wf = new WeakReference<Object>(obj); 
obj = null;

虚引用 (PhantomReference)

虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(obj, queue);

垃圾收集算法

标记清除

  1. 标记阶段,标记程序中每个活动对象
  2. 清除阶段,清除掉为标记对象并且重置标记位

缺点:

  1. 标记、清除过程效率不高
  2. 产生大量内存碎片,无法给大对象分配内存

标记整理

将所有存活对象移动到一起,直接清理掉边界外的内存

优点是不会产生内存碎片,缺点是移动对象导致效率不高,影响处理效率

复制

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

缺点是只使用了内存的一半。

分代收集

现代商业化虚拟机主要采用分代收集算法,它根据对象存活周期将内存划分为几个代,不同的代采用适合其的收集算法。

一般而言,JVM将堆分为新生代和老年代

  1. 新生代使用:复制算法
  2. 老年代使用:标记-清除 或者 标记-整理 算法