JVM Java虚拟机

C/C++是没有 "虚拟机" 这样的概念的,C/C++程序是直接运行在真实的操作系统上的;被编译成二进制的机器指令,直接在CPU上运行。包括Go、Rust......

Java引入了JVM(Java虚拟机),更严格的叫法可以理解为 "Java解释器" "Java执行引擎"。先把java代码编译成 .class 字节码文件 [JVM上运行的指令] => JVM 把这样的字节码再次翻译成二进制机器指令(在程序运行过程中,现场翻译,因此性能会有折扣)。包含虚拟机的编程语言非常多,如Python、Javascript、PHP、Ruby、Lua......他们不叫xxx虚拟机,分别是Python解释器、JS的执行引擎、PHP运行时......

引入虚拟机,可以更好的实现跨平台,支持不同的操作系统和CPU。可以使java代码做到一次编写,到处执行,直接把写好的 编译好的 .class 字节码文件,放到不同平台的JVM上运行,完全不需要修改任何代码,更不必重新编译。Java虚拟机不是只有一个,有很多版本的,windows版本、Linux版本、MacOs版本、Android等,都能解释执行相同的字节码。

一、JVM 内存区域划分

每次运行Java程序,本质上就创建了一个对应的JVM,即每个Java进程内部都包含了JVM。

Java程序中使用的内存其实是JVM的内存。JVM启动的时候,从操作系统申请一大块内存。应用程序后续需要使用的时候,就可以从JVM的内存进行分配了。

JVM运行时数据区域也叫内存布局:

1. 程序计数器

很小的区域,只保存一个数字,即下一条要执行的java字节码指令的地址,在内存中,通过软件维护(JVM的源码),程序计数器 我们在代码中感知不到。CPU专门有一个寄存器,也叫做 "程序计数器",在CPU的寄存器里,通过硬件维护。计算机中,同一个术语,可能有不同的含义,需要结合上下文来理解。

2. 栈

(1)虚拟机栈

给java程序使用的栈,维护了方法调用的关系

此处的这个栈,是JVM中的区域

(2)本地方法栈

给C++代码使用的,JVM底层是C++实现的。Java中写的代码,往下调用着调用着,就变成C++的范围了。

3. 堆

堆是当中最大的区域,new出来的对象和普通成员变量都放在堆中。

4. 元数据区

之前 (Java8) 称为 "方法区"。.java文件里有我们编写的类/方法,经过编译变成 .class文件 二进制的字节码,JVM运行的时候就会把 .class 文件读取到内存中,还需要通过一些特定的结构来表示 即类对象。还有别的地方也提到过类对象,例如 synchronized 修饰静态方法就是在给类对象加锁。

小结:代码中 xxx变量,在哪个内存区域中

  1. 栈:局部变量
  2. 堆:new 出来的对象(引用类型变量)、普通成员变量、全局变量
  3. 元数据区(方法区):类对象、static 修饰的属性

有些情况下,可能会导致内存溢出。

1)栈溢出

栈包含了方法的调用关系(栈帧),如果栈帧太多了会导致栈溢出。比如,递归结束条件有错误,导致无限递归

2)堆溢出

new 的对象太多了,无限循环地往某个集合类中添加元素。

JVM运行的时候提供了一些参数,可以设置栈空间和堆空间。

上面的这些内存区域,针对程序计数器和栈,存在多份(每个线程有一份自己的)。堆和元数据区,一个进程中只有一份,一个线程中new的对象是可以直接被另一个线程使用的,因此有了线程安全问题。

二、JVM 的类加载机制

把 .class 文件 读取放到内存中,构建出类对象的过程

1. 类加载的流程

第一步:加载(找到.class文件)

根据代码中写的 "全限定类名" ,找到对应的 .class 文件,打开文件,并读取文件的数据到内存中。注:.class文件,是放在一些特定目录中的。java代码中,要使用某个类,需要import [全限定类名] 。

第二步:验证

根据读到的二进制内容,验证是否是合法的格式。

Java虚拟机规范 文档:Chapter 4. The class File Format

第三步:准备

给要创建的类对象,分配内存空间(元数据区)。Java默认把新申请的 未初始化的内存,置为0. 如下代码

此时尝试获取 static 成员,得到的值是0

第四步:初始化字符串常量

把当前 .class 中的字符串常量,也放到内存中,此时字符串就有了起始地址,顺便可以把这些地址取出来,放到对应使用这些字符串的地方了。

第五步:初始化

针对类对象进行初始化操作。初始化类的静态成员,执行静态代码块,对父类的加载(构造子类对象要先完成对父类的构造)。

什么时机会加载某个类呢?"懒汉"思想,用到的时候才加载。

1)new这个类的实例 2)调用这个类的静态方法/访问静态成员 3)针对子类的加载,也会触发父类的加载

注:类加载,加载一次即可。每个类的类对象,在一个JVM进程中,也是单例的。

2. 双亲委派模型

双亲委派模型,出现在类加载的第一步。更严格的叫法,"单亲委派模型"/"父亲委派模型",parent(双亲之一),而不是parents(双亲)。双亲委派模型,用来找 .class 文件,涉及到一个模块,称为 "类加载器",Java默认包含了3个类加载器。

举例一:代码中有一个类,java.lang.String

  1. 把全限定类名,交给 ApplicationClassLoader

  2. ApplicationClassLoader 把任务转交给父亲处理

  3. ExtensionClassLoader 还是把任务转交给父亲处理

  4. BootstrapClassLoader 没有父亲,只能自己处理,负责扫描指定的标准库类的目录,查找这个类,发现能够找到,因此就打开对应的 .class 文件,读取文件,进行解析,再进行类加载的后续步骤。

举例二:代码中有一个自定义的类,com.test.Hello

  1. 把全限定类名,交给 ApplicationClassLoader

  2. ApplicationClassLoader 把任务转交给父亲处理

  3. ExtensionClassLoader 还是把任务转交给父亲处理

  4. BootstrapClassLoader 没有父亲,只能自己处理,标准库中找不到对应的类,把任务转交给孩子

  5. ExtensionClassLoader 查询自己负责的目录,发现也没有,继续把任务转交给孩子

  6. ApplicationClassLoader 查询第三方库的目录/当前项目的目录,找到了就可以打开文件 读取文件.... 如果在这一级还没有找到就抛出异常。

举例三:在代码中自己创建了一个 java.lang.String 这样的类

按照双亲委派机制原则,首先会向上委派,由上一层类加载器在他负责的范围内查找是否存在这个类,而 java.lang.String 在%JRE_HOME%\lib\rt.jar中已经存在,所在始终由BootstrapClassLoader加载器加载 rt.jar 中的 java.lang.String,我们自定义的这个同名的类始终无法加载。

  • 本质上,双亲委派模型,约定了 "类加载的优先级",标准库最先加载,其次是扩展库,最后是第三方库/当前项目。JVM类加载模块,源代码就是写的这个逻辑。
相关推荐
千码君20163 小时前
Go语言:关于导包的两个重要说明
开发语言·后端·golang·package·导包
oak隔壁找我3 小时前
Java 高级特性
java·后端
骈拇3 小时前
重写、重载、访问者模式
java
88号技师3 小时前
2025年8月SCI-汉尼拔·巴卡优化算法Hannibal Barca optimizer-附Matlab免费代码
开发语言·人工智能·算法·数学建模·matlab·优化算法
_w_z_j_3 小时前
C++----变量存储空间
开发语言·c++
花菜会噎住3 小时前
Vue3 路由配置和使用与讲解(超级详细)
开发语言·javascript·ecmascript·路由·router
老K的Java兵器库3 小时前
对象创建源码追踪:从 new 指令到 JVM 内部实现
java·jvm
小学鸡!3 小时前
spring boot实现接口数据脱敏,整合jackson实现敏感信息隐藏脱敏
java·spring boot
细节控菜鸡3 小时前
【2025最新】ArcGIS for JavaScript 快速实现热力图渲染
开发语言·javascript·arcgis