一.JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box
JVM 和其他两个虚拟机的区别:
VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
JVM 是一台被定制过的现实当中不存在的计算机
二.JVM 运行流程
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过类加载器 (ClassLoader) 把文件加载到内存中 运行时数据区 (Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 **执行引擎 (Execution Engine)**将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
三.JVM的区域划分
jvm其实是一个java进程,java进程会从操作系统这里申请一大块内存区域,给java代码使用,这一大块内存区域,进一步划分

具体怎么划分的呢?
四个核心区域:1.程序计数器 2.元数据区(方法区jdk8之前叫法) 3.栈 4.堆
接下来逐个介绍
1.程序计数器:
很小的区域,是一种特殊的寄存器,只是用来存储当前正在执行的指令地址或下一条将要执行的指令地址(字节码指令)
2.元数据区:
放的是类加载之后的类对象,静态变量和常量池,类元信息和方法元信息

3.栈:
维护方法之间的调用关系和局部变量存这里,调用关系每次调用方法, 就会进入方法内部执行,当方法执行完毕,返回到调用位置,继续往后走

JVM的栈并不是操作系统直接实现的 ,而是由c++代码实现

4.堆:
new出来的对象和成员变量放这里
堆是 JVM 中最大的空间区域了,new对象,往集合类里面添加元素,成员变量等等都会放到这里
如果堆上的对象,不再使用了的话,就需要被释放掉~~(垃圾回收后面会讲到)
补充:看这样一个代码
Test t = new Test();
这个t引用变量是在栈上的还是堆上的还是在其他地方
分情况讨论:
如果 t是一个 同部变量,t就是在栈上
如果 t是一个 成员变量,t就是在堆上
如果 t是一个 静态成员变量,t 就是在元数据区
具体的JVM区域划分图

元数据区和堆,整个 java 进程共用同一份,程序计数器,和 栈,一个进程中可能有多份,(每个线程有一份)
四.JVM类加载
类加载简单来说就是把.class文件加载到内存,得到类对象这样的一个过程
程序员要想运行程序,就需要把程序依赖的指令和数据加载到内存中
类加载的步骤,非常复杂,但是把类加载的过程分为五个步骤
1.类加载阶段 (Loading)
加载:
a)找到 .class 文件(如何找?双亲委派模型,下面解释)
b)根据 类 的 全限定名(包名 +类名,形如 java.lang.String)
c)打开文件,读取文件内容到内存里~~
2.验证阶段(Verification)
class文件有明确的数据格式(二进制的),jvm就是要验证一下,打开读到的这个.class文件内容,是不是符合这个格式要求,要符合才能继续下一步,不符合就失败了,并且把.class的内容转化为结构化的数据

3.准备阶段(Preparation)
类加载,最终是为了得到类对象,因此准备阶段就是给类对象分配内存空间(这个空间是未初始化的内存中的数据全是0的,类对象中的静态成员啥的也是0)
相当于我租了个写字楼,但是是毛胚房,没装修
4.解析阶段(Resolution)
针对字符串常量,进行初始化~
解析阶段是 Java 虚拟机将常量池内的符号引用(不知道自己具体地址)替换为直接引用(有了自己的地址) 的过程,也就是初始化常量的过程
字符串常量,本身就包含在 .class 文件中
就需要 .class 文件里解析出来的字符串常量
放到 内存空间 里(元数据区, 常量池中)
5.初始化阶段(Initialization)
针对类对象进行初始化(初始化静态成员,执行静态代码块,对类的各种属性进行填充,如果有父类还需要加载父类...)
初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
类加载这个动作,不是jvm一启动,就把所有的.class文件都加载了,整体是一个"懒加载"的策略(懒汉模式),非必要,不加载
什么叫做"必要"
1.创建了这个类的实例
2.使用了这个类的静态方法/静态属性
3.使用子类,会触发父类的加载
五.双亲委派模型
双亲委派模型, 描述了 类加载中,根据 全限定类名, 找到 .class 文件的过程~~

图示↓

这一套流程,目的是为了约定"优先级,收到一个类名之后,一定是先在 标准库中找,再扩展库找
最后才是第三方库找
为啥要从applicationClassLoader开始并且一开始不会查找,直接从BootStrapClassLoader开始依次查找不行吗?
可以的,具体还是取决你JVM代码的设计,大佬设计成这样自有他的用意
补充:
六.JVM垃圾回收(GC)
C语言中,malloc的内存必须手动free,否则就容易出现内存泄露(光申请内存,不释放,内存逐渐用完了,导致程序崩溃),于是java等后续编程语言引入了GC来解决上述问题,能够有效的减少内存泄露的出现概率
C++没有采取GC机制,是因为影响效率(cpp追求极致的效率),GC中还有一个STW(Stop The World)机制,触发大规模的GC,必须使得其他业务代码停下来=>卡了
Java在JDK17之后STW时间一般小于1ms
JVM中的内存分为好几个区域,GC主要释放的是哪个空间呢?
堆!!!(new出来的对象)

GC的工作过程
1.找到垃圾(不再使用的对象)
一个对象,只能通过引用!!如果一个对象,没有引用指向他,此时这个对象一定是无法被使用的(妥妥的是垃圾),就释放,如果一个对象已经不想用了,但是这个引用可能还指向着呢,就不释放
如何找到垃圾?
a)引用计数法(python , php采取的这个方案)
每个对象在 new 的时候,都搭配一个小的内存空间来保存一个整数

两个设计缺陷
内存消耗更多
尤其是对象本身比较小,引用计数消耗的空间的比例就更大
假设引用计数是 4 个字节
对象本身是 8 个字节
引用计数就相当于提高了 50% 的空间占用率
存在循环引用的情况,会导致引用计数的判定逻辑出错


b)可达性分析算法(Java采取这个方法)
把对象之间的引用关系,理解成了一个树形结构.从一些特殊的起点出发,进行遍历.只要能遍历访问到的对象,就是"可达".再把"不可达的"当做垃圾即可
引用计数,是有空间开销,可达性分析,用时间换空间~~

流程


找到垃圾了,接下来就要释放掉垃圾
2.垃圾释放(对应的内存直接释放掉)
a)标记清除

b)复制算法


复制算法,解决了内存碎片问题,但是也有缺点
1.内存利用率比较低(这样最多只能同时使用一半内存)
2.如果当前的对象大部分都是要保留的,垃圾很少,此时复制成本就比较高了
用复制算法的合适场景是垃圾占大多数
c)标记整理算法

优点:解决了内存碎片问题和内存利用率问题
缺点:内存搬运数据的操作,开销是挺大的,复制成本的问题仍然还在
这么来看上述三个算法都有缺点,JVM具体采用的哪个呢?
JVM采用的是分代回收算法 (把上面abc三种方法结合起来(主要是bc)扬长避短)
d)分代回收
"代"=>对象的年龄~年龄是GC的伦次,某个对象,经历一轮 GC 可达性分析之后,不是垃圾;
此时对象的年龄就 +1,初始情况就是 0

伊甸区和幸存区名字来源圣经基督,大小8:1:1(两个幸存区大小一样)
针对不同的年龄的对象采取不同的策略
不同年龄的对象,特点是不同的~~

老年代,GC 频次就可以降低了
新生代, GC 频次就会比较高~~
伊甸区的对象如何到幸存区


幸存区的如何到老年代

晋升的本质是引用更新 :
JVM通过修改对象引用的地址,将其从幸存区的存活对象列表转移到老年代的内存空间中(类似"移动"而非"复制")
老年代内存管理算法采用标记整理
空间效率 :
复制算法需要预留一半内存空间(如新生代的
Survivor区),而老年代存放长期存活对象,占用空间大,浪费50%内存不现实。对象存活率高 :
老年代的对象存活率高,复制大量存活对象性能开销大,整理开销较小。
分代回收的过程
补充:
JVM内存参数也是可以修改的
-
堆内存设置:
-
-Xms:初始堆大小(如-Xms512m) -
-Xmx:最大堆大小(如-Xmx4g) -
-Xmn:年轻代大小(如-Xmn2g) -
-XX:NewRatio:老年代与年轻代的比例(如-XX:NewRatio=2表示老年代是年轻代的2倍)
-
-
元空间/方法区:
-
-XX:MetaspaceSize:初始元空间大小 -
-XX:MaxMetaspaceSize:最大元空间大小
-
-
直接内存:
-XX:MaxDirectMemorySize:直接内存大小限制
这只是内存参数,其他还有更多参数可以调,具体可以AI或百度搜索,修改教程可取抖音B站看相关视频