目录
一.JVM
1>.介绍
在之前的学习,知道一个java程序运行的过程:.java程序被javac编译成.class二进制文件,最终在JVM虚拟机解释并运行
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机
作用:将二进制字节码文件翻译为二进制指令并运行
对于c/c++来说,一次编写多次编译 ,当换个电脑可能代码运行出错,是因为每个电脑的指令与API各有差异,所以要多次调试;对于java、PHP、大蟒蛇来说,代码都是通过虚拟机编译的运行的,只需要一次编写,就可以到处运行
**注:**与软件虚拟机不一样,软件虚拟机指通过软件模拟一个真实的电脑,包括:cpu、显卡等;而这里的JVM只起到翻译并运行的作用
2>.JVM的内存分配
每一次JVM启动时,就会从内存申请一块空间,后续应用程序在运行时,就可以按照JVM内存进行分配
而申请的内存空间:

1.程序计数器
很小一块内存空间里,用于指向下一条要执行的java字节码指令地址的一个数字
2.栈
此栈非数据结构的栈,而是java程序用于维护方法调用关系的栈,结构依然是先进后出,分为两类
(1).虚拟机栈
存储java方法调用关系的栈

递归 也是函数内调用自己,因为有虚拟机栈 的存在,才能定位到自己返回到哪里,而在多线程 中,每一个线程都会有自己的栈与计数器 ,为的就是确保执行顺序与返回正确,而剩下的一个进程存在一份
(2).本地方法栈
专门给一些不是Java代码实现的方法,提供的存储调用关系的栈
比如:jJVM底层是c++实现的,就需要调用一些函数,此时在本地方法栈存储函数的调用关系
3.堆
最大的内存区域 ,用于存储对象 与普通成员变量
4.元数据区(java8之前为方法区)
JVM将.class文件解析表示出来的类对象 与static修饰的属性
3>.类加载机制
类加载的过程总共分为5个(详细) / 3个(合并)
1.加载
找到对应的.class文件.并打开文件将二进制数据存纯在内存中
引入概念:
全限定类名:由包名与类名组合在一起;
如:java.util.Scanner:就是一个全限定类名
具体过程如下:


2.验证
根据java官方规定的模版,验证其二进制数据的正确性
可以在该网址下查看: 第 4 章。类文件格式


总的而言,这个过程会将对应的二进制数据套入该模版,若失败,则类有问题直接报错;否则,继续向下类加载
3.准备
给要创建的类对象申请一块空间,并把static修饰的成员值全部为默认值
比如:static String age = "123",在这一步 age = null;
4.解析
针对常量池进行初始化,即把当前.class中的字符串常量(无论静态还是普通属性)放入内存(字符串常量池)中
5.初始化
针对类对象进行初始化,此时开始进行:静态方法/属性-->实例成员方法/属性-->构造方法的初始化流程
6.小总结
听起来有点难搞,画个图再清晰一下最终形成的class对象的面目

对应了我在反射提到的class对象

4>.jvm垃圾回收机制GC
1.认识GC
C 语言需手动通过 malloc 分配、free 释放内存,若未正确释放会导致内存泄漏;而 Java 引入自动垃圾回收机制(GC), 属于后台进程 ,能"周期性"自动识别 并回收 不再使用的对象内存,大幅减少内存泄漏问题
2.GC回收的对象
每个线程 都会有自己一份栈区 与程序计数器 ,他们随着线程结束而销毁 ,而在进程中又共享元数据区与堆区, 元数据区存储类对象与静态属性 且全局只中有一份,不需要处理;在堆区有大量的对象存在,有些则创建但没使用,因此GC回收的就是堆区的对象
3.第一种寻找方案:引用计数
简述 :创建的对象在堆区 会有一块计数空间,大小不固定一般为8字节
工作原理 :每有一个引用指向该对象 ,计数器自动 + 1 ,若某个时刻计数器变0,此时GC就会回收该空间
如图:

**优点:处理速度快,**当计数器的值为0,就直接释放内存,无需等待GC周期性检验导致的时耗
缺点一: 堆内存开销严重
堆内存是有限的 ,当堆中开辟了大量的对象,每个对象又额外有8字节的"计算空间",整体的空间开销太大了
缺点二:循环引用
java
Test A = new Test();
Test B = new Test();
A.xxx = B;
B.xxx = A;
A = null;
B = null;

后续进行赋值,即:

此时这种情况,想要访问0x002对象,但此时该引用在0x001中,要想找到0x001对象,就必须在0x002对象内访问,.........................开始循环,并且此时这两个对象引用个数不为0无法销毁且无法获得
因为这些问题,JVM并没有采取引用计数方式
4.第二种寻找方案:可达性分析算法
从特定的根对象集合开始向下遍历,遇到不可达时先标记不可达对象,经过确认后才会被回收销毁
听起来很绕,举个例子来阐述根对象:
java
// 有以下类
TestA{
TestB B = new TestB();
TestC C = new TestC();
TestD D = new TestD();
}
TestB{
TestF F = new TestF();
}
TestC{
TestG G = new TestG();
}
TestD{
TestH H = new TestH();
}
具体流程:移动
- 从一系列 "根对象"(GC Roots) 开始,这些根对象 包括虚拟机栈中引用的对象 、常量池引用指向的对象 、所用引用类型的对象等
- 从根对象出发,向下遍历所有能直接或间接引用 到的对象,形成 "可达性对象集合"
- 堆中所有未被纳入这个可达性集合的对象,被视为 "不可达对象"
- 不可达对象并不会立即被销毁,它们需要经历至少两次标记过程,只有真正 "死亡" 的对象才会被垃圾回收器清理
优点:没有引入额外内存,没有循环引用问题
缺点 :可达性分析的遍历非常消耗CPU资源
5.处理方式
垃圾可以通过上述两种方式找到,但具体如何销毁?
(1).标记清除
直接将标记的数据直接清除

发现清除后内存变得不连续的,当后续创建对象时,可能总体上剩余空间足够,但没有连续且合适的空间开辟对象
优点:开发维护低,减少移动对象的CPU 消耗(下面讲到)
缺点:造成内存碎片化
(2).复制算法
将堆空间 一分为二,先把对象 放在一侧(左),等进行标记 后,将**"存活"**的对象放在另一侧(右),并整体销毁左侧对象

优点:解决了碎片化空间问题
缺点:
a.空间利用率低(一分为二,只能用一边)
b. 若当前存活的对象多 ,此时赋值就要消耗大量资源
(3).标记整理法
在标记删除的基础标记要删除的对象,将存活的对象向前移动,并将清除边界
类似于数组删除元素
优点: 解决了碎片化空间,增加空间利用率
缺点:移动消耗的资源还是有点大
6.分代回收
Jvm真正的回收机制,集齐了上述的所有优点,根据对象存活不周期的特点,采取不同的方案
每一次GC扫描,对象的"年龄"加一,当一个对象经过多次GC还没回收,后续它大概率还是存活的,这是经验总结

1.新创建 的对象都会放在伊甸区
2.经过一轮扫描后,会把"死亡 "的对象直接标记清除
3.接着存活的对象年龄加一 ,通过复制算法到一个幸存区
4.下一次扫描同时会扫描幸存区 ,"死亡"的对象直接标记清除,存活的年龄加一并放在另一个幸存区
5.随着每一轮扫描,都会将"死亡"的对象清除,存活的年龄加一复制算法到另一个幸存区
6.当"年龄"达到阈值(通常为15) ,就会复制算法到老年区
7.到老年区,扫描频率降低,将少了开销,若发现以"死亡",直接标记清除
注:
(1).若对象内存特别大,为了减少复制开销,也会直接放在老年区
(2).两个幸存区中必然有一个是空闲的(To 区),另一个存放着上一次 GC 后存活的对象(From 区)
7.垃圾回收器
上述的分代回收,只是思想,具体落实在程序上看JVM垃圾回收器
CMS :在不影响业务逻辑情况下,尽可能的多线程标记,减少可达性分析扫描时间
G1 :当内存空间 特别大时.将内存分为更多小块 ,其中有些为伊甸区、幸存区、老年区等,每次只回收其中一部分区域,进行多次回收
ZGC:在不影响业务逻辑下使垃圾回收对业务的影响时间变短(尽量达到0.1ms以内)
OK.到此JVM就告离一段落,若对你有帮助,点个赞吧~~