• JVM相关知识点


    目录

    ​编辑一、JVM 内存区域划分

    1、栈 

    2、堆

    3、元数据区(方法区)

    二、JVM 类加载机制

    1、类加载机制介绍

    2、双亲委派模型(经典问题)

    三、JVM 垃圾回收机制 GC

    1、了解 GC

    2、GC 实际工作过程:

    (1)找到垃圾 / 判定垃圾

    (2)对象的释放


    一、JVM 内存区域划分

    JVM 也就是启动的时候,会申请到一整个很大的内存区域

    JVM 是一个应用程序,要从操作系统这里申请内存(相当于租了个写字楼)

    JVM 就根据需要,把整个空间,分成几个部分,每个部分各自有不同的功能作用

    1、栈 

    2、堆

    堆,是整个 JVM 空间最大的区域

    new 出来的对象,都是在 堆 上,类的成员变量,也是在堆上面

    堆是一个进程只有一份的,栈是每个线程有一份,一个进程有多个

    堆,多个线程用的都是同一个堆,栈,每个线程用自己的栈

    每个 jvm ,就是一个 java 进程,如果弄两个 java 进程,就是两个 jvm 了


    3、元数据区(方法区)

    类对象,常量池,静态成员等都存放在这里


    如何判断某个变量在哪个区域上?

    1、局部变量:在 栈 上

    2、普通成员变量:在 堆 上

    3、静态成员变量:在 方法区 / 元数据区

    二、JVM 类加载机制

    1、类加载机制介绍

    准确的来说,类加载就是 .class 文件,从文件(硬盘)被加载到内存中(元数据区)这样的一个过程

    加载:把 .class 文件找到(找的过程),打开文件,读文件,把文件内容读到内存中

    验证:根据 jvm 虚拟机规范,检查下 .class 文件格式是否正确

    准备:给类对象分配内存空间(现在元数据区占个位),此时内存初始化成全为0,也会使静态成员设置成 0 值

    解析:初始化字符串常量,把符号引用转为直接引用

    初始化:调用构造方法,进行成员的初始化,执行代码块,静态代码块,加载父类 .....

    把符号引用转为直接引用:字符串常量,得有一块内存空间,来存这个字符的实际内容,还得有一个引用,来保存这个内存空间的起始地址

    在类加载之前,字符串常量,此时是处在 .class 文件中的,此时 “引用” 记录的并非字符串常量的真正的地址,而是它在文件中的 “偏移量” 这个东西(或者是个占位符)

    类加载之后,才真正把这个字符串常量给放到内存中,此时才有 “内存地址”,这个引用才能真正被赋值成指定内存地址(直接引用)

    类加载到底什么时候会触发?

    不是 java 程序一运行,就把所有的类都加载的,而是真正用到才加载(懒汉模式)

    1、构造 类 的实例

    2、调用这个类的静态方法 / 使用静态属性

    3、加载子类,就会先加载其父类

    一旦加载过之后,后续再使用,就不必重复加载了


    2、双亲委派模型(经典问题)

    双亲委派模型,描述的是这个加载,找 .class 文件的基本过程

    JVM 默认提供了三个类加载器,他们三个各有分工

    1、BootstrapClassLoader ,负责加载标准库中的类(Java 规范,要求提供哪些类,无论是哪种 JVM 的实现,都会提供一样的类)

    2、ExtensionClassLoader ,负责加载 JVM 扩展库中的内容(规范之外,由实现 JVM 的厂商 / 组织,提供的额外的功能)

    3、ApplicationClassLoader ,负责加载用户提供的第三方库 / 用户项目代码中的类

    上述三个类,存在 “父子关系”,(不是父类子类,相当于每个 classLoader 有一个 parent 属性,指向自己的父 类加载器

    上述三个类加载器如何配合工作?

    首先,加载一个类的时候,是先从 ApplicationClassLoader 开始,但是 ApplicationClassLoader 会把加载任务交给父亲,让父亲去进行

    于是 ExtensionClassLoader 要去加载了,但是也不是真加载,而是再委托自己的父亲

    于是 BootstrapClassLoader 要去加载了,也是想委托自己的父亲,结果发现自己的父亲是 null,没有父亲 / 父亲加载完了,没找着类,才由自己进行加载

    此时,BootstrapClassLoader 就会搜索自己负责的标准库目录相关的类,如果找到就加载,如果没找到,就继续由子 类加载器 进行加载,于是 ExtensionClassLoader 真正搜索扩展库相关的墓库,如果找到就加载,如果没找到就还是由子 类加载器 进行加载

    ApplicationClassLoader 真正搜索用户项目相关的目录,没找到就由子类加载器进行加载(由于当前没有 子 了,就只能抛出 类找不到 这样的异常)

    为什么要有上述这个顺序?

    上述这套顺序,其实出自于 jvm 实现代码的逻辑,这段代码大概是类似于 “递归” 的方式写的

    这个顺序,最主要的目的就是为了保证 BootstrapClassLoader 能够先加载,ApplicationClassLoader 能够后加载,这就可以避免用户创建了一些奇怪的类,引起不必要的 bug

    假设用户在自己的代码中,写了个 java.lang.String 这个类,按照上述加载流程, jvm 加载的还是标准库的类,不会加载到用户自己写的类

    这样就能保证,即使出现上述问题,也不会让 jvm 已有代码混乱,最多是用户自己写的类不生效罢了

    再另一方面,类加载器其实是可以用户自定义的,上述三个类加载器是 jvm 自带的,用户自定义的类加载器,也可以加入到上述流程,就可以和先有的加载器配合使用

    站在 jvm 的角度,类加载的核心应该是:解析 .class 文件,解析每个字节是干什么的

    破坏双亲委托模型:

    自己写的类加载器,可以去遵守也可以不遵守,是否遵守,主要是看需求

    tomcat ,,去加载 webapp 这里就是单独的类加载器,不遵守双亲委派模型


    三、JVM 垃圾回收机制 GC

    1、了解 GC

    什么是垃圾?

    垃圾指的是不再使用的内存

    垃圾回收就是把不用的内存帮我们自动释放了

    C 语言中有 malloc ,C++ 有 new ,这些属于动态申请内存(在堆上申请一块内存空间)

    上述内存空间需要手动方式进行释放:free,delete

    如果不手动释放,这块内存的空间就会一直存在,一直存在到进程结束(堆上的内存生命周期比较长,不像 栈,栈的空间会随着方法的执行结束,栈帧自动销毁而自动释放,堆则默认不能自动释放

    这可能会导致一个很严重的问题:内存泄露

    如果内存一直占着,又不释放,就会导致剩余空间越来越少,进一步导致后续的内存申请操作申请失败

    因为内存泄露是一个很严重的问题,因此大佬们就想了一些办法来解决这个问题,垃圾回收(GC)是其中最主流的一种方式

    GC 的好处:非常省心,让程序员写代码简单点,不容易出错

    GC 的坏处:需要消耗额外的系统资源,也有额外的性能开销

    另外,GC 这里还有一个比较关键的问题:STW 问题(stop the world)

    1、如果有时候,内存中的垃圾已经很多了,此时触发一次 GC 操作,开销可能非常大,大到可能就把系统资源吃了很多

    2、另一方面,GC 回收垃圾的时候,可能会涉及一些 锁 操作,导致业务代码无法正常执行

    JVM 里面有很多块内存区域,GC 主要是针对其中的 堆 进行释放

    GC 是以 “对象” 为基本单位进行回收的!!!(而不是字节)

    GC 回收的是整个对象都不在使用的情况,而一部分使用,一部分不使用的对象,暂时先不回收

    要回收,就是回收整个对象,而不会回收半个对象,因此我们说 GC 是以对象为基本单位进行回收的

    这样设定,目的就是 “简单”


    2、GC 实际工作过程:

    (1)找到垃圾 / 判定垃圾

    判定哪个对象以后一定不用了,哪个对象后面还可能使用

    关键思路:抓住这个对象,看看到底有没有 “引用” 指向它

    在 java 中,只有一条路:通过引用来使用!!!

    如果一个对象,有引用指向它,就可能被使用到,如果一个对象没有引用指向,就一定不会再被使用了

    具体如何知道对象是否有引用指向?

    两种典型实现:

    (1)引用计数 [不是 java 的做法]

    给每个对象,都分配了一个计数器(整数)

    每次创建引用指向该对象,计数器就 +1,每次该引用被销毁了,计数器就 -1

    缺点:

    1、内存空间浪费的多(利用率低)

    每个对象都得分配一个计数器,如果对象特别多,占用的额外空间就会很多(尤其是对象特别小的情况)

    2、循环引用的问题

    接下来,如果 a 和 b 引用销毁,此时 1 号对象和 2 号对象引用计数都 -1,但是结果都还是 1,不是 0

    虽然不是 0,不能释放内存,但是实际上这两个对象已经没有办法被访问到了


    (2)可达性分析 [ java 的做法]

    java 中的对象,都是通过引用来指向并访问的

    经常,是一个引用指向一个对象,这个对象里的成员,又指向别的对象

    整个 java 中所有的对象,就通过类似于上述的关系,通过这种 链式 / 树形 结构,整体给串起来

    可达性分析,就是把所有这些对象被组织的结构,视为是 树,从树根节点出发,遍历树,所有能被访问到的对象,标记为 “可达”,不能被访问到的,就是不可达

    每次你 new 一个对象,jvm 都会记录下来,jvm 会知道一共哪些对象,每个对象的地址...

    相当于 jvm 自己捏着一个所有对象的名单,通过上述遍历,把可达的标记出来,剩下的不可达的就可以作为垃圾进行回收了

    可达性分析,需要进行类似于 “树遍历” 这个操作,相比于引用计数来说,肯定要更慢一些,但是空间利用率提高了,同时解决了循环引用的问题

    但是速度慢没关系,上述可达性分析遍历操作,并不需要一直执行,只需要每隔一段时间,分析一遍就可以了(虽迟,但到)

    进行可达性分析遍历的起点,称为:GCroots,主要有以下几种:

    1、栈上的局部变量

    2、常量池中的对象

    3、静态成员变量

    一个代码中有很多这样的起点,把每个这样的起点都往下遍历一遍即可,就完成了一次扫描过程


    (2)对象的释放

    如何清理垃圾?,主要是三种基本方法

    1、标记清除

    简单粗暴,但是存在内存碎片问题

    被释放的内存空间是零散的,不是连续的,而申请空间要求的是连续空间

    这就可能会导致总的空闲空间很大,但是每一个具体空间都很小,此时申请大一点的空间就会失败了


    2、复制算法

    复制算法,就是把不是垃圾的对象,复制到另外一半,然后把整个空间删除掉

    每次触发算法,都是向另一侧复制,内存中的数据拷贝过去

    缺点:空间利用率低,如果垃圾少,有效对象多,复制成本就比较大了


    3、标记整理

    类似于顺序表删除中间元素,会有元素搬运的操作,保证了空间利用率,同时也解决了内存碎片的问题

    缺点:效率不高,如果要搬运的空间比较大,开销也很大


    上述做法,并不完美,基于上述这些基本策略,我们又有了一个复合策略:“分代回收”

    把垃圾回收,分成不同的常见,有的常见使用这个算法,有的场景使用那个算法,各展所长

    分代是怎么分的? 

    基于一个经验规律:如果一个东西存在的时间比较长了,大概率会继续长时间持续存在下去

    java 对象,要么就是生命周期特别短,要么就是特别长,我们就根据生命周期的长短,分别使用不同的算法

    我们给对象引入一个概念:年龄(熬过 GC 的轮次)

    我们可以认为:年龄越大,对象存在的时间就越久

    伊甸区:刚 new 出来的,年龄是 0 的对象

    熬过一轮 GC,对象就要被放到幸存区了

    虽然看起来幸存区很小,伊甸区很大,但是一般够放(根据经验规律,大部分的 java 对象都是 “朝生夕死”,生命周期特别短)

    伊甸区 => 幸存区 使用的是 “复制算法”

    到了幸存区域之后,也要周期性的接受 GC 的考验,如果变成垃圾,就要被释放,如果不是垃圾,就拷贝到另一个幸存区(这两个幸存区同一时刻只存在一个,两个之间使用复制算法来回拷贝)

    由于幸存区体积不大,此处的空间浪费,也能接受

    如果这个对象,已经在两个幸存区中,来回拷贝很多次,就要进入老年代

    老年代,都是年纪大的对象,生命周期普遍更长

    针对老年代,也要周期性GC 扫描,但是频率更低了,如果老年代的对象是垃圾了,使用标记整理的方式进行释放

    上述是 GC 中典型的垃圾回收算法

    实际上,JVM 在实现的时候,会有一些差异,事实上,JVM 有很多的 “垃圾回收实现”,称为 垃圾回收器

    回收期具体的实现做法,会按照上述算法思想展开,但是会有一些 变化 / 改进,不同的垃圾回收器,侧重点不同,这里就不再介绍了

  • 相关阅读:
    学习JAVA的第四天(基础)
    Anaconda虚拟环境创建管理流程
    设计模式-建造者模式
    提高工作效率的神器:基于前端表格实现Chrome Excel扩展插件
    java计算机毕业设计计算机组成原理教学演示软件源码+数据库+系统+lw文档+mybatis+运行部署
    事务操作(详细讲解)
    记录一个git无法push的问题
    Linux I/O schedulers类型简介
    解决 80% 的工作场景?GitHub 爆赞的 Java 高并发与集合框架,太赞了
    苹果ipad触控笔哪个好?ipad手写笔推荐
  • 原文地址:https://blog.csdn.net/weixin_73616913/article/details/132611461