JVM运行时数据区
JVM内存分为虚拟机栈 (VM Stack)、堆 (Heap)、方法区 (Method Area)、程序计数器 (Program Counter Register)、 本地方法栈 (Native Method Stack)。其中,
- 线程共享,堆、方法区
- 线程独享,虚拟机栈、程序计数器、本地方法栈
虚拟机栈
每个线程有一个私有的栈,跟随线程创建而创建。
栈中存放栈帧 (frame),每个方法运行时会创建一个栈帧,存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从被调用到执行完毕,对应栈帧在虚拟机栈中的入栈和出栈。
通常栈指的是虚拟机栈中的局部变量表部分,局部变量表所需要的内存在编译时就被分配好了。栈的大小可动态扩展,
- 当扩展到无法再申请到足够内存时,抛出一个错误
OutOfMemoryError。 - 当栈调用深度大于JVM允许的栈深度(非确定值)时,抛出一个错误
StackOverflowError。
本地方法栈
和虚拟机栈类似,主要为JVM使用到的native方法服务。
程序计数器
每个线程有自己的程序计数器。
- 如果现在执行的是native方法,则PC寄存器值为空
- 如果现在执行的是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中对象死亡至少需要经历两次标记过程,
- 第一次是在可达性分析时被标记为不可达
- 第二次是在虚拟机简历的
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);
垃圾收集算法
标记清除
- 标记阶段,标记程序中每个活动对象
- 清除阶段,清除掉为标记对象并且重置标记位
缺点:
- 标记、清除过程效率不高
- 产生大量内存碎片,无法给大对象分配内存
标记整理
将所有存活对象移动到一起,直接清理掉边界外的内存
优点是不会产生内存碎片,缺点是移动对象导致效率不高,影响处理效率
复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
缺点是只使用了内存的一半。
分代收集
现代商业化虚拟机主要采用分代收集算法,它根据对象存活周期将内存划分为几个代,不同的代采用适合其的收集算法。
一般而言,JVM将堆分为新生代和老年代。
- 新生代使用:复制算法
- 老年代使用:标记-清除 或者 标记-整理 算法