概述
这部分是JVM中最为重要的部分。我们研究JVM主要的目的就是为了对JVM进行调优,本质上就是针对运行时数据区进行调优。
所谓运行时数据区,其实就是JVM对内存的使用
运行时数据区大致分为:
- 线程共享区域
- 方法区
- 堆
- 线程独享区域
- 栈
- 程序计数器
- 本地方法栈
示意图大致如下:
线程共享空间就是所有线程共同使用的内存空间,而独享空间,则是创建线程才有的空间,且每一个线程都有自己的这么一块空间。
方法区
方法区主要存放以下内容:
存储内容 | 说明 |
---|---|
类型信息 | Class信息 |
方法信息 | 如Method,含方法名称,参数列表,方法返回值等 |
字段信息 | 如Field,含字段类型,名称 |
Code区 | 存储方法执行对应的字节码指令 |
方法表 | 例如调用A类的某个method时,是根据A类的方法表找到对应的方法 |
静态变量 | JDK1.7之后,移到堆中 |
运行时常量池 (字符串常量池最为重要) |
从class文件中的常量池加载,JDK1.7之后移到堆中。 参考精通JVM(一):class文件详解 |
JIT编译器编译之后的代码缓存 | JIT编译器将字节码编译成系统能够识别的机器码,为了效率进行了缓存 |
永久代与元空间
方法区其实只是一个统称,或者说是一种抽象,一种规范。而永久代和元空间则是方法区的具体实现
在JDK1.8之前使用的是永久代,JDK1.8开始使用的是元空间
两者之间的区别如下:
存储位置不同:
- 永久代占用的是JVM进程所使用的内存空间,大小受JVM空间限制。
- 元空间使用的是物理内存区域,只受机器物理内存大小限制
存储内容不同:
- 永久代存储如上面的表格所示的内容。
- 元空间只存储类的元信息。静态变量和运行时常量池都已经挪到了堆中
永久代和元空间的历史变成过程大致如下:
方法区OOM异常
方法区能够导致OOM异常的情况主要两种:
- 加载太多的类,超出方法区空间大小。
- JDK1.8之前将抛出
java.lang.OutOfMemoryError: PerGem space
- JDK1.8之后将抛出
java.lang.OutOfMemoryError: Metaspace
- JDK1.8之前将抛出
- 字符串OOM异常:当我们创建太多的字符串时,将发生OOM。
- JDK1.6及之前版本,字符串常量还在永久代中,因此异常为
java.lang.OutOfMemoryError: PerGem space
- JDK1.7开始,字符串常量池在堆中,因此异常为
java.lang.OutOfMemoryError:Java heap space
- JDK1.6及之前版本,字符串常量还在永久代中,因此异常为
堆内存
Java堆是Java应用中最大的一块内存,几乎所有的对象都在堆上,例如我们new出来的对象,字符串常量等等。
堆内存是由垃圾收集器分配和管理,因此也叫做GC堆。
堆内存的分配与垃圾收集器息息相关,并不是固定不变的,不同的垃圾收集器堆内存可能是不一样的。
最为经典的是分代设计:主要划分为年轻代(Young)和老年代(Old),二者之间默认大小比例为1:2。
其中年轻代又具体划分为Eden,Survivor From, Survivor To三个区域,三者之间默认大小比例为8:1:1。
具体示意图如下:
由于不同的垃圾收集器的堆内存设计是不一样的,因此想要深入理解堆内存,建议参考垃圾收集器部分
程序计数器
程序计数器是线程私有的,也就是说每一次线程都会有一个程序计数器,它的作用是用来记录当前线程下一个要执行的字节码的行号。
比如有这样一段代码:
int a = 1;
int b = 3;
int c = a + b;
字节码指令如下:(使用IDEA插件jclasslib bytecode viewer查看)
左侧红色数字就是字节码的行号。
正是由于程序计数器记录了行号,当线程发生切换时才知道应该从哪里继续执行。
if,while等控制流语句的完成也依赖程序计数器来完成
如果正在执行的方法是native方法,那么程序计数器记录的值为空。
虚拟机栈
Java中方法是虚拟机执行的最基本单元,栈帧则是对应了一个方法。
当某一个方法A要执行时,对应的栈帧将入栈,如果方法A中又调用了方法B,那么执行到方法B时,B对应的栈帧也将入栈,位于栈顶。
也就是说栈顶的那一个栈帧就是当前正在执行的方法,因此称之为“当前栈帧”
一个栈帧包含以下内容:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
局部变量表
局部变量表用于存放当前方法的局部变量,如方法入参,方法内部定义的局部变量等,可以保存boolean,byte,char,short,int,reference,returnAddress
类型的数据。
其中reference是对一个对象实例的引用,returnAddress比较少见,它指向了一条字节码指令。
局部变量表中变量顺序为:
- this引用
- 方法参数
- 方法内声明的局部变量
在局部变量表中,存储变量是以变量槽(slot)为单位,像boolean,int这种占用四个字节的变量都占用一个slot,而double/long这种8字节的变量需要两个slot
操作数栈
操作数栈的基本数据结构是一个先进先出、后进后出的栈结构,它是JVM用于计算用的。
比如有一个方法如下:
public int add(int a, int b) {
return a + b;
}
查看该方法的字节码如下:
其中iload
指令就是将加载int类型的变量到操作数栈中,查看JVM规范文档)可以证实这一点:
将a和b变量iload入栈之后,然后iadd
指令会将栈顶的两个变量出栈并相加,然后把结果入栈,ireturn
从操作数栈的栈顶元素出栈用作返回结果。
这就是操作数栈的作用,不过操作数栈可不仅仅是计算基本数据类型,操作数栈中的元素可以是任意Java数据类型,一个32位的数据栈1个栈容量,64位的数据栈2个容量
动态链接
动态链接就是将符号引用转换为内存地址中的直接引用,这个过程有一部份是在类加载阶段或第一次使用的时候执行(参考类加载过程中的解析阶段)。比如静态方法和私有方法,这两者在编译期间就已经确定下来,因此适合在类加载阶段就进行连接。
而另一部分就是在运行期间执行,也就是这里说到的动态链接,比如public方法,protect方法,可能被子类继承重写,在类加载阶段无法确定到底要调用哪个版本的方法,因此适合在运行期间再进行情况进行动态链接
方法返回地址
一个方法执行结果无非就两种:
- 正常返回(return)
- 异常返回(throw new exception)
无法哪种方法返回,方法返回之后都需要回到方法调用的地方,程序才能继续往后执行,因此方法返回时需要在栈帧中保存一些信息,用于帮助调用者恢复状态继续向后执行
为什么这里用到了“可能”这个模糊的字眼呢?因为JVM仅仅是提供了一个规范,具体的实现还得依靠各个JVM厂商,不同的实现方法,不同的退出方式,可能都会有不一样的过程
本地方法栈
本地方法就是native关键字修饰的方法,它是由C/C++实现的。Java方法执行的时候需要栈,同样的C++方法执行的时候也是需要栈的。
本地方法栈就是native方法执行时所用的栈空间