JVM(java虚拟机)原理

本文详细介绍了Java虚拟机(JVM)的基本概念,包括JVM如何实现跨平台运行、JRE与JDK的区别,以及JVM的内部结构如栈、堆、程序计数器等。同时还讲解了JVM执行过程及垃圾回收机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自:
https://blog.youkuaiyun.com/sinat_35512245/article/details/54744815
https://segmentfault.com/a/1190000014836320

什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

JRE/JDK/JVM是什么关系?

JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。

JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。

JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

JVM原理

Java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
在这里插入图片描述

JVM体系结构

在这里插入图片描述

(1)java栈内存

栈的内存地址是不连续的, 每个线程都拥有自己的栈。 栈里面存储着的是StackFrame,在《JVM Specification》中文版中被译作java虚拟机框架,也叫做栈帧。

栈帧主要包含:

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 返回地址

StackFrame包含三类信息:局部变量,执行环境,操作数栈。

局部变量用来存储一个类的方法中所用到的局部变量。

执行环境用于保存解析器对于java字节码进行解释过程中需要的信息,包括:上次调用的方法、局部变量指针和 操作数栈的栈顶和栈底指针。

操作数栈用于存储运算所需要的操作数和结果。

返回地址一个方法开始执行后只有两种方式可以退出,方法返回指令,异常退出。方法退出时总是要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来恢复它的上层方法的执行状态。

动态链接(Dynamic Linking): 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。其实就是在此方法中调用其它的方法时,因为多态,方法是动态绑定的,因此指向方法的引用是动态的,动态链接就是这个方法的引用。
在这里插入图片描述

StackFrame在方法被调用时创建,在某个线程中,某个时间点上,只有一个 框架是活跃的,该框架被称为Current Frame,而框架中的方法被称为Current Method,其中定义的类为Current Class。局部变量和操作数栈上的操作总是引用当前框架。当Stack Frame中方法被执行完之后,或者调用别的StackFrame中的方法时,则当前栈变为另外一个StackFrame。Stack的大小是由两种类 型,固定和动态的,动态类型的栈可以按照线程的需要分配。 下面两张图是关于栈之间关系以及栈和非堆内存的关系基本描述(来自http://www.programering.com/a/MzM3QzNwATA.html ):
在这里插入图片描述

(2) Java堆
Java堆是用来存放对象信息的,和Stack不同,Stack代表着一种运行时的状态。换句话说,栈是运行时单位,解决程序该如何执行的问题,而堆是存储的单位, 解决数据存储的问题。Heap是伴随着JVM的启动而创建,负责存储所有对象实例和数组的。堆的存储空间和栈一样是不需要连续的。
注意,基本类型、对象引用是放在堆中,真正的对象是放在堆中。

(3)程序计数寄存器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

(4)方法区域(Method Area)
主要有三个:

  1. 类型信息,也就是.class对象
  2. 静态变量
  3. final常量

在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

  1. jdk1.6以及jdk1.7时,都是在永久代中
  2. jdk1.8后就没有永久代,改成了“元空间”,方法区也在这里面

既然静态变量是放在方法区,那静态的集合类放在哪里呢?

静态集合类的引用是放在方法区,但是静态集合的类是放在堆的老年代中。

(5)运行时常量池(Runtime Constant Pool)

用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法

其空间分配情况如下:

  1. jdk1.6版本之前,是放在方法区中
  2. jdk1.7和1.8,是在堆中开辟一部分空间给它,就是放在堆中

这里存的到底是啥呢?以String s1=new String(“abc”); 为例。

如果你用了String s1 = new String(“abc”);

那么,会有两个String被创建,一个是你的Class被CLassLoader加载时,你的"abc"被作为常量读入,在constant pool里创建了一个共享的"abc"

然后,当调用到new String(“abc”)的时候,会在heap里创建这个new String(“abc”);

考虑类加载阶段和实际执行时。

(1)类加载对一个类只会进行一次。"abc"在类加载时就已经创建并驻留了(如果该类被加载之前已经有"abc"字符串被驻留过则不需要重复创建用于驻留的实例)。驻留的字符串是放在全局共享的字符串常量池中的。

(2)在这段代码后续被运行的时候,"abc"字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1持有。

这条语句创建了2个对象。

(6)本地方法堆栈(Native Method Stacks)
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

(7)堆外内存

堆外内存就是指非堆内的内存,包括 元空间 Metaspace、GC用的数据(例如ZGC用的map等)、直接分配的内存等:

  1. 直接内存(Direct Memory):通过 malloc 直接向操作系统申请的内存,用于NIO的DirectByteBuffer、Netty等网络框架的零拷贝技术。
  2. JVM自身使用的本地内存: 元空间 Metaspace。
  3. JVM自身使用的本地内存: ZGC/Shenandoah的标记表等。
  4. 本地库(如OpenCV、TensorFlow JNI)直接分配的内存。
  5. 第三方库分配的堆外内存。

JVM执行过程

Java的整个过程如下:
① Java源文件—->编译器—->字节码文件
② 字节码文件—->JVM—->机器码

JVM执行的过程就是其中的 字节码文件—->JVM:

1、类装载器将.class文件装入JVM虚拟机,通过JVM的解释器,将.class中的方法、静态变量、final常量、Field信息等放入方法区域(Method Area)。运行时常量池(Runtime Constant Pool)也在方法区内。方法区是共享的。
(静态变量+常量+类信息+运行时常量池存在方法区中,实例变量存在堆内存中。)

2、程序初始化
实例化对象,并将对象信息放入堆中,堆是共享的。
(方法区和堆一样,是各个线程共享的内存区域)
(JVM规范将方法区描述为堆的一个逻辑部分)

3、程序运行,线程开始,每个线程创建自己的栈。每调用一个方法,便产生一个与之对应的栈帧(F1),并将该栈帧(F1)压入栈中。如果该方法中又调用了另一个方法,那么再产生一个栈帧F2,并将F2压入栈中。然后,先弹出F2,再弹出F1。

参考:
https://segmentfault.com/a/1190000014836320
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进……F3栈帧,再弹出F2栈帧,再弹出F1栈帧。

此外,JVM还有比较重要的GC机制(垃圾回收)。
详情见:
https://blog.youkuaiyun.com/weixin_43751710/article/details/90896056

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值