您的位置:首页 > 其它

深入理解JVM之JVM内存区域详解

2016-02-20 00:07 337 查看
一、 JVM概述

Java虚拟机是整个Java平台的基石,是Java技术用以实现硬件无关与操作系统无关的关键部分,是Java语言生成出极小体积的编译代码的运行平台,是保障用户机器免于恶意代码损害的保护屏障。JVM是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

Java语言写的源程序通过Java编译器,编译成与平台无关的字节码程序(.class文件),字节码文件是一种和任何具体机器环境及操作系统环境无关的中间代码,它是一种二进制文件,是Java源文件由Java编译器编译后生成的目标代码文件。Java解释器负责将字节码文件翻译成具体硬件环境和操作系统平台下的机器代码,以便执行。因此Java程序不能直接运行在现有的操作系统平台上,它必须运行在被称为Java虚拟机的软件平台之上。Java虚拟机(JVM)是运行Java程序的软件环境,Java解释器就是Java虚拟机的一部分。在运行Java程序时,首先会启动JVM,然
后由它来负责解释执行Java的字节码,并且Java字节码只能运行于JVM之上。这样利用JVM就可以把Java字节码程序和具体的硬件平台以及操作系统环境分隔开来,只要在不同的计算机上安装了针对于特定具体平台的JVM,Java程序就可以运行,而不用考虑当前具体的硬件平台及操作系统环境,也不用考虑字节码文件是在何种平台上生成的。JVM把这种不同软硬件平台的具体差别隐藏起来,从而实现了真正的二进制代码级的跨平台移植。JVM是Java平台 无关的基础,Java的跨平台特性正是通过在JVM中运行Java程序实现的。Java的这种运行机Java语言这种“一次编写,到处运行(writeonce,run
anywhere)”的方式,有效地解决了目前大多数高级程序设计语言需要针对不同系统来编译产生不同机器代码的问题,即硬件环境和操作平台的异构问题,大大降低了程序开发、维护和管理的开销。

需要注意的是,Java程序通过JVM可以达到跨平台特性,但JVM是不跨平台的。也就是说,不同操作系统之上的JVM是不同的,Windows平台之上的JVM不能用在Linux上面,反之亦然。



图一、java代码执行过程

二、JVM内存区域

JVM由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区域),Execution Engine(执行引擎),Native Interface(本地库接口)



图二、JVM组成结构图

下面主要来看运行时数据区域,也就是JVM所管理的内存:
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
1、PC寄存器
为了模拟一个栈结构,虚拟机的设计者们必须提供一套能够保存指令的地址的机制。在真实机器中,往往提供一个PC寄存器专门用来保存程序运行的指令在内存中的地址。在HotSpot实现中,为每个线程分配一个字长的存储空间,以实现类似硬件级的PC寄存器。
Java虚拟机可以支持多条线程同时执行,每一条Java虚拟机线程都有自己的PC(ProgramCounter)寄存器。在任意时刻,一条Java虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法。如果这个方法不是native的,那PC寄存器就保存Java虚拟机正在执行的字节码指令的地址,如果该方法是native的,那PC寄存器的值是undefined,这是因为本地方法的执行依赖硬件PC寄存器,其值由操作系统来维护的,虚拟机实现的PC寄存器对本地方法不会产生任何作用。PC寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值。
2、Java虚拟机栈

Java虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在Java栈中,只有在调用一个方法时,才为当前栈分配一个帧,然后将该帧压入栈。帧中存储了对应方法的局部数据,方法执行完,对应的帧则从栈中弹出,并把结果存储在调用方法的帧的操作数栈中。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是JVM运行时数据区中的虚拟机栈的元素。对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,即当前栈帧。执行引擎所运行的所有字节码指令中都只针对当前栈帧进行操作。



图三、栈帧组成结构图

Java虚拟机栈可能发生如下异常情况:

如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。

如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

2.1 局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。
在方法执行时,虚拟机是使用局部变量表完成参数变量列表的传递过程,如果是实例方法,那么局部变量表中的每0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数,其余参数则按照参数列表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域来分配其余的Slot。局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法,如果当前字节码PC计算器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其它变量使用。
局部变量不像前面介绍的类变量那样存在“准备阶段”。类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。
2.2 操作数栈
操作数栈也常被称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个”字宽“占4个字节,对于64位虚拟机来说,一个”字宽“占8个字节。
当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。
2.3 动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
2.4 方法返回地址
当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(NormalMethod Invocation Completion)。
另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(AbruptMethod Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。
无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

3、 本地方法栈
Java虚拟机实现可能会使用到传统的栈(通常称之为“C Stacks”)来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack)。当Java虚拟机使用其他语言(例如C语言)来实现指令集解释器时,也会使用到本地方法栈。如果Java虚拟机不支持natvie方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈,如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError。
4 、 Java堆
Heap是用来存放对象信息的,和Stack不同,Stack代表着一种运行时的状态。换句话说,栈是运行时单位,解决程序该如何执行的问题,而堆是存储的单位,解决数据存储的问题。Heap是被所有线程共享的一块区域,伴随着JVM的启动而创建,负责存储所有对象实例和数组的。堆的存储空间和栈一样是不需要连续的,从内存回收角度看,由于现在收集器基本都采用分代收集算法,所以java堆还可以细分为YoungGeneration(新生代)和Old
Generation(老年代)两大部分。YoungGeneration分为Eden和Survivor,Survivor又分为FromSpace和
ToSpace。如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。

5 、方法区
在Java虚拟机中,方法区是可供各条线程共享的运行时内存区域。Object Class Data(加载类的类定义数据) 是存储在方法区的。除此之外, 常量 、静态变量 、JIT(即时编译器)编译后的代码也都在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置为可扩展的,这点与堆一样。垃圾回收在这个区域会比较少出现,这个区域内存回收的目的主要针对常量池的回收和类的卸载。
6、 运行时常量池
方法区内部有一个非常重要的区域,叫做运行时常量池。在字节码文件(Class文件)中,除了有类的版本、字段、方法、接口等先关信息描述外,还有常量池(Constant Pool Table)信息,用于存储编译器产生的字面量和符号引用。这部分内容在类被加载后,都会存储到方法区中的运行时常量池。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如
String 类中的 intern() 方法产生的常量。

下面看一个例子来说明JVM的运行原理(该部分引用原文http://www.cnblogs.com/hellocsl/p/3969768.html?utm_source=tuicool&utm_medium=referral)、
public class JVMShowcase {
//静态类常量,
public final static String ClASS_CONST = "I'm a Const";
//私有实例变量
private int instanceVar=15;
public static void main(String[] args) {
//调用静态方法
runStaticMethod();
//调用非静态方法
JVMShowcase showcase=new JVMShowcase();
showcase.runNonStaticMethod(100);
}
//常规静态方法
public static String runStaticMethod(){
return ClASS_CONST;
}
//非静态方法
public int runNonStaticMethod(int parameter){
int methodVar=this.instanceVar * parameter;
return methodVar;
}
}


这个类没有任何意义,不用猜测这个类是做什么用,只是写一个比较典型的类,然后我们来看
看 JVM 是如何运行的,也就是输入 java JVMShow 后,我们来看 JVM 是如何处理的:
第 1 步 、向操作系统申请空闲内存。JVM 对操作系统说“给我 64M(随便模拟数据,并不是真实数据) 空闲内存”,于是,JVM 向操作系统申请空闲内存作系统就查找自己的内存分配表,找了段 64M 的内存写上“Java 占用”标签,然后把内存段的起始地址和终止地址给 JVM,JVM 准备加载类文件。
第 2 步,分配内存内存。JVM 分配内存。JVM 获得到 64M 内存,就开始得瑟了,首先给 heap 分个内存,然后给栈内存也分配好。
第 3 步,文件检查和分析class 文件。若发现有错误即返回错误。
第 4 步,加载类。加载类。由于没有指定加载器,JVM 默认使用 bootstrap 加载器,就把 rt.jar 下的所有类都加载到了堆类存的Method Area,JVMShow 也被加载到内存中。我们来看看Method Area区域,如下图:(这时候包含了 main 方法和 runStaticMethod方法的符号引用,因为它们都是静态方法,在类加载的时候就会加载



Heap 是空,Stack 是空,因为还没有对象的新建和线程被执行。

第 5 步、执行方法。执行 main 方法。执行启动一个线程,开始执行 main 方法,在 main 执行完毕前,方法区如下图所示:
public final static String ClASS_CONST = "I'm a Const";



在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被访问时产生的(runStaticMethod方法内部)。

堆内存中有两个对象 object 和 showcase 对象,如下图所示:(执行了JVMShowcase showcase=new JVMShowcase(); )



为什么会有 Object 对象呢?是因为它是 JVMShowcase 的父类,JVM 是先初始化父类,然后再初始化子类,甭管有多少个父类都初始化。

在栈内存中有三个栈帧,如下图所示:



于此同时,还创建了一个程序计数器指向下一条要执行的语句。

第 6 步,释放内存。释放内存。运行结束,JVM 向操作系统发送消息,说“内存用完了,我还给你”,运行结束。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: