系统记录jvm相关知识点,形成系统性知识结构。
[toc]
jvm是什么
Java虚拟机
(英语:Java Virtual Machine
,缩写为JVM),一种能够运行Java bytecode的虚拟机,以堆栈结构机器来进行实做。
Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
我的理解: jvm其实就是一个软件,运行于操作系统之上
,当我们输入信息(字节码文件)时,它就帮我们解释成当前操作系统能理解的机器指令,由机器执行完,输出结果。这就是java程序执行的过程。
jvm的组成
java虚拟机主要有四部分组成:类装载子系统(ClassLoader)
、运行时数据区
、执行引擎
、垃圾收集
.
其中我们最为关注的运行时数据区
,也就是JVM的内存部分则是由方法区(Method Area)
、JAVA堆(Java Heap)
、虚拟机栈(JVM Stack)
、程序计数器
、本地方法栈(Native Method Stack)
这几部分组成。
先看张详细的总图,各部分之间的联系,这张图是结合下面这段简单代码来绘制的jvm工作流程。
当然,想要更进一步的理解jvm工作流程,也要学会理解分析jvm指令码(javap生成的),有必要的话,之后会总结一片分析指令码的文章(//todo)
现在也可以自己使用javap命令
生成下,然后对照jvm字节码指令表分析一下下面程序。
class Math {
public static final Integer CONSTANT = 666;
public static void main(String[] args) {
Math math = new Math();
int c = math.compute();
System.out.println(c);
Math math2 = new Math();
int c2 = math2.compute();
System.out.println(c2);
}
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
}
下面分开理解jvm各个部分。
程序计数器(Program Counter Register)
也叫PC寄存器
,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 –《深入理解Java虚拟机》
特点:
- 线程私有内存,每条线程都要有一个独立的程序计数器
- 如果线程正在执行
Java
方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行native
方法,则计数器为空 - 唯一一个不会出现
OutOfMemoryError
的内存区域
虚拟机栈区(JVM Stack)
虚拟机栈
描述了Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧
,用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每一个方法从调用直至完成的过程,都对应一个栈帧从入栈到出栈的过程。 –《深入理解Java虚拟机》
特点:
- 线程私有内存,它的生命周期与线程相同(随线程而生,随线程而灭)
- 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError
异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError
异常。 –《深入理解Java虚拟机》
这部分结构最好结合代码来理解一下,比较抽象。
栈帧:执行main()
方法时,创建一个栈帧,压入栈,我们称其为main()-栈帧
吧,同样的,执行到compute()
, 会有一个compute()-栈帧
被压入栈。当compute()
执行完成,那么,就伴随着compute()-栈帧
出栈,main()
是同样的道理。这也就符合了栈先进后出(FILO)数据结构特点。
局部变量表:一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。比如本例中:a=1
, a=2
, math
, math2
, ……
操作数栈:当方法刚开始执行时,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。如:(a + b) * 10
, 即是先将a的值1,压入操作数栈,将b的值2压入栈,指令执行相加变为值3(其实是2,1出栈执行相加指令得到3,把3重新压入操作数栈),再把把数值10压入栈,同样的操作,出栈相乘,把相乘的值压入栈,最后,把数值赋给局部变量,存入局部变量表。通过这样的一系列操作看,操作数栈其实就是一个进行计算操作的临时中转存储区域。
动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)
。这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析
。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接
。我的理解:当执行到调用方法处时,jvm是是如何找到对应的方法代码的呢?其实就是在动态链接区域存储着方法对应jvm指令码的内存地址,方法执行时,通过这个内存地址,去方法区中找到方法对应的代码指令执行(理解可能不完全准确,比较抽象,最好结合jvm指令码文件自己分析一下)
解释一:
符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。
一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,需要知道其名字;符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里(.class 文件);名字是知道了,但是Java真正运行起来的时候,如何靠这个名字(符号引用)找到相应的类和方法。
需要解析成相应的直接引用,利用直接引用来准确地找到。
方法出口:两种方式退出该方法,正常完成出口
和异常完成出口
。方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
附加信息:虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧之中,例如和调试相关的信息,这部分信息完全取决于不同的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其他附加信息一起归为一类,称为栈帧信息。
具体的关于通过javap命令
生成的指令码的分析方法,可以参考:文章
本地方法栈(Native Method Stack)
本地方法栈(Native Method Stack)
与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务(也就是字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。 –《深入理解Java虚拟机》
特点:
- Java虚拟机规范对本地方法栈使用的语言、使用方法与数据结构并没有强制规定,因此可以由虚拟机自由实现。例如:HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一。
- 同虚拟机栈相同,Java虚拟机规范对这个区域也规定了两种异常情况
StackOverflowError
和OutOfMemoryError
异常。
Java堆(Java Heap)
Java堆
是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 –《深入理解Java虚拟机》
特点:
- 线程共享内存,所有线程共享的一块内存区域
- 所有的
对象实例以及数组
都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配
、标量替换优化
技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。参考文章 - Java堆是垃圾收集器管理的主要区域,也称为
GC堆
.从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代、老生代 - 如果在堆中没有内存完成实例分配,并且堆上也无法再扩展时,将会抛出OutOfMemoryError异常
//todo Java堆涉及到垃圾收集,会单独写文章来分析垃圾收集相关知识。
方法区(Method Area)
方法区(Method Area)
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息
、常量
、静态变量
、即时编译器编译后的代码
等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆 )
,目的应该是与Java堆区分开来。 –《深入理解Java虚拟机》
特点:
- 线程共享内存,各个线程共享的内存区域
- Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样 不需要连续的内存和可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。 这区域的内存回收目标主要是针对常量池的回收和类型的卸载,一般而言,这个区域的内存回收比较难以令人满意,尤其是类型的回收,条件相当苛刻,但是这部分区域的内存回收确实是必要的
- 当方法区无法满足内存分配需求时,将抛出
OutOfMemoryError
异常
永久代与元空间
这里区分两个概念, 有时会看到方法区被称为永久代
,其实两者有着本质的区别。方法区是 JVM 规范中的定义,而永久代是 JVM 规范的一种实现,并且只有在 HotSpot 虚拟机中如此,其他虚拟机中没有永久代的说法。
在 JDK1.6
之前,HotSpot 虚拟机把 GC 分代收集扩展至方法区,或者说使用永久代实现方法区。不过永久代有 -XX:MaxPermSize
的上限,很容易遇到内存溢出问题。
所以在 JDK1.7
中,将部分数据已经转移 Java Heap
或 Native Heap
中,例如:将原本放在永久代中的字符串池
和类的静态变量
移出到 Java Heap
中,将符号引用转移到 Native Heap
中。但永久代仍然存在,并没有移除。
在 JDK1.8
中,取消了永久代,代替为元空间
实现,它也是 JVM 规范中方法区的一种实现。不过它与永久代最大的不同是:元空间并不在虚拟机中,而是将元空间放到本地内存中。所以默认情况下,它只受本地内存的限制,可以通过 -XX:MetaspaceSize
参数设置初始空间大小,默认没有最大空间限制。
why 取消永久代
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
运行时常量池(Runtime Constant Pool)
运行时常量池(Runtime Constant Pool)
是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。 –《深入理解Java虚拟机》
特点:
动态性
是运行时常量池相对于 Class 文件常量池的一个重要特征,即不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如String类的intern()
方法- 运行时常量池受到方法区内存的限制,如果常量池无法再申请内存,就会抛出
OutOfMemoryError
异常。
直接内存
直接内存
不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域, 是利用 Native
函数库在 Java 堆外申请分配的内存区域.
比如 NIO
中的 DirectByteBuffer
就可以作为这块内存的引用进行操作直接内存.
特点:
- 避免在
Java 堆
和Native 堆
中复制数据以提高性能 - 这一部分内存也被频繁使用,而且也有可能导致
OutOfMemoryError
异常出现 - 内存的分配不受Java堆大小的限制,但是他还是会收到服务器总内存的影响