—— 参考周志明的《understanding the JVM》
运行时数据区域
Java虚拟机在执行程序的过程中,会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,创建及销毁的时间。这些区域的生命周期主要与虚拟机进程和用户线程的启动和结束相关。
区域划分如下:
其中方法区和堆是所有线程共享的数据区,另外的就是线程隔离的数据区。
下面一一介绍这些数据区的用途。
线程私有的数据区域
程序计数器
Java虚拟机栈
本地方法栈
程序计数器
可以将它理解成当前线程所执行的字节码的行号指示器(字节码就是Java源程序经编译后生成的文件,Java程序的执行实际上是JVM里的字节码解释器执行字节码指令),在JVM的概念模型里,字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,进而来完成分支、循环、跳转、异常处理、线程恢复等基础功能。
线程恢复
这里再说说是如何实现线程恢复的。首先一个处理器一次只能执行一个线程,要在一个处理器要实现多线程,JVM是通过线程轮流切换并分配处理器执行时间的方式来实现的。每条线程都有一个独立的程序计数器,且各条线程之间计数器互不影响。在线程切换的过程中,程序计数器记录下该线程的当前执行位置,当该线程再次被切换时,就可以恢复原来的执行位置了。现在,你也应该理解为什么程序计数器属于线程私有了吧。
Java虚拟机栈(Java方法执行的内存模型)
- Java虚拟机栈里面装的是一个个栈帧(Stack Frame)。栈帧存储了局部变量名、操作数栈、动态链接、方法出口等信息。
- 每个方法执行的时候都会创建一个栈帧。一个方法从调研直至执行完成,对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
局部变量表
- 有的人Java内存区分为堆内存和栈内存。这是比较粗糙的分法。这里所指的”栈”,就是现在讲的虚拟机栈,再狭窄一点就是虚拟机栈中的局部变量表部分。
- 局部变量表存放了编译器可知的各种基本数据类型、引用类型和returnAddress类型(指向了一条字节码指令的地址)。
- 局部变量表所需的内存空间在编译期间完成分配,进入一个方法时,栈帧要分配多大的局部变量空间是完全确定的,运行时不会改变。
异常状况
- *Error:线程请求的栈深度大于虚拟机允许的深度
- OutOfMemoryError:虚拟机栈动态扩展时无法申请到足够的内存
在JVM规范中,该区域规定了以上两种异常情况,即如果抛出以上异常,则该区域出现了问题。
本地方法栈
与虚拟机栈相似,只是本地方法栈为虚拟机用到的Native方法服务,而虚拟机栈里面装的是Java方法(字节码)
Native方法:用来扩展Java程序的功能,如访问到操作系统底层。可以将native方法比作Java程序和C程序接口,如JNI技术。
线程共享的数据区
Java堆
方法区
Java堆
- 随虚拟机启动时创建,为所有对象实例及数组分配内存,GC管理的主要区域
- 区域细分图如下:首先从内存回收的角度,分为Young Generation和Old Generation。Young又细分为Eden Space和Survivor Space。Survivor又分为From Survivor和To Survivor空间。这些区域的作用及分配和回收细节见下文GC机制的讨论。
从内存分配的角度,线程共享的Java堆可能会划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。这样进一步划分,能够更快地分配内存、更好地回收内存。
方法区
- 用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 与Java堆相比,这区域可以选择不实现GC,因为一般来说,回收的内存并不多(这区域内存回收目标主要是针对常量池的回收和对类型的卸载),但也会有因未回收完成而导致内存泄漏的情况。
运行时常量池
- 运行时常量池是方法区的一部分
- Class文件包含一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用。(类加载后进入方法区的运行时常量池中存放)
GC机制
内存回收的主要对象
- 我们知道Java内存运行时区域主要分为5部分:程序计数器、虚拟机栈、本地方法栈,以及Java堆、方法区。
- 由于程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,且每一个栈帧分配多少内存基本上在类结构确定下来就已知的,故这些区域的内存分配和回收具备确定性。
- 而,Java堆和方法区是不确定的,一个接口的多个实现类需要的内存不一样,一个方法中的多个分支需要的内存也不一样,只有在程序运行期间才能知道创建哪些对象,内存分配和回收是动态的。
- 垃圾收集器GC主要关注的是 —— Java堆和方法区,也是线程共享的区域。
确定对象已死
引用计数算法
原理:给对象添加一个引用计数器、每当有一个地方引用它的时候,计数器+1;引用失效时,计数器-1;任何时刻计数器为0的对象就不可能再被使用。
缺陷:难以解决对象之间相互循环引用的问题。
class A {
Object o = null;
public static void testGC(){
A a1 = new A(); //a1、a2引用计数器增至1
A a2 = new A();
a1.o = a2; //a1、a2引用计数器增至2
a2.o = a1;
a1 = null; //a1、a2引用计数器减至1,但两各项仍相互引用
a2 = null; //解决方法:先执行语言a1.o=null和a2.o=null,让相互引用失效
System.gc();
}
}
可达性分析算法
原理:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(从图论的角度,即不可达),则证明此对象是不可用的。
如下:object5、object6、object7虽然有关联,但到GC Root是不可达的,所以会被定为是可回收的对象。
在Java语言中,可作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
不可达的对象也并非是“非死不可”
这篇文章就总结到这里吧,想知道为什么“不可达的对象也并非是“非死不可””,且看下回分解~