第一篇 JVM 之 Class文件结构
JVM定义了一系列程序运行期间使用的运行时数据区(run-time data area)。这些数据区域中的一些随着JVM的启动而创建直到JVM的停止而销毁,而另一些则随着某个线程的创建而创建,随着线程的销毁而销毁。
为了能更直观的了解JVM的运行时数据区,我们先上张图来瞅瞅整个JVM内存的逻辑布局:
以上仅是一个JVM运行时内存布局的概念模型,我们可以看出JVM主要定义5大类运行时数据区:
1)虚拟机栈,2)方法区,3)本地方法栈,4)堆,5)程序计数器。
当然除了这几个数据区还有1)运行时常量池,2)帧,3)本地变量表,4)操作数栈等数据区,下面我们都会一一分析。
对于上图个人觉得除了看到这几块区域,也没什么深入的细节上的信息了,而且很多情况下还会误导初学者,比如很多人认为虚拟机栈就那么一块区域,其实不然,而且虚拟机栈可以是不连续的。
因此作为一名程序员,个人一直认为代码是最好的注释和文档,一行代码胜过千言万语。因此为了更好的理解JVM的内存模型,我们下面用JAVA代码的形式来深入分析下。
零,JVM
像上一篇文章一样,我们还是从整体着手,然后到具体的细节逐个分析。下面就是一个可能的JVM内存的Java实现的类结构图:
下面我们逐个列出每个数据区域的类实现来(注:该实现只是一个用来帮助理解的模型,会忽略很多细节,并且可能有不正确的地方,欢迎讨论)
//JVM.java
public class JVM {
private Heap heap;
private MethodArea methodArea;
private Map<String, NativeMethodStack> nativeMethodStacks;
private Map<String, VMStack> vmStacks;//假设线程名为键
private Map<String, PCRegister> pcRegisters;//假设线程名为键
//....getter, setter
}
上述代码很简单,清晰明了,不多说了。接下来我们就逐个深入分析。
===
一,Heap(堆)
堆是虚拟机中线程共享的一块数据区域,也就说所有的线程都可以访问这块区域的数据。同时堆是虚拟机中用来对象和数组分配内存的地方。堆的生命周期跟虚拟机一样,在虚拟机启动时创建,在虚拟机关闭时销毁。另外虚拟机中的对象无需显示的进行内存回收,JVM垃圾回收器会自动回收那些‘不用的’对象和数组。为更好的实现来及回收机制,通常JVM的实现会将堆内存划分为新生代(New Generation)和老年代(Tenured Generation),而新生代中又分为Eden Area和Survivor Area。下面我们看下堆内存的结构:
//堆
public class Heap {
private long xms;//min heap size
private long xmx;//max heap size
private NewGeneration newGenration;//新生代
private TenuredGeneration tenuredGeneration;//老年代
}
//新生代
public class NewGeneration {
private int survivorRatio;// = (eden size / survivor size)
private long xmn;//new generation
private EdenArea eden;
private SurvivorArea fromSurvivor;
private SurvivorArea toSurvivor;
}
//老年代
public class TenuredGeneration {
private byte[] memory;
}
//Eden
public class EdenArea {
private byte[] memory;
}
//Survivor
public class SurvivorArea {
private byte[] memory;
}
相信看到代码后你会感觉更加直观了。
二,VM Stack(虚拟机栈)
JVM虚拟机栈是线程私有数据区域,也就是每个线程都有一个自己的虚拟机栈内存,该内存随着线程的创建而创建,随着线程的销毁而销毁。虚拟机栈用来存储栈帧(frame),而栈帧会在下文详解。虚拟机栈类似于传统语言(如C)中的栈。它主要用来完成方法的调用和返回。由于虚拟机栈除了push和pop栈帧没有其他操作,所以虚拟机栈的内存可以是不连续的。下面是虚拟机栈的Java代码结构
import java.util.Stack;
public class VMStack {
private Thread owner;
private long stackDeep;//最大栈容量
private Stack<Frame> frames;
}
废话不多说,继续往下说,既然提到栈帧,我们就看看什么是栈帧。
三,Frame(栈帧)
栈帧用来存储方法执行期间的数据和部分结果,同时还会执行动态链接,返回方法返回值,以及分派异常等动作。
每当有方法被调用时,就会创建一个新的栈帧,并压入执行该方法的线程的虚拟机栈中。当方法执行结束后,该栈帧就会被弹出并销毁,无论该方法是正常结束还是异常退出。每个栈帧内部都有一个本地变量表和操作数栈,以及一个指向当前方法所属类的运行时常量池的引用。(本地变量表,操作数栈,运行时常量池将在下文分析)
本地变量表和操作数栈的大小在编译期就会被确定,并且其大小由与该栈帧关联的方法的代码决定,另外他们的内存可以在方法被调用时再分配。
对每个线程,任意时刻都只会有一个栈帧(当前执行方法的栈帧)处于活动状态。这个栈帧被称为当前栈帧(current frame),相关联的方法叫做当前方法(current method),当前方法所定义的类叫做当前类(current class)。
如果当前方法调用另一个方法,那么就会创建一个新的栈帧,并成为当前栈帧。当当前方法返回时,当前栈帧就会将返回值传递回前一帧,该栈帧销毁,前一帧成为当前栈帧。
注意:某个线程创建的栈帧是该线程私有的,其他线程无法访问到。至于详细的方法调用和执行的过程我们在后续文章会进行更为详细的分析。
import java.lang.reflect.Method;
public class Frame {
private LocalVariable[] localVariableTable;//本地变量表
private OperandStack operandStack;//操作数栈
private RuntimeConstantPool constantpool;//当前方法所属类的运行时常量池的引用
private VMStack ownerStack;//所属虚拟机数栈
}
四,LocalVariable (局部变量表)
==========================
上面已经提到,每个栈帧都会包含一个局部变量表(局部变量数组),用来存储方法参数,局部变量等数据。而且局部变量表的大小由所属栈帧的关联方法的代码决定,并在编译器就确定了。
一个局部变量可以保存一个boolean,byte,char,short,int,float,reference或returnAddress的值,一对局部变量可以保存一个long或double的值。局部变量表由下标索引,索引从0开始,最大值不超过变量表大小。
long和double的值占用两个相邻的局部变量,而且不许用两个局部变量中较小的那个下表来索引该long或double值。
虚拟机使用局部变量表来进行方法调用过程中的参数传递。在静态方法调用时,所有的参数会按照顺序保存到局部变量表中从第0个位置开始的连续的局部变量。而调用实例方法时,局部变量表的第0个位置始终保存调用该方法的对象的引用(this),然后从第1个位置开始保存方法的参数。
public class LocalVariable {
private Type type;
private Slot slot;
public enum Type{
_boolean,
_byte,
_char,
_short,
_int,
_float,
_reference,
_returnAddress,
_long,
_double
}
public static class Slot{
private byte[] values;
}
}
五,OperandStack(操作数栈)
第三部分已经提到,每个栈帧都包含一个后进先出的操作数栈,栈的深度在编译器便已确定,其大小由与该栈帧关联的方法体代码决定。
JVM提供了一些将常量或局部变量表中的变量加载到操作数栈中的指令,同样也提供了一些用来从操作数栈中获取数据,并操作他们,然后重新放回栈中的指令。操作数栈也会被用来准备传递给方法的参数以及接受方法的返回值。举个例子,iadd指令要求操作数栈顶预先有两个int值(其他指令压入),并将两个值弹出栈相加,让后将结果重新压回栈中。
操作数栈中的每个值都可以用来存储所有类型的数据,包括long,double。
操作数栈中的数据必须按照其类型进行适当的操作,比如我们不能将一个int值压入栈顶后按float类型弹出并操作。
public class OperandStack {
private Slot[] values;
public Slot pop(){return new Slot();}
public void push(Slot slot){}
public static class Slot{
private byte[] v;
}
}
六,Method Area(方法区)
同堆内存一样,方法区也是一个线程共享的数据区域。方法区有点类似传统编程语言(如C)中的用来存放编译代码的内存区域,或者类似于操作系统进程中的文本段。它主要保存着每个类的常量池,字段,方法,以及方法或构造器中的代码等数据(简单理解就是,每个类的Class文件加载,解析后就被存放在方法区中了)。
方法区的生命周期与虚拟机相同。尽管虚拟机中指出逻辑上方法区是堆内存的一部分,只是垃圾回收没有那么频繁,但是我们习惯上都会分开来讲。
更新:在HotSpot的实现中,方法区包含在了永久代中,同样在永久代中的还有一块区域我们可以称之为String literal pool(字符串常量池),该区域用于存放代码中的字符串字面量,以减少相同字符串对象创建带来的开销。最终内存布局参看下图。
public class PermGen{
private MethodArea methodArea;
private StringLiteralPool literalPool;
}
public class StringLiteralPool{
private byte[] values;
}
public class MethodArea {
private ClassInfo[] classes;
public static class ClassInfo{
private RuntimeConstantPool constantPool;
private Field[] fields;
private Method[] methods;
}
}
这里我们没有用java.lang.Class是因为我们下面要讲到RuntimeConstantPool。其实方法区中存放的主要就是java.lang.Class实例集合。
七,Runtime Constant Pool(运行时常量池)
每个运行时常量池都是某个对应类或者接口的class文件中的常量池的运行时映射。一个运行时常量池就像是传统编程语言里面的符号表,不过它所包含的数据类型比符号表丰富。
所有的运行时常量池都分配在方法区中,某个类或者接口的运行时常量池会在该类或者接口被加载时创建。
public class RuntimeConstantPool {
private ClassInfo clazz;
private byte[] values;
}
八,PC Register(程序计数器)
或许这个该放在最前面分析的。Java的多线程机制离不开程序计数器,每个线程都有一个自己的程序计数器,以便完成不同线程上下文环境的切换。
任意时刻,如果当前方法不是native的,那么程序计数器都会保存当前被执行的指令的地址。如果当前方法是native的,那么程序计数器的值为undefined。程序计数器应该足够大以至于可以容纳returnAddress和特定平台的指针。
public class PCRegister {
private Thread ownerThread;
private byte[] values;
}
九,Native Method Stack (本地方法栈)
JVM的实现可以使用本地方法区来作为传统语言的栈来支持本地方法的调用(native方法)。本地方法栈同样可以用于其他语言(如C)写的虚拟机指令集的解释器实现。通常本地方法栈也是线程私有的数据区,生命周期同线程相同。
更新:引用http://blog.jamesdbloom.com/JVMInternals.html 文章中的图(很详细)