文章目录
- 程序计数器
- Java 虚拟机栈
- 栈帧
- 局部变量表
- 动态连接
- 方法返回地址
- 操作数栈
- 本地方法栈
- Java 堆
- 参数
- 方法区
- 元空间
- 运行时常量池
- 直接内存
- 案例
- 参考资料
(上图的常量池应在Java heap中)
程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java 虚拟机栈
常说的“堆栈”中的栈即是虚拟机栈。
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
Person a = new Person();//new 会在堆中产生一个 Person() 类的对象,a存在于栈中,指向堆中的对象
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M:
java -Xss2M HackTheJava
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
- HotSpot 虚拟机不可以动态拓展,申请失败时仍然会出现 OOM。
栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。
因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表
主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。
也就是说局部变量表在栈中起着举足轻重的作用。
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
- 编译器可知的各种 Java 虚拟机基本数据类型,存储空间的单位是局部变量槽(Slot)
- boolean / byte / char / short / int / float / long / double
- 槽复用:为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕),那这个变量对应的Slot就可以交给其他变量使用。
- 优点 : 节省栈帧空间。
- 缺点 : 影响到系统的垃圾收集行为。(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。)
- 对象引用
- reference类型,不等同于对象本身,可能只是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或者其它与此对象相关的位置。通过该类型可以查找:
①在Java堆中的数据存放的起始地址索引。
②所属数据类型在方法区中的存储的类型数据。
- returnAddress
- 指向了一条字节码指令的地址
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。
方法返回地址
当一个方法开始执行后,只有2种方式可以退出这个方法 :
方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
操作数栈
操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。
操作数栈的每一个元素可用是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型占用的栈容量为2。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。
例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。
虽然两个栈帧作为虚拟机栈的元素是完全独立的,但是虚拟机会做出相应的优化,令两个栈帧出现一部分重叠。
如上图所示,栈帧的部分操作数栈与上一个栈帧的局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递。
本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
Java 堆
Java 堆在 JVM 启动时创建,唯一的目的就是存放对象实例。所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。
在1.8及以后,堆还存放静态变量和常量池。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
- 新生代(Young Generation)
- 老年代(Old Generation)
将堆细分的目的只是为了更好地回收内存以及更快地分配内存。
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
参数
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M -Xmx 1024M
,其中 -X
这个字母代表它是JVM运行时参数,ms
是memory start
的简称,中文意思就是内存初始值,mx
是 memory max
的简称,意思就是最大内存。
在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力 所以在线上生产环境中 JVM的Xms
和 Xmx
会设置成同样大小,避免在GC 后调整堆大小时带来的额外压力。
方法区
用于存放线程共享数据,如已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。
由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。
为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类元信息、字段、静态属性、方法,静态变量和常量池、常量等放入堆中。
元空间
Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?
- 由于 PermGen 内存经常会溢出,引发恼人的
java.lang.OutOfMemoryError: PermGen
,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM - 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用,会将符号引用翻译为直接引用(地址)然后保存)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
因为运行时常量池是方法区的一部分,所以该常量池无法申请到内存时会OOM。
直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
大小不受 Java 堆大小的限制,但是会收到本机总内存大小以及处理器寻址空间的限制。
案例
package org.louis.jvm.codeset;
/**
* JVM 原理简单用例
* @author louis
*
*/
public class Bootstrap {
public static void main(String[] args) {
String name = "Louis";
greeting(name);
}
public static void greeting(String name)
{
System.out.println("Hello,"+name);
}
}