JVM深入理解(一)
JVM内存模型
- 堆:(new)对象存储
- 方法区:类的加载信息、常量、静态变量、即时编译后的代码
- 程序计数器:每个线程私有的标记代码的运行位置
- 虚拟机栈:存储每个方法运行创建的栈帧(局部变量表(对象的引用(对C++中的指针的封装)、基础数据类型)、操作数栈、动态链接、方法出口)
- 本地方法栈:存储本地方法的服务
JVM中GC算法流程——标记&清除
不同的JVM有不同的GC算法,但是都遵循“标记&清除”的基础流程:
首选标记分为二次标记:第一次先通过GCroot节点对所有的存活的对象进行引用追踪,其中GCroot节点来源于——虚拟机栈中的对象引用、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象,通过GCroot节点第一次的检查可以发现不能到达的对象,然后对这些对象进行第一次标记,在标记过程对这些对象进行检查——是否重写了finalize方法、或者finalize方法是否被调用过,结果分为两种情况:
-
若重写了finalize方法且这个方法没有被调用过则对这个方法进行调用
-
若没有重写则不进行调用。
-
若重写了finalize方法但是之前调用过此方法则不执行。
通过第一次标记之后调用finalize方法有的对象可能又被重新引用(逃离死亡),而有的对象则没有逃离,所以在GC发起第二次标记的时候剩下的对象则被清除。其中要注意的是在执行finalize方法时,GC不会等待finalize方法,主要是因为finalize方法可能会长时间执行或假死而导致整个系统的崩溃。
不同的GC算法主要是针对基础的标记清除算法中的不足之处的改进:
- 复制算法:将堆中的内存分为两个部分,每次只使用一个部分,每次清除也只清除另一个部分,如果这个部分使用完,则将这个部分的中对象复制到另一部分中(能放多少放多少),缺点在于将堆内存分为两个部分,导致内存可用空间缩小,代价太高。
- 标记整理算法:这个算法只是对标记清除后的碎片内存空间进行整理
- 分代收集算法:所谓分代指的是将堆中的内存分为新生代和老年代,这两个部分中GC效率是不同的,选用不同的算法,在由于新生代中对象死亡率高,所以选用复制算法,而老年代中对象死亡率低则使用基础的标记清除或者标记整理算法。
在GC过程中要对对象进行标记,在此过程中对象不能再进行更改引用,因此在GC过程中必须要暂停所有线程,但是暂停不能过于频繁,也不能太少,要选择合适的点进行暂停(安全点,这个安全点一般在需要长时间执行的代码处进行标记(for循环、方法调用、异常跳转)),在运行到安全点暂停之后进行GC的检查过程中如果对所有的对象进行遍历检查,代价过高,hotspot中采用oop数据结构对GCroot节点中的对象引用进行标记,在检查时就可以很快的找到引用对象的位置,因而可以快速确定未被引用的对象位置,但是在代码运行过程中对象的引用是不断变化的,可能运行到这行代码对象引用还是这样,但是下一行代码又产生新的对象引用,这样oop的内容过多,导致一系列的问题,所以在代码运行过程中只在安全点处进行标记——运行到这行代码时,那些对象有引用,那些对象没有引用?。
上面说到要对所有线程进行暂停,但是有的线程执行时,不能立即暂停,需要让它运行到最近的安全点然后暂停,对于所有线程如何暂停——当需要暂停时,JVM生成一个test轮询指令,所有线程对这个指令进行轮询,当线程轮询到这个指令时就暂停。然而还有一个问题,就是在进行GC需要暂停时,在运行的线程可以进行轮询然后暂停,但是若线程此时处于sleep或者blocked状态时,显然它收不到轮询的指令,但是JVM又不知道这个线程什么时候会开始执行,所以为了防止在暂停时由于sleep或blocked状态的线程开始运行而导致对象引用发生变化,JVM设置一个安全域(safeRegion)——在安全域中的线程禁止对象引用发生改变。在线程要离开安全域时检查系统是否完成根节点枚举或者整个GC过程,如果完成则继续运行,反之则等待。