JVM工作原理详解 - 极悦
首页 课程 师资 教程 报名

JVM工作原理详解

  • 2022-05-30 11:23:42
  • 1528次 极悦

作为Java用户,掌握JVM架构也是很有必要的。说起Java,人们首先想到的就是Java编程语言。然而,实际上,Java 是一种技术,它由四个方面组成:Java 编程语言、Java 类文件格式、Java 虚拟机和 Java 应用程序编程接口(Java API)。它们的关系如下图所示:

运行时环境代表 Java 平台。开发人员编写 Java 代码(.java 文件),然后将其编译成字节码(.class 文件),然后将字节码加载到内存中。字节码一旦进入虚拟机,就会被解释器解释执行,或者有选择地转换成即时代码生成器执行的机器码。

Java平台由Java虚拟机和Java应用程序接口构建而成。Java语言是进入这个平台的渠道。用 Java 语言编写和编译的程序可以在这个平台上运行。该平台的结构如下图所示:

在Java平台的结构中,可以看出以Java虚拟机(JVM)为核心,是程序独立于底层操作系统和硬件的关键。下面是移植界面。移植接口由适配器和Java操作系统两部分组成,依赖于平台的部分称为适配器;JVM通过移植接口在特定平台和操作系统上实现;以上JVM是Java的基础类库和扩展类库以及它们的API,使用Java API编写的应用程序(application)和applet(Java小程序)可以运行在任何Java平台上,无需考虑底层平台,因为有一个Java虚拟machine(JVM)实现了程序和操作系统的分离,

JVM在其生命周期中有一个明确的任务,就是运行Java程序。因此,当一个Java程序启动时,会生成一个JVM实例;当程序结束时,实例也会消失。下面我们将从JVM架构及其运行过程两个方面对其进行更深入的研究。

Java虚拟机的架构

1.每个JVM都有两种机制:

类加载子系统:加载合适名称的类或接口

执行引擎:负责执行加载的类或接口中包含的指令

2.每个 JVM 包含:

方法区、Java堆、Java栈、本地方法栈、指令计数器等隐藏寄存器

对于 JVM 的研究,在我看来,这些部分是最重要的:

Java代码编译执行的全过程

JVM内存管理和垃圾回收机制

这些部分描述如下:

Java代码编译执行的全过程

前面说过,编译和执行Java代码的整个过程大概是:开发者编写Java代码(.java文件),然后编译成字节码(.class文件),然后字节码被加载,一旦字节码进入虚拟机器,它将由解释器解释和执行,或者将选择性地转换为机器代码以供即时代码生成器执行。

1.Java代码编译由Java源代码编译器完成,即从Java代码到JVM字节码(.class文件)的过程。流程图如下:

2.Java字节码的执行由JVM执行引擎完成。流程图如下:

Java代码编译和执行的整个过程包括以下三个重要机制:

Java源码编译机制

类加载机制

类执行机制

(1)Java源码编译机制

Java源代码编译包括以下三个过程:

分析并输入符号表

注释处理

语义分析和类文件生成

流程图如下:

最终生成的类文件由以下部分组成:

结构信息:包括class文件格式的版本号和各部分的数量和大小

元数据:Java源代码中声明和常量对应的信息。包含类/继承超类/实现接口的声明信息、域和方法声明信息和常量池

方法信息:对应Java源码中的语句和表达式的信息。包括字节码、异常处理表、评估栈和局部变量区大小、评估栈类型记录、调试符号信息

(2)类加载机制JVM类加载是通过ClassLoader及其子类完成的。类的层次关系和加载顺序可以用下图来描述:

1)引导类加载器

负责加载$JAVA_HOME中jre/lib/rt.jar中的所有类,由C++实现,不是ClassLoader子类

2)扩展类加载器

负责加载java平台扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App类加载器

负责记录classpath中指定的jar包和目录下的class

4)自定义类加载器

它属于应用程序根据自己的需要自定义的ClassLoader,比如tomcat和jboss会根据j2ee规范来实现ClassLoader。

在加载过程中,首先会检查类是否已经加载完毕。检查的顺序是从下到上,从Custom ClassLoader到BootStrap ClassLoader逐层检查。只要加载了某个类加载器,就认为它已加载。这个类保证被所有的 ClassLoader 加载。一次。加载的顺序是从上到下,即上层尝试逐层加载这个类。

(3)类执行机制

JVM 是基于堆栈的虚拟机。JVM 为每个新创建的线程分配一个堆栈。也就是说,对于一个Java程序来说,它的操作是通过对栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM只对栈进行两种操作:以帧为单位的push和pop操作。

JVM 执行类字节码。创建线程后,会生成程序计数器(PC)和堆栈(Stack)。程序计数器存储方法中要执行的下一条指令的偏移量。栈中存放一个栈帧,每个栈帧对应每个方法的每次调用,栈帧由局部变量区和操作数栈两部分组成。局部变量区用于存放方法中的局部变量和参数,操作数栈用于存放方法执行过程中产生的中间结果。栈的结构如下图所示:

JVM内存管理和垃圾回收机制

JVM内存结构分为:方法区(method)、栈内存(stack)、堆内存(heap)、本地方法栈(java中的jni调用),结构图如下:

1.堆内存(heap)

new创建的所有对象的内存都是在堆中分配的,其大小可以通过-Xmx和-Xms来控制。操作系统有一个记录空闲内存地址的链表。当系统收到该程序的申请时,会遍历链表,找到第一个空间大于请求空间的堆节点,然后将该节点从空闲节点列表中删除,删除并分配该节点的空间到程序。另外,对于大部分系统来说,这个分​​配的大小会记录在这个内存空间的首地址,这样代码中的delete语句才能正确的释放内存空间。但是,由于找到的堆节点的大小可能不完全等于应用程序的大小,系统会自动将多余的部分放入空闲列表中。这时候new分配的内存一般比较慢,容易出现内存碎片,但它是最方便使用的。另外,在WINDOWS下,最好的办法是使用VirtualAlloc来分配内存。它不在堆上,也不在栈上,而是直接在进程的地址空间中预留了一块内存。这种方法虽然用起来最不方便,但速度很快,也最灵活。堆内存是一种扩展到高地址的数据结构,是一个不连续的内存区域。由于系统使用链表来存储空闲内存地址,自然是不连续的,链表的遍历方向是从低地址到高地址。堆的大小受计算机系统中有效虚拟内存的限制。可以看出,堆获得的空间更加灵活,更大。最好的方法是使用 VirtualAlloc 来分配内存。它不在堆上,也不在栈上,而是直接在进程的地址空间中预留了一块内存。这种方法虽然用起来最不方便,但速度很快,也最灵活。堆内存是一种扩展到高地址的数据结构,是一个不连续的内存区域。由于系统使用链表来存储空闲内存地址,自然是不连续的,链表的遍历方向是从低地址到高地址。堆的大小受计算机系统中有效虚拟内存的限制。可以看出,堆获得的空间更加灵活,更大。最好的方法是使用 VirtualAlloc 来分配内存。它不在堆上,也不在栈上,而是直接在进程的地址空间中预留了一块内存。这种方法虽然用起来最不方便,但速度很快,也最灵活。堆内存是一种扩展到高地址的数据结构,是一个不连续的内存区域。由于系统使用链表来存储空闲内存地址,自然是不连续的,链表的遍历方向是从低地址到高地址。堆的大小受计算机系统中有效虚拟内存的限制。可以看出,堆获得的空间更加灵活,更大。这种方法虽然用起来最不方便,但速度很快,也最灵活。堆内存是一种扩展到高地址的数据结构,是一个不连续的内存区域。由于系统使用链表来存储空闲内存地址,自然是不连续的,链表的遍历方向是从低地址到高地址。堆的大小受计算机系统中有效虚拟内存的限制。可以看出,堆获得的空间更加灵活,更大。这种方法虽然用起来最不方便,但速度很快,也最灵活。堆内存是一种扩展到高地址的数据结构,是一个不连续的内存区域。由于系统使用链表来存储空闲内存地址,自然是不连续的,链表的遍历方向是从低地址到高地址。堆的大小受计算机系统中有效虚拟内存的限制。可以看出,堆获得的空间更加灵活,更大。链表的遍历方向是从低地址到高地址。堆的大小受计算机系统中有效虚拟内存的限制。可以看出,堆获得的空间更加灵活,更大。链表的遍历方向是从低地址到高地址。堆的大小受计算机系统中有效虚拟内存的限制。可以看出,堆获得的空间更加灵活,更大。

(2)栈内存(stack)

在Windows下,栈是向低地址扩展的数据结构,是一个连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先定义好的。在 WINDOWS 下,堆栈的大小是固定的(在编译时确定的常数)。如果请求的空间超过堆栈大小,当有空闲空间时,会提示溢出。因此,堆栈中可用的空间更小。只要堆栈的剩余空间大于请求的空间,系统就会为程序提供内存,否则会报异常,表示堆栈溢出。它由系统自动分配,速度更快。但是程序员无法控制它。

堆内存和栈内存需要说明一下:

基本数据类型直接分配在栈空间中,方法的形参直接分配在栈空间中,在方法调用完成时从栈空间中回收。引用数据类型需要用new创建,不仅在栈空间分配地址空间,还在堆空间分配对象的类变量。方法的引用参数在栈空间中分配一个地址空间,指向堆空间的对象区域,在方法调用完成时从栈空间中回收。当局部变量new出来时,在栈空间和堆空间中分配空间。当局部变量的生命周期结束时,栈空间立即被回收,堆空间区域等待GC回收。调用方法时传入的字面量参数首先分配在栈空间中,调用方法后从栈空间中回收。字符串常量和静态分配在 DATA 区域中,而这分配在堆空间中。数组不仅分配了栈空间中的数组名,还分配了堆空间中数组的实际大小。

如:

(3) 本地方法栈(java中的jni调用)

用于支持native方法的执行,并存储每个native方法调用的状态。对于native方法接口,JVM的实现不需要它的支持,甚至根本不需要。Sun 的 Java 本机接口 (JNI) 实现是出于可移植性考虑。当然,我们也可以设计其他的原生接口来代替 Sun 的 JNI。然而,这些设计和实现是更复杂的事情。有必要确保垃圾收集器不会释放被本地方法调用的对象。

(4) 方法区(method)

它保存方法代码(编译的java代码)和符号表。存储要加载的类信息、静态变量、最终类型常量、属性和方法信息。JVM使用Permanet Generation来存储方法区,可以通过-XX:PermSize和-XX:MaxPermSize指定最小值和最大值。

垃圾回收机制

应用程序创建的所有对象都聚集在堆中。JVM也有相应的指令如new、newarray、anewarray和multianewarray。但是,并没有释放空间给C++ delete、free等的指令。Java中所有的释放都是由GC完成的。GC除了回收内存,还进行内存压缩。这在其他语言中也类似地实现。与C++相比,它不仅易于使用,而且增加了安全性。当然,它也有缺点,比如性能。这个大问题。

Java虚拟机运行过程示例

上面对虚拟机的各个部分进行了比较详细的解释,并通过一个具体的例子来分析它的运行过程。

通过调用指定类的main方法启动虚拟机,将字符串数组参数传递给main,从而加载指定的类,同时链接该类使用的其他类型,并对其进行初始化。例如程序:

编译后在命令行模式下输入:java HelloApp run virtual machine

java虚拟机将通过调用HelloApp方法main来启动,一个包含三个字符串“run”、“virtual”和“machine”的数组将被传递给main。现在我们概述虚拟机在执行 HelloApp 时可能采取的步骤。

开始尝试执行HelloApp类的main方法,发现类没有加载,说明虚拟机当前不包含该类的二进制代表,所以虚拟机使用ClassLoader尝试找到这样的二元代表。如果此过程失败,则会引发异常。类加载完成后,main方法调用前,HelloApp类必须与其他类型链接,然后初始化。该环节包括三个阶段:检查、准备和分析。检查加载的主类的符号和语义,准备创建类或接口的静态字段并将这些字段初始化为标准默认值。解析负责检查主类对其他类或接口的符号引用。在这一步中,它是可选的。类的初始化是静态初始化函数的执行和类中声明的静态域的初始化构造方法。一个类必须在其父类被初始化之前被初始化。整个过程如下:

以上就是关于“JVM工作原理详解”的介绍,大家如果想了解更多相关知识,不妨来关注一下极悦的Java极悦在线学习,里面的课程内容细致全面,从入门到精通,很适合没有基础的小伙伴学习,希望对大家能够有所帮助哦。

选你想看

你适合学Java吗?4大专业测评方法

代码逻辑 吸收能力 技术学习能力 综合素质

先测评确定适合在学习

在线申请免费测试名额
价值1998元实验班免费学
姓名
手机
提交