第一篇 JVM 之 Class文件结构
html
JVM定义了一系列程序运行期间使用的运行时数据区(run-time data area)。这些数据区域中的一些随着JVM的启动而建立直到JVM的中止而销毁,而另外一些则随着某个线程的建立而建立,随着线程的销毁而销毁。java
为了能更直观的了解JVM的运行时数据区,咱们先上张图来瞅瞅整个JVM内存的逻辑布局:ios
以上仅是一个JVM运行时内存布局的概念模型,咱们能够看出JVM主要定义5大类运行时数据区:程序员
1)虚拟机栈,2)方法区,3)本地方法栈,4)堆,5)程序计数器。编程
固然除了这几个数据区还有1)运行时常量池,2)帧,3)本地变量表,4)操做数栈等数据区,下面咱们都会一一分析。数组
对于上图我的以为除了看到这几块区域,也没什么深刻的细节上的信息了,并且不少状况下还会误导初学者,好比不少人认为虚拟机栈就那么一块区域,其实否则,并且虚拟机栈能够是不连续的。多线程
所以做为一名程序员,我的一直认为代码是最好的注释和文档,一行代码赛过千言万语。所以为了更好的理解JVM的内存模型,咱们下面用JAVA代码的形式来深刻分析下。编程语言
零,JVM布局
像上一篇文章同样,咱们仍是从总体着手,而后到具体的细节逐个分析。下面就是一个可能的JVM内存的Java实现的类结构图:this
下面咱们逐个列出每一个数据区域的类实现来(注:该实现只是一个用来帮助理解的模型,会忽略不少细节,而且可能有不正确的地方,欢迎讨论)
//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 }
上述代码很简单,清晰明了,很少说了。接下来咱们就逐个深刻分析。
堆是虚拟机中线程共享的一块数据区域,也就说全部的线程均可以访问这块区域的数据。同时堆是虚拟机中用来对象和数组分配内存的地方。堆的生命周期跟虚拟机同样,在虚拟机启动时建立,在虚拟机关闭时销毁。另外虚拟机中的对象无需显示的进行内存回收,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; }
相信看到代码后你会感受更加直观了。
JVM虚拟机栈是线程私有数据区域,也就是每一个线程都有一个本身的虚拟机栈内存,该内存随着线程的建立而建立,随着线程的销毁而销毁。虚拟机栈用来存储栈帧(frame),而栈帧会在下文详解。虚拟机栈相似于传统语言(如C)中的栈。它主要用来完成方法的调用和返回。因为虚拟机栈除了push和pop栈帧没有其余操做,因此虚拟机栈的内存能够是不连续的。下面是虚拟机栈的Java代码结构
import java.util.Stack; public class VMStack { private Thread owner; private long stackDeep;//最大栈容量 private Stack<Frame> frames; }
废话很少说,继续往下说,既然提到栈帧,咱们就看看什么是栈帧。
栈帧用来存储方法执行期间的数据和部分结果,同时还会执行动态连接,返回方法返回值,以及分派异常等动做。
每当有方法被调用时,就会建立一个新的栈帧,并压入执行该方法的线程的虚拟机栈中。当方法执行结束后,该栈帧就会被弹出并销毁,不管该方法是正常结束仍是异常退出。每一个栈帧内部都有一个本地变量表和操做数栈,以及一个指向当前方法所属类的运行时常量池的引用。(本地变量表,操做数栈,运行时常量池将在下文分析)
本地变量表和操做数栈的大小在编译期就会被肯定,而且其大小由与该栈帧关联的方法的代码决定,另外他们的内存能够在方法被调用时再分配。
对每一个线程,任意时刻都只会有一个栈帧(当前执行方法的栈帧)处于活动状态。这个栈帧被称为当前栈帧(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;//所属虚拟机数栈 }
上面已经提到,每一个栈帧都会包含一个局部变量表(局部变量数组),用来存储方法参数,局部变量等数据。并且局部变量表的大小由所属栈帧的关联方法的代码决定,并在编译器就肯定了。
一个局部变量能够保存一个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; } }
第三部分已经提到,每一个栈帧都包含一个后进先出的操做数栈,栈的深度在编译器便已肯定,其大小由与该栈帧关联的方法体代码决定。
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; } }
同堆内存同样,方法区也是一个线程共享的数据区域。方法区有点相似传统编程语言(如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实例集合。
每一个运行时常量池都是某个对应类或者接口的class文件中的常量池的运行时映射。一个运行时常量池就像是传统编程语言里面的符号表,不过它所包含的数据类型比符号表丰富。
全部的运行时常量池都分配在方法区中,某个类或者接口的运行时常量池会在该类或者接口被加载时建立。
public class RuntimeConstantPool { private ClassInfo clazz; private byte[] values; }
或许这个该放在最前面分析的。Java的多线程机制离不开程序计数器,每一个线程都有一个本身的程序计数器,以便完成不一样线程上下文环境的切换。
任意时刻,若是当前方法不是native的,那么程序计数器都会保存当前被执行的指令的地址。若是当前方法是native的,那么程序计数器的值为undefined。程序计数器应该足够大以致于能够容纳returnAddress和特定平台的指针。
public class PCRegister { private Thread ownerThread; private byte[] values; }
JVM的实现可使用本地方法区来做为传统语言的栈来支持本地方法的调用(native方法)。本地方法栈一样能够用于其余语言(如C)写的虚拟机指令集的解释器实现。一般本地方法栈也是线程私有的数据区,生命周期同线程相同。
更新:引用http://blog.jamesdbloom.com/JVMInternals.html 文章中的图(很详细)