Java 的 .class
文件经过类加载器加载进虚拟机内存里面,由 JVM 虚拟机经过解析执行、或编译执行。JVM 为了方便管理被加载进来的 .class
内容,提出了 Java 虚拟机运行时数据区的概念。Java 虚拟机运行时数据区能够划分为线程私有、线程共享两大类型的数据区,其中线程私有包括程序计数器、虚拟机栈、本地方法栈;而线程共享包括 Java 堆、方法区。程序员
在没有深刻理解 JVM 以前,咱们经常会把 Java 运行时数据区粗粒度地划分为 "堆"、"栈" 两大部分,"堆" 是用来存放对象地实例,而 "栈" 则是用来存放对象地引用。随着咱们对 JVM 地深刻学习,咱们发现 JVM 对内存的划分远比咱们在学习 Java 初级阶段所认知的运行时数据区要复杂。C/C++ 的程序员须要手动释放程序里面不须要再用到的内存空间,而在 Java 里面,虚拟机会自动地帮咱们回收不须要用到的资源,这就须要咱们深刻理解 JVM 运行时的内存划分,有利于咱们对程序有更加深入的认识。算法
程序计数器(Program Counter Register)是一块比较小的数据区域。由于在 Java 中是支持多线程的,那就意味着每条线程内须要一块内存空间,记录当前线程切换的时候(现场销毁),当前字节码执行的行号数,以便在该线程从新获取到 CPU 执行时间的时候,能够接着上次执行的字节码行数号继续执行(现场恢复)。而程序计数器则是为了记录当前线程的字节码执行的行数号而提出的。数组
字节码的解析器是根据该计数器的值来选取下一条须要解析执行的字节码指令。Java 代码的循环、跳转、异常处理、线程恢复等都须要依靠该计数器来完成。数据结构
若是线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是 native 方法,这个计数器的值则为空(Undefined)。程序计数器是 JVM 惟一一个没有规定任何 OOM 状况的区域。多线程
虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:在 Java 中每执行一个方法都会建立一个栈帧,而后系统会把这个栈帧压入虚拟机栈。栈帧还能够划分为局部变量表、操做数栈、动态连接、方法出口(方法返回地址)、以及额外的附加信息。Java 方法的执行就对应着栈帧在虚拟机中入栈和出栈的过程。布局
局部变量表主要存放的是方法在执行过程当中的局部变量。如在方法中定义的八种基本类型、局部的变量等。学习
操做数栈存放的是操做数。操做数栈是一个栈结构的数据结构,栈元素能够为任意的 Java 数据类型。一个方法刚开始分配栈帧空间执行的时候,操做数栈是空的,当在执行方法内局部变量的运算的时候,操做数栈便会执行进栈/出栈操做。线程
动态连接的做用主要是支持 Java 语言的多态性(须要类加载、运行时才能肯定的方法)、动态性。指针
方法返回地址返回的是方法结果的返回。若是是正常返回,则调用程序计数器中的地址做为返回;若是是异常返回,则是经过异常处理器表(非栈帧中的)来肯定。code
同时,Java 虚拟机规范中定义了虚拟机栈有两种异常:
异常 | 定义 |
---|---|
StackOverFlowError | 当虚拟机请求的栈深度超过当前栈帧所一开始定义的栈帧深度时抛出 |
OutOfMemoryError | 若是虚拟机能够在运行时动态申请栈内存空间,当 Java 虚拟机没法申请到更多的栈内存的时候抛出 |
本地方法栈(Native Method Stack)与虚拟机栈所发挥的做用时比较类似的,只不过虚拟机栈为 Java 的普通方法提供内存空间,而本地方法栈则为 Java 的本地方法提供内存空间。所以,有的虚拟机如 Sun HotSpot 虚拟机就直接地把虚拟机栈和本地方法栈合二为一。
同时,本地方法栈和虚拟机栈同样,也会抛出 StackOverFlowError
、OutOfMemoryError
两种异常。
Java 堆是虚拟机中最大地一块内存空间,同时该内存区域也是每条线程能够共享的。Java 堆是在虚拟机建立的时候建立的,其主要的目的使用来存放对象实例、数组数据。但随着 JIT编译器的发展、逃逸分析技术的发展,使得对象能够在栈中分配。
Java 堆是 GC 垃圾回收的主要区域。从内存回收的角度,由于 Java 堆主要采起分代收集算法,于是 Java 堆能够划分为新生代、老年代,而新生代又能够划分为 Eden空间、From Survivor和 To Survice空间。
从内存共享的角度,Java 堆能够被划分为多个线程共享的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
根据虚拟机规范,Java 堆能够是物理内存上不连续,而在逻辑上连续的。同时,堆在没法完成实力的分配,而且虚拟机没法申请到更多的堆内存的时候,会抛出 OutOfMemoryError
异常。
在虚拟机中,对象实例主要是分配 Java 堆中,对象实例在内存的布局以下:
对象实例的内存布局分为 3 块区域:对象头(Header)、实例数据(Instance)、对齐填充(Padding)。
对象头(Header)在 HotSpot 中包括两部分信息:用于存储对象自身的运行时数据、类型指针。对于第一部分用于存储自身运行时数据如上图;而对于第二部分类型指针,即对象指向它的元数据的指针,虚拟机经过这个指针来肯定这个对象它属于哪个类的实例。但并非全部的虚拟机都须要在对象头数据上面保留类型指针,由于查找对象的元数据不必定须要经过对象的自己(如反射)。
实例数据部分是对象真正存储的有效信息的,如在 Java 源码中所定义的各类类型的字段内容,包括从父类继承的数据内容。同时在实例数据部分的存储收到虚拟机的分配策略(FieldsAllocationStyle)和字段在 Java 源码中顺序的影响。
对齐填充并非必然存在对象实例布局中的,其没有特别的含义。由于虚拟机的自动内存管理系统要求对象的大小必须须要 8 字节的整数倍。对象头大小恰好是 8 字节的倍数(1 倍或 2 倍),当实例数据没有对齐的时,就须要对齐填充来补全。
在 Java 堆中存放对象的实例的目的是为了访问使用对象,Java 程序须要经过虚拟机栈上本地变量表上的 reference 数据来操做堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。
同时,该 Java 堆能够抛出 OutOfMemoryError
异常。
方法区(Method Area)与 Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据,GC 在该区域出现的比较少。同时,该方法区还包含着运行时常量池。
运行时常量池用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。
各类字面量与 Java 语言层面概念相近,包含文本字符串、声明为 final 的常量等;
符号引用包括:类和接口的全限定名称、字段的名称和描述符、方法的名称和描述符;
同时,方法区也能够抛出 OutOfMemoryError
异常。
以上,包含了 JVM 运行时为 Java 的 .class
加载进 JVM 所划分的区域,分别为线程私有和线程共享的区域。固然,这只是深刻理解 JVM 的过程的一小步,接下来还须要了解 JVM 对 Java 堆的垃圾标记、以及垃圾收集......