Java虚拟机详解(二)------运行时内存结构

  首先经过一张图了解 Java程序的执行流程:html

  

  咱们编写好的Java源代码程序,经过Java编译器javac编译成Java虚拟机识别的class文件(字节码文件),而后由 JVM 中的类加载器加载编译生成的字节码文件,加载完毕以后再由 JVM 执行引擎去执行。在加载完毕到执行过程当中,JVM会将程序执行时用到的数据和相关信息存储在运行时数据区(Runtime Data Area),这块区域也就是咱们常说的JVM内存结构,垃圾回收也是做用在该区域。java

  关于这幅图涉及到的:算法

  ①、class文件数组

  ②、类加载器数据结构

  ③、运行时数据区多线程

  ④、执行引擎并发

  ⑤、垃圾回收器oracle

  这都是接下来将要介绍的重点。jvm

  本篇博客咱们将首先介绍什么是运行时数据区。函数

  PS:下面介绍的是根据 Java虚拟机规范 定义的运行时数据区,上一篇博客咱们讲过根据虚拟机规范实现的虚拟机有不少个,而不一样的虚拟机其运行时数据区定义也会有所不一样。好比默认的 HotSpot 在实现 JDK1.7 虚拟机规范时,其常量池的定义不在方法区中,而是移到了堆中;到了 HotSpot JDK1.8 中,则完全移除了持久代(方法区)而使用Metaspace(元数据区)来进行替代等等,关于这些区别本篇博客也会在文章末尾进行相应的说明。

一、运行时数据区结构图

  ①、Java虚拟机规范定义的运行时数据区

   

  ②、HotSpot JDK1.8定义的运行时数据区

  

  注意:HotSpot实现的运行时数据区和Java虚拟机规范定义的仍是有所不一样的,

  ①、将Java虚拟机栈和本地方法栈合二为一;

  ②、元数据区取代了方法区,而且元数据区不在Java虚拟机中,而是在本地内存中。

  ③、运行时常量池由方法区中移到了堆中

二、程序计数器

  程序计数器(Program Conputer Register)这是一块较小的内存空间,能够看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器的工做就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。

  ①、线程私有

  Java虚拟机支持多线程,是经过线程轮流切换并分配处理器执行时间的方式来实现的,在任一肯定的时刻,一个处理器只会执行一条线程中的指令,所以为了线程切换后能恢复到正确的执行位置,每条线程都须要一个独立的程序计数器。所以线程启动时,JVM 会为每一个线程分配一个PC寄存器(Program Conter,也称程序计数器)。

  ②、记录当前字节码指令执行地址

  若是当前线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是 Native 方法,则这个计数器值为空(Undefined)。

  ③、不抛 OutOfMemoryError 异常

  程序计数器的空间大小不会随着程序执行而改变,始终只是保存一个 returnAdress 类型的数据或者一个与平台相关的本地指针的值。因此该区域是Java运行时内存区域中惟一一个Java虚拟机规范中没有规定任何 OutOfMemoryError 状况的区域。

三、虚拟机栈

  Java虚拟机栈(Java Virtual Machine stack),这块区域也是线程私有的,与线程同时建立,用于存储栈帧。Java 每一个方法执行的时候都会同时建立一个栈帧(Stack Frame)用于存储局部变量表、操做栈、动态连接、方法出口等信息,每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  

  ①、线程私有

  随线程建立而建立,声明周期和线程保持一致。

  ②、由栈帧组成

  线程每一个方法被执行的时候都会建立一个栈帧,用于存储局部变量表、操做栈、动态连接、方法出口等信息,每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  ③、抛出 StackOverflowError 和 OutOfMemoryError 异常

  若是线程请求的栈深度大于虚拟机所容许的深度,将抛出 StackOverflowError 异常;若是虚拟机栈能够动态扩展,当扩展时没法申请到足够的内存时会抛出 OutOfMemoryError  异常。

四、本地方法栈

  本地方法栈(Native Method Stacks)做用和虚拟机栈类型,虚拟机栈执行的是Java方法,本地方法栈执行的是 Native 方法,本地方法栈也会抛出抛出 StackOverflowError 和 OutOfMemoryError 异常。

  注意:因为虚拟机规范并无对本地方法栈中的方法使用语言、使用方式和数据结构强制规定,所以具体的虚拟机能够自由实现它。上图咱们也给出在 HotSpot 虚拟机中,本地方法栈和虚拟机栈合为一体了。

五、Java堆

  Java堆是Java虚拟机所管理内存最大、被全部线程共享的一块区域,目的是用来存放对象,基本上全部的对象实例和数组都在堆上分配(不是绝对)。Java堆也是垃圾回收器管理的主要区域。

  ①、线程共享

  堆存放的对象,某个线程修改了对象属性,另一个线程从堆中获取的该对象是修改后的对象,为何堆要设计成线程共享呢?

  咱们能够假设堆是线程私有的,很显然一个系统建立的对象会有不少,并且有些对象会比较大,若是设计成线程私有的,那么若是有不少线程同时工做,那么都必须给他们分配相应的私有内存,我相信内存很快就撑爆了,很显然将堆设计为线程共享是最好不过了,不过凡事都具备两面性,线程共享的设计这也带来了多线程并发资源冲突问题,关于这个问题因为不是本系列博客的主旨,这里就不作详细介绍了。

  ②、存放对象

  基本上全部的对象实例和数组都要在堆上进行分配,可是随着 JIT 编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换等优化技术会致使对象不必定在堆上进行分配。

  ③、垃圾收集

  Java堆也被称为“GC堆”,是垃圾回收器的主要操做内存区域。当前垃圾回收器都是使用的分代收集算法,因此Java堆还能够分为:新生代和老年代,而新生代又能够分为 Eden 空间、From Survivor 空间、To Survivor空间。这是为了更好的回收内存,关于垃圾回收算法在后续博客会详细介绍。

  

  ④、抛出 OutOfMemoryError 异常

  根据Java虚拟机规范,Java堆能够处于物理上不连续的内存空间中,只要逻辑上连续便可,实现时既能够实现成固定大小,也能够是扩展的。若是在堆中没有完成实例分配,而且堆也没法扩展,将抛出OutOfMemoryError 异常。

六、方法区

   方法区(Method Area)用来存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  方法区也称为“永久代”,这是由于垃圾回收器对方法区的垃圾回收比较少,主要是针对常量池的回收以及对类型的卸载,回收条件比较苛刻。常常会致使对此内存未彻底回收而致使内存泄露,最后当方法区没法知足内存分配时,将抛出 OutOfMemoryError 异常。

  PS:在Java虚拟机规范中把方法区描述为堆的一个逻辑部分(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4),在不少虚拟机中(JRockit、IBM J9等虚拟机不存在永久代的概念)。

     在JDK1.8 的 HotSpot 虚拟机中,已经去掉了方法区的概念,用 Metaspace 代替,而且将其移到了本地内存来规划了。

七、运行时常量池

   在Java虚拟机规范中,运行时常量池(Runtime Constant Pool)用于存放编译期生成的各类字面量和符号引用,是方法区的一部分。可是Java虚拟机规范对其没有作任何细节的要求,因此不一样虚拟机实现商能够按照本身的需求来实现该区域,好比在 HotSpot 虚拟机实现中,就将运行时常量池移到了堆中。

  ①、存放字面量、符号引用、直接引用

  一般来讲,该区域除了保存Class文件中描述的引用外,还会把翻译出来的直接引用也存储在运行时常量池,而且Java语言并不要求常量必定只能在编译器产生,运行期间也可能将常量放入池中,好比String类的intern()方法,当调用intern方法时,若是池中已经包含一个与该String肯定的字符串相同equals(Object)的字符串,则返回该字符串。不然,将此String对象添加到池中,并返回此对象的引用。关于该方法的介绍能够看我这篇博客

  ②、抛出 OutOfMemoryError 异常

  运行时常量池是方法区的一部分,会受到方法区内存的限制,当常量池没法申请到内存时,会抛出该异常。

八、直接内存

  直接内存(Direct Memory)并非虚拟机运行时数据区的一部分,它也不是Java虚拟机规范定义的内存区域。咱们能够看到在 HotSpot 中,就将方法区移除了,用元数据区来代替,而且将元数据区从虚拟机运行时数据区移除了,转到了本地内存中,也就是说这块区域是受本机物理内存的限制,当申请的内存超过了本机物理内存,才会抛出 OutOfMemoryError 异常。

  直接内存也是受本机物理内存的限制,在JDK1.4中新加入的 NIO(new input/output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可使用 Native 函数库直接分配堆外内存,而后经过一个存储在Java堆里面的 DirectByteBuffer 对象做为这块内存的引用操做,这样避免了在Java堆和Native堆中来回复制数据,显著提升性能。

相关文章
相关标签/搜索