JVM 是 字节码的运行环境,负责装载class到JVM内部,解释编译为对应平台的机器码指令进行执行,对于JVM设计有权威的定义规范,了解 JVM 类加载各部的主要功能 和 运行时数据区域组成 很有意义。
磁盘上有一个.java文件,通过编译器的javac 命令又变成了一个平台无关的.class 文件,相信大家都看过,其本质就是一个本地机器码变成字节码的过程。当变成.class File之后的Java技术体系,就即将展开一个关于JVM运行的新世界了。
首先,就要聊到JVM的类加载机制了。
- 加载loading过程:将类的文件,网络等其他方式生成的字节流 注入JVM内存中,同时将Klass 类模板信息 放到方法区中,并创建一个指向 该Klass 的 java.lang.Class对象放到堆中。
- 链接Linking:
- Verification 校验阶段 : 校验当前由class文件变成的字节流是否正确的符合规范,一般有格式验证(这一步是配合加载阶段完成的)、语义验证、符号引用验证等 。
- Preparation 准备阶段: 为属于类域的静态变量分配存储空间并赋一个初始零值 。基本类型设置对应初始值,引用类型设置null,基本类型常量跟字符串字面量(final static修饰)设置为具体的值。
- Resolution 解析阶段:把类中的符号引用转换为直接引用,如果不理解符号引用跟直接引用可以参考知乎文章
- Initialization 初始化,调用类构造< clinit >()方法,对类变量及静态代码块进行赋值。
program counter register 程序计数器寄存器
内存较小,它是当前线程所执行的class文件上的字节码byte code 行号的指示器。在JVM设计的概念模型,字节码解释器在工作的时候,需要改变这个计数器的值来选取下一条需要执行的字节码指令,Java的 if分支,forwhile循环,break跳转,trycatch异常,线程切换 等程序控制流的基础功能实现都需要这个计数器来实现。每一个线程都会有一个program counter register ,否则它就无法恢复到它能正确执行的位置。场景举例:你开辟了99个线程来帮你办事,就要99个程序计数器。各个线程间的计数器互不影响,独立存储,这类内存区域就是线程私有的内存。
JVM Stack 虚拟机栈
是程序执行字节码即运行方法时必须的一片空间,也是线程私有,即 99个线程进入这个方法时,每个方法都会来一片这样的内存空间。在进入一个方法前,需要多少槽数的局部变量表(基础数据类型+引用指针+线程切换恢复的地址位置returnAddress)是确定的(64位的数据 long,double 需要2个变量槽,其他1个槽就够了)。出现栈深度不够会抛出StackOverflowError和栈扩展失败会抛出 OOM OutOfMemeryError。
program counter register 中的值 就是当前指令所在的内存地址,这类内存地址,returnAddress就是线程切换后的能恢复的地址位置。
Native Method Stacks 本地方法栈
JVM使用本地方法时需要的空间,也是线程私有的。和JVMStack一样,出现栈深度不够会抛出StackOverflowError和栈扩展失败会抛出 OOM OutOfMemeryError。
Java Heap 堆
在JVM设计规范中,所有的对象和数组都应当在堆上分配空间,所以这块内存是JVM最大的内存空间。属于所有线程共有的内存区域,因为大家都会使用这个操场,那么这个区域的内存管理就GC机制尤为重要,也是GC大显身手的地方。
当前Java堆大部分都被设计为大小可扩展的,可通过 -Xmx 和 -Xms 来设定 最大或最小范围。如果堆内存空间不足满足实例分配就会出现OOM。
Method Area 方法区
各线程共享,它用于存储被JVM加载的class信息,常量,静态变量,JIT compile 之后的 code cache 代码缓存数据。《Java JVM规范》把Method Area 看作为 Heap的一个逻辑部分,所以Method Area 内存分配不足时也会抛出OOM异常。
Method Area 中有一部分叫做 Runtime Constant Pool 运行时常量池 , Class文件 中除了有类版本信息,字段,方法,接口 的描述外,还有一个存放编译期间生成的各种字面量和符号引用,这部分内容 在class load 后,存放在Method Area 的 运行时常量池中。
Direct Memory 直接内存
- Direct Memory 并不是虚拟机运行时数据区的一部分;
- 由于在 JDK 1.4 中引入了 NIO 机制,为此实现了一种通过 native 函数直接分配对外内存的,而这一切是通过以下两个概念实现的:
- 通道(Channel);
- 缓冲区(Buffer);
- 通过存储在 Java 堆里面的 DirectByteBuffer 对象对这块内存的引用进行操作;
- 因避免了 Java 堆和 Native 堆(native heap)中来回复制数据,所以在一些场景中显著提高了性能;
- 直接内存出现 OutOfMemoryError 异常的原因是物理机器的内存是受限的,但是我们通常会忘记需要为直接内存在物理机中预留相关内存空间;