一、认识Java虚拟机
Java虚拟机是一个抽象(虚拟)的计算机,使得您的电脑能够运行 Java 程序,以及其他编译成 Java 字节码的程序
Java代码编译成.class字节码文件 => 在JVM上运行的指令 => JVM把这样的字节码再次翻译成二进制机器指令
引入虚拟机,就可以更好的跨平台,很好的支持不同操作系统和CPU
JVM 的核心作用:
- 内存区域划分
- 类加载机制
- 垃圾回收机制
二、JVM的内存区域划分
每次执行Java程序,本质上就是创建了一个对应的JVM,每个Java进程内部都包含了JVM
JVM运行时数据区
1. 程序计数器
很小的区域,只保存一个数字 => 下一条要执行的Java字节码指令的地址
在内存中,通过软件维护(JVM的源码)
2.栈
1.虚拟机栈
给Java程序使用的栈,维护了方法调用的关系(后进先出)
一个栈帧包括调用方法的实参、方法内部的局部变量、方法结束后要返回的上层方法的位置、返回值等等
2.本地方法栈
给C++代码使用的,因为JVM底层是C++实现的(方法修饰中的native)
3.堆(最大的区域)
存储new对象/普通成员变量
new出来的对象全都放在堆中
4.元数据区
存储类对象/static修饰的成员
.Java文件的时候,编写类/方法 => .class文件
JVM运行的时候,就会把.class文件读取到内存中,还需要一些特定的结构表示
注意:有些情况下,内存会溢出
栈溢出:包含的方法调用关系太多了(栈帧)
堆溢出:new的对象太多了
上面这些内存区域,针对程序计数器和栈是存在多份的(每个线程都有自己的),而对于堆和元数据区,一个进程中只有一份(new出来的对象,是可以直接被另一个线程使用的)
三、JVM的类加载机制
把.class文件读取放到内存中,构建出类对象过程
1.类加载的流程
- 加载
把.class文件找到,打开文件,并且读取文件的数据到内存当中,根据代码中的全限定类名,找到对应的.class文件 - 验证
确保加载的字节流符合 JVM 规范,并且不会危害虚拟机自身的安全 - 准备
为类的静态变量分配内存,并设置默认的初始值,在元数据区,Java默认把新申请的,未初始化的内存全都置为0,此时static成员的值也是0 - 针对字符串常量初始化
把当前的.class中的字符串常量,也放到内存中 - 初始化
针对类对象进行初始化操作,初始化类的静态成员,执行静态代码块,对父类的加载
什么时候会加载某个类???
懒汉思想,用的时候再加载
1.new这个对象的实例
2.调用这个类的静态方法/访问静态成员
3.针对某个子类的加载,会触发父类的加载
2.双亲委派模型
出现在类加载的第一步,用来找.class文件,涉及到一个模块,称为类加载器,JVM中包含3个类加载器
- 启动类加载器 (Bootstrap CL)
负责加载Java标准库中的类 - 扩展类加载器 (Extension CL)
负责加载Java扩展库中的类 - 应用程序类加载器 (Application CL)
负责加载第三方库/当前项目中的类
双亲委派模型,约定了类加载的优先级
标准库最先加载,其次是扩展库,最后是第三方库/当前项目
四、JVM的垃圾回收机制
1.内存泄漏:
堆上的内存光申请不释放,使用的内存越来越多,直到没有空闲内存,内存申请就会失败
JVM的垃圾回收机制就可以更好的解决内存泄漏问题,JVM 专门指派一些线程,这些线程周期性扫描已经申请内存(new的对象)自动判定,当前这个内存是否已经不再使用,如果不使用,就会释放掉对象/内存
Java的GC回收的内存是啥?

GC是以对象为单位进行内回收的(不是字节)
2.垃圾回收是如何进行的?
(1)找出谁是垃圾(不再使用的对象)
(2)释放垃圾对象对应的内存
如果某个对象,没有引用指向,此时就可以认为这个对象不再使用(宁可放过,也不要错杀)
3. 如何判定对象有没有引用指向?
方案一:引用计数(是Python/PHP方案)
给每个对象身上安排一个空间,这个空间存储一个整数表示指向这个对象引用的个数,若计数为0,则可以释放
缺点:1.消耗更多的内存空间 2.产生循环引用,导致误判

方案二:可达性分析
在一个Java代码中,一系列对象,引用存在一定的关系,类似于树型结构
可达性分析就是从树的根节点出发,尝试遍历这个对象树,遍历过程中凡是经过的对象,全都标记为可达 ,另一方面,JVM自身知道自己一共有哪些对象,除去可达的,剩下的就是不可达 ,随着代码的运行,对象之间的引用关系实时变化,上述可达性分析需要周期性进行
GCRoots作用对象:
1.栈上的局部变量
2.常量池引用所指向的对象
3.所有的引用类型静态成员
注意:一轮GC要把所有的GCRoots都尽可能的遍历,尽可能标记可达
缺点:需要消耗更多的CPU资源/更多的时间
4. 如何识别对象是垃圾?
通过引用识别,可达性分析,从所有的GCRoots出发,尽可能遍历访问到的对象都标记为可达,剩下的就是不可达
5. 识别出垃圾之后,如何释放内存?
- 标记-清除
把标记出来的垃圾,直接释放掉,这些被释放的内存的地址是离散的 ,形成了内存碎片 ,二申请内存,都是申请连续内存 - 复制算法
能够有效解决内存碎片问题,同一时刻只是用其中的一半
缺点:1.空间利用率很低(最多只能用到50%) 2.如果当前存活的对象很多,复制开销就很大 - 标记-整理
对于空间利用率,得到了改善
类似于顺序表删除中间元素 => 搬运(开销同样可能很大)
6.对于Java实际情况来说,综合了上面的策略,构成了一个更复杂的方案:分代回收
根据不同对象的情况/特点,采取不同的方案
对象年龄 => GC扫描的轮次,某个对象经过很多次GC都没有被释放,说明年龄比较大
整个堆,分为两个大部分:

- 新new的对象,放到伊甸区
- 伊甸区对象经过第一轮GC的时候,把大部分淘汰掉(经验规律:大部分的新对象,生命周期非常短)没有淘汰的对象,通过复制算法进入到幸存区,幸存区有两部分,每次只用其中的一部分
- 下一轮GC也会针对幸存区的对象扫描,还会再次淘汰一大批的对象,没有淘汰的对象,通过复制算法,进入到另一个幸存区(经验规律:每轮GC淘汰大部分的新对象,因此触发复制对象很少)
- 随着每一轮GC的进行,对象就会在新生代来回拷贝,每次拷贝,对象的年龄 + 1
- 经过一定时间之后,对象的年龄到达一定的阈值,此时就会把这个对象拷贝到老年代
- 对象进入老年代之后,针对老年代的GC频率比新生代低很多,减少了扫描的开销,老年代发现对象是垃圾,采取标记-整理的方式处理,虽然整理一次,比较消耗资源,但是频率很少
- 如果某个对象占据的内存非常大,就会直接放到老年代
上面的分代回收过程,严格来说,并不是真实情况,而是一个简化版
7.JVM垃圾回收器
CMS:能够尽可能多线程标记,尽可能不影响业务
GI:能够处理内存空间特别大的情况(划分更多的内存空间),一次GC只回收其中的一部分
ZGC:目前没有被JVM采纳,尽量使垃圾回收对业务逻辑的影响时间短,理想情况达到0.1ms以内
本期内容到此为止,喜欢的话请点个赞,谢谢观看!!!