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变量,在哪个内存区域中
- 栈:局部变量
- 堆:new 出来的对象(引用类型变量)、普通成员变量、全局变量
- 元数据区(方法区):类对象、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

-
把全限定类名,交给 ApplicationClassLoader
-
ApplicationClassLoader 把任务转交给父亲处理
-
ExtensionClassLoader 还是把任务转交给父亲处理
-
BootstrapClassLoader 没有父亲,只能自己处理,负责扫描指定的标准库类的目录,查找这个类,发现能够找到,因此就打开对应的 .class 文件,读取文件,进行解析,再进行类加载的后续步骤。
举例二:代码中有一个自定义的类,com.test.Hello
-
把全限定类名,交给 ApplicationClassLoader
-
ApplicationClassLoader 把任务转交给父亲处理
-
ExtensionClassLoader 还是把任务转交给父亲处理
-
BootstrapClassLoader 没有父亲,只能自己处理,标准库中找不到对应的类,把任务转交给孩子
-
ExtensionClassLoader 查询自己负责的目录,发现也没有,继续把任务转交给孩子
-
ApplicationClassLoader 查询第三方库的目录/当前项目的目录,找到了就可以打开文件 读取文件.... 如果在这一级还没有找到就抛出异常。
举例三:在代码中自己创建了一个 java.lang.String 这样的类
按照双亲委派机制原则,首先会向上委派,由上一层类加载器在他负责的范围内查找是否存在这个类,而 java.lang.String 在%JRE_HOME%\lib\rt.jar中已经存在,所在始终由BootstrapClassLoader加载器加载 rt.jar 中的 java.lang.String,我们自定义的这个同名的类始终无法加载。
- 本质上,双亲委派模型,约定了 "类加载的优先级",标准库最先加载,其次是扩展库,最后是第三方库/当前项目。JVM类加载模块,源代码就是写的这个逻辑。