深入理解Java虚拟机
JVM(Java Virtual Machine,Java 虚拟机)顾名思义就是用来执行 Java 程序的“虚拟主机”,实际的工作是将编译生成的.class 文件(字节码)翻译成底层操作系统可以运行的机器码并且进行调用执行,这也是 Java 程序能够跨平台(“一次编写,到处运行”)的原因(因为它会根据特定的操作系统生成对应的操作指令),Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持
前言
JVM是什么?
JVM(Java Virtual Machine,Java 虚拟机)顾名思义就是用来执行 Java 程序的“虚拟主机”,实际的工作是将编译生成的.class 文件(字节码)翻译成底层操作系统可以运行的机器码并且进行调用执行
,这也是 Java 程序能够跨平台(“一次编写,到处运行”)的原因(因为它会根据特定的操作系统生成对应的操作指令),Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
Java程序的执行过程
一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到方法区,执行引擎将会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译存在,输入字节码,调用操作系统函数。
过程如下:Java 文件->编译器>字节码->JVM->机器码。
JVM、JRE、JDK的关系
JVM只是一个翻译,把Class翻译成机器识别的代码,但是需要注意,JVM 不会自己生成代码,需要大家编写代码,同时需要很多依赖类库,这个时候就需要用到JRE。
JRE是什么,它除了包含JVM之外,提供了很多的类库(就是我们说的jar包,它可以提供一些即插即用的功能,比如读取或者操作文件,连接网络,使用I/O等等之类的)这些东西就是JRE提供的基础类库。JVM 标准加上实现的一大堆基础类库,就组成了 Java 的运行时环境,也就是我们常说的 JRE(Java Runtime Environment)。
但对于程序员来说,JRE还不够。我写完要编译代码,还需要调试代码,还需要打包代码、有时候还需要反编译代码。所以我们会使用JDK,因为JDK还提供了一些非常好用的小工具,比如 javac(编译代码)、java、jar (打包代码)、javap(反编译<反汇编>)等。这个就是JDK。
JVM的跨平台
JVM的内存区域
运行时数据区域
定义:
Java虚拟机在执行Java程序的过程中会把它所管理的内存
划分为若干个不同的数据区域。
线程私有区域:
一个线程拥有单独的一份内存区域;
线程共享区域:
被所有线程共享,且只有一份;
直接内存:
可以理解成没有被虚拟化的操作系统上的其他内存;
虚拟机栈
定义:
虚拟机栈是用来存放线程运行Java方法所需的数据、指令、返回地址。
结构图
![虚拟机栈整体图](https://img-blog.csdnimg.cn/33e78d3f910d4212a9c4787daef042af.png
在实际开发中,一个线程是可以运行多个方法的。
public class MethodStack {
public static void main(String[] args) {
A();
}
private static void A() {
B();
}
private static void B() {
C();
}
private static void C() {
}
}
这里从main
方法开始运行,打包一个栈帧
送入虚拟机栈中;
C方法运行结束,C方法出栈,然后是B,A,最后main方法运行结束,main方法这个栈帧就出栈了。
这个就是Java方法运行对虚拟机栈的一个影响,虚拟机栈就是用来存储线程运行方法中的数据的,而每一个方法对应一个栈帧。
栈帧
:在每个Java方法被调用的时候,都会创建一个栈帧并入栈,一旦方法完成调用,则出栈。
栈帧大体包含四个区域:
- 局部变量表
顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)
。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。 - 操作数栈
存放java方法执行的操作数的
,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。
操作数栈本质上是JVM执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。 - 动态连接
Java语言特性多态 - 返回地址
正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)
栈的数据结构:
先进后出(FILO)
虚拟机栈是基于线程的:
即使你只有一个main方法,也是以线程的方式运行的,在线程的生命周期中,参与计算的数据会频繁的入栈和出栈,栈的生命周期是和线程一致的。
虚拟机栈的大小默认为1M,可用参数-Xss调整大小,例如-Xss256k
运行时数据区中的其他区域
本地方法栈
本地方法栈是为JVM使用到本地【native】方法服务;
直接内存
NIO
中的DirectByteBuffer
方法区
永久代与元空间运行时常量池
堆
堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的
JVM的整体内存结构
直接内存
直接内存有一种更加科学的叫法,堆外内存。
JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。
JVM内存处理流程
- JVM申请内存
- 初始化运行时数据区
- 类加载
- 执行方法
- 创建对象
一句话总结:
JVM在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。
方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的。
内存溢出
栈溢出
java.lang.StackOverflowError
一般的方法调用是很难出现的,如果出现了可能会是无限递归。OutOfMemoryError
不断建立线程,JVM申请栈内存,机器没有足够的内存。
堆溢出
内存溢出:申请内存空间,超出最大堆内存空间。
如果是内存溢出,则通过 调大 -Xms,-Xmx参数。
如果不是内存泄漏,就是说内存中的对象却是都是必须存活的,那么久应该检查JVM的堆参数设置,与机器的内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。
方法区溢出
(1) 运行时常量池溢出
(2)方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。
本机直接内存溢出
直接内存的容量可以通过MaxDirectMemorySize
来设置(默认与堆内存最大值一样),所以也会出现OOM异常;由直接内存导致的内存溢出,一个比较明显的特征是在HeapDump文件中不会看见有什么明显的异常情况,如果发生了OOM,同时Dump文件很小,可以考虑重点排查下直接内存方面的原因。
更多推荐
所有评论(0)