JVM(Java虚拟机)~

一、认识Java虚拟机

Java虚拟机是一个抽象(虚拟)的计算机,使得您的电脑能够运行 Java 程序,以及其他编译成 Java 字节码的程序
Java代码编译成.class字节码文件 => 在JVM上运行的指令 => JVM把这样的字节码再次翻译成二进制机器指令

引入虚拟机,就可以更好的跨平台,很好的支持不同操作系统和CPU

JVM 的核心作用:

  1. 内存区域划分
  2. 类加载机制
  3. 垃圾回收机制

二、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.类加载的流程

  1. 加载
    把.class文件找到,打开文件,并且读取文件的数据到内存当中,根据代码中的全限定类名,找到对应的.class文件
  2. 验证
    确保加载的字节流符合 JVM 规范,并且不会危害虚拟机自身的安全
  3. 准备
    为类的静态变量分配内存,并设置默认的初始值,在元数据区,Java默认把新申请的,未初始化的内存全都置为0,此时static成员的值也是0
  4. 针对字符串常量初始化
    把当前的.class中的字符串常量,也放到内存中
  5. 初始化
    针对类对象进行初始化操作,初始化类的静态成员,执行静态代码块,对父类的加载

什么时候会加载某个类???

懒汉思想,用的时候再加载

1.new这个对象的实例

2.调用这个类的静态方法/访问静态成员

3.针对某个子类的加载,会触发父类的加载

2.双亲委派模型

出现在类加载的第一步,用来找.class文件,涉及到一个模块,称为类加载器,JVM中包含3个类加载器

  1. 启动类加载器 (Bootstrap CL)
    负责加载Java标准库中的类
  2. 扩展类加载器 (Extension CL)
    负责加载Java扩展库中的类
  3. 应用程序类加载器 (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. 标记-清除
    把标记出来的垃圾,直接释放掉,这些被释放的内存的地址是离散的 ,形成了内存碎片 ,二申请内存,都是申请连续内存
  2. 复制算法
    能够有效解决内存碎片问题,同一时刻只是用其中的一半
    缺点:1.空间利用率很低(最多只能用到50%) 2.如果当前存活的对象很多,复制开销就很大
  3. 标记-整理
    对于空间利用率,得到了改善
    类似于顺序表删除中间元素 => 搬运(开销同样可能很大)

6.对于Java实际情况来说,综合了上面的策略,构成了一个更复杂的方案:分代回收

根据不同对象的情况/特点,采取不同的方案

对象年龄 => GC扫描的轮次,某个对象经过很多次GC都没有被释放,说明年龄比较大

整个堆,分为两个大部分:

  1. 新new的对象,放到伊甸区
  2. 伊甸区对象经过第一轮GC的时候,把大部分淘汰掉(经验规律:大部分的新对象,生命周期非常短)没有淘汰的对象,通过复制算法进入到幸存区,幸存区有两部分,每次只用其中的一部分
  3. 下一轮GC也会针对幸存区的对象扫描,还会再次淘汰一大批的对象,没有淘汰的对象,通过复制算法,进入到另一个幸存区(经验规律:每轮GC淘汰大部分的新对象,因此触发复制对象很少)
  4. 随着每一轮GC的进行,对象就会在新生代来回拷贝,每次拷贝,对象的年龄 + 1
  5. 经过一定时间之后,对象的年龄到达一定的阈值,此时就会把这个对象拷贝到老年代
  6. 对象进入老年代之后,针对老年代的GC频率比新生代低很多,减少了扫描的开销,老年代发现对象是垃圾,采取标记-整理的方式处理,虽然整理一次,比较消耗资源,但是频率很少
  7. 如果某个对象占据的内存非常大,就会直接放到老年代

上面的分代回收过程,严格来说,并不是真实情况,而是一个简化版

7.JVM垃圾回收器

CMS:能够尽可能多线程标记,尽可能不影响业务

GI:能够处理内存空间特别大的情况(划分更多的内存空间),一次GC只回收其中的一部分

ZGC:目前没有被JVM采纳,尽量使垃圾回收对业务逻辑的影响时间短,理想情况达到0.1ms以内

本期内容到此为止,喜欢的话请点个赞,谢谢观看!!!

相关推荐
每天进步一点点dlb4 小时前
JVM中的垃圾回收算法和垃圾回收器
jvm·算法
tant4 小时前
声明式事务@Transactional失效场景研究
java
沐苏瑶4 小时前
PHP反序列化漏洞
java·开发语言
Chloe_lll4 小时前
threejs(七)PBR材质
开发语言·javascript·材质
zh_xuan5 小时前
c++ stringstream字符串流的用法
开发语言·c++
该用户已不存在5 小时前
写了这么多年Java,这几个神仙技巧你用过吗?
java·后端
小王不爱笑1325 小时前
Java 核心知识点查漏补缺(二)
java·开发语言
Lacrimosa&L5 小时前
OS_2 进程与线程(进程管理)
java·开发语言
zl9798995 小时前
SpringBoot-Web开发之嵌入式容器
java·spring boot