JVM运行时内存结构-数据区
JVM在运行时将数据划分为6个区来储存,而不仅仅是大家所熟悉的Stack和Heap区域,这6个区域的示意图如下:

PC寄存器、Java栈、本地方法栈是每个线程独自拥有的,其他区域是整个JVM实例中的所有线程共有的。
第一块:PC 寄存器
每一个线程都拥有一个PC Register,当线程启动(start)时,PC Register被创建。
用于存储每个线程下一步该执行的字节码指令(JVM指令)的地址,如该方法是native的,则寄存器中不存储任何信息。
第二块:JVM栈
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本数据类型变量,部分的返回结果以及Stack Frame,对象则在栈上保存一个指向堆上的地址。
栈存放着一系列的栈帧(Stack Frame),JVM只能进行压入(Push)和弹出(Pop)栈帧这两种操作。每当调用一个方法时,JVM就往栈里压入一个栈帧,方法结束返回时弹出栈帧。如果方法执行时出现异常,可以调用printStackTrace等方法来查看栈的情况。

现在我们再来详细看看每一个栈帧中都放着什么东西。从示意图很容易看出,每个栈帧包含三个部分:本地变量数组,操作数栈,方法所属类的常量池引用。
※局部(本地)变量数组:从0开始按顺序存放方法所属对象的引用、传递给方法的参数、局部变量。
举个例子:
public void doSomething(int a, double b, Object o) {
...
}
这个方法的栈帧中的局部变量存储的内容分别是:
0: this
1: a
2,3:b
4:0
看仔细了,其中double类型的b需要两个连续的索引。取值的时候,取出的是2这个索引中的值。如果是静态方法,则数组第0个不存放this引用,而是直接存储传递的参数。
※操作数栈
操作数栈中存放方法执行时的一些中间变量,JVM在执行方法时压入或者弹出这些变量。其实,操作数栈是方法真正工作的地方,执行方法时,局部变量数组与操作数栈根据方法定义进行数据交换。例如,执行以下代码时,操作数栈的情况如下:
int a = 100;
int b = 98;
int c = a + b;

注意在这个图中,操作数栈的地步是在上边,所以先压入的100位于上方。可以看出,操作数栈其实是一个数据临时存储区,存放一些中间变量,方法结束了,操作数栈也就没有啦。
※栈帧中数据引用
除了局部变量数组和操作数栈之外,栈帧还需要一个常量池的引用。当JVM执行到需要常量池的数据时,就是通过这个引用来访问常量池的。
栈帧中的数据还要负责处理方法的返回和异常。
如果通过return返回,则将该方法的栈帧从Java栈中弹出。如果方法有返回值,则将返回值压入到调用该方法的方法的操作数栈中。
另外,数据区中还保存中该方法可能的异常表的引用。
下面的例子用来说明:
class Example3C{
public static void addAndPrint(){
double result = addTwoTypes(1,88.88);
System.out.println(result);
}
public static double addTwoTypes(int i, double d){
return i+d;
}
}
执行上述代码时,Java栈如下图所示:

花些时间好好研究上图。一样需要注意的是,栈的底部在上方,先压入addAndPrint方法的栈帧,再压入addTwoTypes方法的栈帧。
第三块:本地方法堆栈(Native Method Stacks)
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
当程序通过JNI(Java Native Interface)调用本地方法(如C或者C++代码)时,就根据本地方法的语言类型建立相应的栈。
第四块:堆(Heap)
用来储存对象实例和以及数组值的区域,这些对象由GC负责回收。
JAVA将Heap分为New Generation和Old Generation两块来进行管理:

(1)New Generation(新生代)
程序中新生成的对象都分配在这里,新生代又由Eden Space和两块Survivor Space构成。可用-Xmn参数来指定其大小。
(2)Old Generation(旧生代)
经过几次垃圾回收还存活的对象则进入旧生代,例如缓存的对象等。旧生代占用的内存大小为-Xmx减去-Xmn的大小。
对堆的解释:
(1)堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
(2)鉴于上面的原因,Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
(3)TLAB仅作用于新生代的Eden Space 但这种方法同时也带来了两个问题,一是空间的浪费,二是对象内存的回收上仍然没法做到像Stack那么高效,同时也会增加回收时的资源的消耗,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效,通过在启动参数上增加-XX:+PrintTLAB来查看TLAB这块的使用情况。
第五块:方法区域(Method Area)
(1)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,可见方法区域的重要性,同样,方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
(2)在Sun JDK中这块区域对应的为Permanet Generation,又称为持久代,默认为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其大小。
第六块:运行时常量池(Runtime Constant Pool)
运行常量池本应该属于方法区,但是由于其重要性,JVM规范将其独立出来说明。
类似C中的符号表,存放类和接口的常量、方法和Field的引用信息等。当一个方法或者Field被引用的时候,JVM就通过运行常量池中的这些引用来查找方法和Field在内存中的的实际地址。
其空间从方法区域中分配。

浙公网安备 33010602011771号