15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > jvm(一)虚拟机概述

jvm(一)虚拟机概述

时间:2023-07-02 03:30:01 | 来源:网站运营

时间:2023-07-02 03:30:01 来源:网站运营

jvm(一)虚拟机概述:

一、基础知识

1、Java 程序的执行过程

一个 Java 程序, 首先经过 javac 编译成 .class 文件, 然后 JVM 将其加载到方法区, 执行引擎将会执行这些字节码。 执行时, 会翻译成操作系统相关的函数。 JVM 作为 .class 文件的翻译存在, 输入字节码, 调用操作系统函数。

过程如下: Java 文件->编译器>字节码->JVM->机器码。

JVM 全称 Java Virtual Machine, 也就是我们耳熟能详的 Java 虚拟机。 它能识别 .class 后缀的文件, 并且能够解析它的指令, 最终调用操作系统上的函数, 完成我们想要的操作。

2、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。

具体可以文档可以通过官网去下载: https://www.oracle.com/java/technologies/javase-jdk8-doc-downloads.html

JVM 的作用是: 从软件层面屏蔽不同操作系统在底层硬件和指令的不同。 这个就是我们在宏观方面对 JVM 的一个认识。

3、跨平台

我们写的一个类, 在不同的操作系统上(Linux、 Windows、 MacOS 等平台) 执行, 效果是一样, 这个就是 JVM 的跨平台性。

跨语言( 语言无关性) : JVM 只识别字节码, 所以 JVM 其实跟语言是解耦的, 也就是没有直接关联, JVM 运行不是翻译 Java 文件, 而是识别 class文件, 这个一般称之为字节码。 还有像 Groovy 、 Kotlin、 Scala 等等语言, 它们其实也是编译成字节码, 所以它们也可以在 JVM 上面跑, 这个就是 JVM 的跨语言特征。 Java 的跨语言性一定程度上奠定了非常强大的 java 语言生态圈。

4、常见jvm实现

二、jvm内存区域

运行时数据区

运行时数据区的定义: Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域

所以要深入理解 JVM 必须理解内存虚拟化的概念。

在 JVM 中, JVM 内存主要分为堆、 程序计数器、 方法区、 虚拟机栈和本地方法栈等。

同时按照与线程的关系也可以这么划分区域:

线程私有区域: 一个线程拥有单独的一份内存区域。

线程共享区域: 被所有线程共享, 且只有一份。

这里还有一个直接内存, 这个虽然不是运行时数据区的一部分, 但是会被频繁使用。 你可以理解成没有被虚拟机化的操作系统上的其他内存(比如操作系统上有 8G 内存, 被 JVM 虚拟化了 3G, 那么还剩余 5G, JVM 是借助一些工具使用这 5G 内存的, 这个内存部分称之为直接内存)

程序计数器是一块很小的内存空间, 主要用来记录各个线程执行的字节码的地址, 例如, 分支、 循环、 跳转、 异常、 线程恢复等都依赖于计数器。

由于 Java 是多线程语言, 当执行的线程数量超过 CPU 核数时, 线程之间会根据时间片轮询争夺 CPU 资源。 如果一个线程的时间片用完了, 或者是其它原因导致这个线程的 CPU 资源被提前抢夺, 那么这个退出的线程就需要单独的一个程序计数器, 来记录下一条运行的指令。

因为 JVM 是虚拟机, 内部有完整的指令与执行的一套流程, 所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号) , 如果是遇到本地方法(native 方法) , 这个方法不是 JVM 来具体执行, 所以程序计数器不需要记录了, 这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址, 所以在执行 native 方法时, JVM 中程序计数器的值为空(Undefined)。

另外程序计数器也是 JVM 中唯一不会 OOM(OutOfMemory)的内存区域。

每个线程私有的, 线程在运行时, 在执行每个方法的时候都会打包成一个栈帧, 存储了局部变量表, 操作数栈, 动态链接, 方法出口等信息, 然后放入栈。 每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。 方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。

虚拟机栈的作用: 在 JVM 运行过程中存储当前线程运行方法所需的数据, 指令、 返回地址。 其实在我们实际的代码中, 一个线程是可以运行多个方法的。

这段代码, 就是起一个 main 方法, 在 main 方法运行中调用 A 方法, A 方法中调用 B 方法, B 方法中运行 C 方法。

我们把代码跑起来, 线程 1 来运行这段代码, 线程 1 跑起来, 就会有一个对应 的虚拟机栈, 同时在执行每个方法的时候都会打包成一个栈帧。

比如 main 开始运行, 打包一个栈帧送入到虚拟机栈。

栈的数据结构: 先进后出(FILO)的数据结构,

虚拟机栈是基于线程的: 哪怕你只有一个 main() 方法, 也是以线程的方式运行的。 在线程的生命周期中, 参与计算的数据会频繁地入栈和出栈, 栈的生命周期是和线程一样的。

虚拟机栈的大小缺省为 1M, 可用参数 –Xss 调整大小, 例如-Xss256k。

栈帧: 在每个 Java 方法被调用的时候, 都会创建一个栈帧, 并入栈。 一旦方法完成相应的调用, 则出栈。

栈帧组成:

1、局部变量表

用于存放我们的局部变量的(方法中的变量) 。 首先它是一个 32 位的长度, 主要存放Java 的八大基础数据类型, 一般 32 位就可以存放下, 如果是 64 位的就使用高低位占用两个也可以存放下, 如果是局部的一些对象, 比如我们的 Object 对象, 我们只需要存放它的一个引用地址即可。(基本数据类型、 对象引用、 returnAddress 类型)

2、 操作数据栈:

操作数栈是执行引擎的一个工作区,类似于缓存

存放 java 方法执行的操作数的, 它就是一个栈, 先进后出的栈结构, 操作数栈, 就是用来操作的, 操作的的元素可以是任意的 java 数据类型, 一个方法刚刚开始的时候, 这个方法的操作数栈就是空的。

3、 动态连接:

Java 语言特性多态(后续章节细讲, 需要结合 class 与执行引擎一起来讲) 。

4、 返回地址:

正常返回(调用程序计数器中的地址作为返回) 、 异常的话(通过异常处理器表<非栈帧中的>来确定

正常返回: (调用程序计数器中的地址作为返回)

三步曲:

恢复上层方法的局部变量表和操作数栈、

把返回值(如果有的话) 压入调用者栈帧的操作数栈中、

调整程序计数器的值以指向方法调用指令后面的一条指令、

异常的话: (通过异常处理表<非栈帧中的>来确定)

栈帧执行对内存区域的影响

public class Person { public int work()throws Exception{ int x =1; int y =2; int z =(x+y)*10; return z; } public static void main(String[] args) throws Exception{ Person person = new Person();//person 栈中--、 new Person 对象是在堆 person.work(); person.hashCode(); }}work方法对应指令

0 iconst_1 1 istore_1 2 iconst_2 3 istore_2 4 iload_1 5 iload_2 6 iadd 7 bipush 10 9 imul10 istore_311 iload_312 ireturn具体指令含义查看:[三] java虚拟机 JVM字节码 指令集 bytecode 操作码 指令分类用法 助记符




大概执行过程:先把数据压入到操作数栈中,然后存储到局部变量表中或者通知操作引擎进行指令计算(运算后的结果自动入栈),最终出栈

本地方法栈跟 Java 虚拟机栈的功能类似, Java 虚拟机栈用于管理 Java 函数的调用, 而本地方法栈则用于管理本地方法的调用。 但本地方法并不是用 Java 实现的, 而是由 C 语言实现的(比如 Object.hashcode 方法)。

本地方法栈是和虚拟机栈非常相似的一个区域, 它服务的对象是 native 方法。 你甚至可以认为虚拟机栈和本地方法栈是同一个区域。

虚拟机规范无强制规定, 各版本虚拟机自由实现 , HotSpot 直接把本地方法栈和虚拟机栈合二为一 。

方法区(Method Area) 是可供各条线程共享的运行时内存区域。 它存储了每一个类的结构信息, 例如运行时常量池(Runtime Constant Pool)字段和方法数据、 构造函数和普通方法的字节码内容、 还包括一些在类、 实例、 接口初始化时用到的特殊方法。

方法区是 JVM 对内存的“逻辑划分” , 在 JDK1.7 及之前很多开发者都习惯将方法区称为“永久代”, 是因为在 HotSpot 虚拟机中, 设计人员使用了永久代来实现了 JVM 规范的方法区。 在 JDK1.8 及以后使用了元空间来实现方法区。

1、Class 常量池(静态常量池)

在 class 文件中除了有类的版本、 字段、 方法和接口等描述信息外, 还有一项信息是常量池 (Constant Pool Table), 用于存放编译期间生成的各种字面量和符号引用。

字面量: 给基本类型变量赋值的方式就叫做字面量或者字面值。

比如: String a=“b” , 这里“b”就是字符串字面量, 同样类推还有整数字面值、 浮点类型字面量、 字符字面量。

符号引用 : 符号引用以一组符号来描述所引用的目标。 符号引用可以是任何形式的字面量, JAVA 在编译的时候一个每个 java 类都会被编译成一个 class文件, 但在编译的时候虚拟机并不知道所引用类的地址(实际地址), 就用符号引用来代替, 而在类的解析阶段(后续 JVM 类加载会具体讲到) 就是为了把这个符号引用转化成为真正的地址的阶段。

一个 java 类(假设为 People 类) 被编译成一个 class 文件时, 如果 People 类引用了 Tool 类, 但是在编译时 People 类并不知道引用类的实际内存地址, 因此只能使用符号引用(org.simple.Tool) 来代替。 而在类装载器装载 People 类时, 此时可以通过虚拟机获取 Tool 类的实际内存地址, 因此便可以既将符号org.simple.Tool 替换为 Tool 类的实际内存地址。

符号引用主要包括:

常量表中的数据结构:

2、运行时常量池

运行时常量池( Runtime Constant Pool) 是每一个类或接口的常量池( Constant_Pool) 的运行时表示形式, 它包括了若干种不同的常量: 从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。

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

运行时常量池是方法区的一部分。 运行时常量池相对于 Class 常量池的另外一个重要特征是具备动态性 。

在 JDK1.8 中, 使用元空间代替永久代来实现方法区, 但是方法区并没有改变, 变动的只是方法区中内容的物理存放位置, 但是运行时常量池和字符串常量池被移动到了堆中。 但是不论它们物理上如何存放, 逻辑上还是属于方法区的。

3、字符串常量池

以 JDK1.8 为例, 字符串常量池是存放在堆中, 并且与 java.lang.String 类有很大关系。 设计这块内存区域的原因在于: String 对象作为 Java 语言中重要的数据类型, 是内存中占据空间最大的一个对象。 高效地使用字符串, 可以提升系统的整体性能。

所以要彻底弄懂, 我们的重心其实在于深入理解 String。

堆是JVM 上最大的内存区域, 我们申请的几乎所有的对象, 都是在这里存储的。 我们常说的垃圾回收, 操作的对象就是堆。

堆空间一般是程序启动时, 就申请了, 但是并不一定会全部使用。 堆一般设置成可伸缩的。

随着对象的频繁创建, 堆空间占用的越来越多, 就需要不定期的对不再使用的对象进行回收。 这个在 Java 中, 就叫作 GC( Garbage Collection) 。那一个对象创建的时候, 到底是在堆上分配, 还是在栈上分配呢? 这和两个方面有关: 对象的类型和在 Java 类中存在的位置。

Java 的对象可以分为基本数据类型和普通对象。对于普通对象来说, JVM 会首先在堆上创建对象, 然后在其他地方使用的其实是它的引用。 比如, 把这个引用保存在虚拟机栈的局部变量表中。对于基本数据类型来说( byte、 short、 int、 long、 float、 double、 char), 有两种情况。当你在方法体内声明了基本数据类型的对象, 它就会在栈上直接分配。 其他情况, 都是在堆上分配。

直接内存有一种更加科学的叫法, 堆外内存。

JVM 在运行时, 会从操作系统申请大块的堆内存, 进行数据的存储; 同时还有虚拟机栈、 本地方法栈和程序计数器, 这块称之为栈区。 操作系统剩余的内存也就是堆外内存。

它不是虚拟机运行时数据区的一部分, 也不是 java 虚拟机规范中定义的内存区域; 如果使用了 NIO,这块区域会被频繁使用, 在 java 堆内可以用directByteBuffer 对象直接引用并操作;这块内存不受 java 堆大小限制, 但受本机总内存的限制, 可以通过

-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样) , 所以也会出现 OOM 异

常。

1、 直接内存主要是通过 DirectByteBuffer 申请的内存, 可以使用参数“MaxDirectMemorySize” 来限制它的大小。

2、 其他堆外内存, 主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。

堆外内存的泄漏是非常严重的, 它的排查难度高、 影响大, 甚至会造成主机的死亡。 同时, 要注意 Oracle 之前计划在 Java 9 中去掉 sun.misc.Unsafe API。 这里删除 sun.misc.Unsafe 的原因之一是使 Java 更加安全, 并且有替代方案。

目前我们主要针对的 JDK1.8, JDK1.9 暂时不放入讨论范围中, 我们大致知道 java 的发展即可。

三、栈和堆区别

1、功能

以栈帧的方式存储方法调用的过程, 并存储方法调用过程中基本数据类型的变量(int、 short、 long、 byte、 float、 double、 boolean、 char 等) 以及对象的引用变量, 其内存分配在栈上, 变量出了作用域就会自动释放;

而堆内存用来存储 Java 中的对象。 无论是成员变量, 局部变量, 还是类变量, 它们指向的对象都存储在堆内存中;

2、 线程独享还是共享

栈内存归属于单个线程, 每个线程都会有一个栈内存, 其存储的变量只能在其所属线程中可见, 即栈内存可以理解成线程的私有内存。

堆内存中的对象对所有线程可见。 堆内存中的对象可以被所有线程访问。

3、空间大小

栈的内存要远远小于堆内存

关键词:虚拟

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭