• 面试害怕考到JVM? 看这一篇就够了~


    目录

    前言

    一、JVM内存划分

    二、类加载

    2.1、类加载是在干什么?

    2.2、类加载的过程

    2.3、何时触发类加载?

    2.4、双亲委派模型(重点考察)

    2.4.1、什么是双亲委派模型?

    2.4.2、涉及到的类加载器

    2.4.3、详细过程图解

    三、GC(垃圾回收机制)

    3.1、STW问题(Stop The World)

    3.2、GC回收哪部分内存?

     3.3、垃圾对象的判定算法

    3.3.1、引用计数法(非JVM采取的办法)

    3.3.2、可达性分析(JVM采取的办法)

    3.4、垃圾回收算法

    3.4.1、标记-清除算法

    3.4.2、复制算法

    3.4.3、标记整理算法

    3.4.4、分代算法


    前言

            面试中要考到有关JVM的话题,主要也就是三个方面:1.JVM内存划分,2.JVM类加载,3.JVM的垃圾回收;弄清楚这三个方面的内容,带你体会面试就如聊天?本篇会用通俗简洁的话,高效的带你理解JVM这三个模块!


    一、JVM内存划分

            java程序,是一个名字为java的进程,这个进程就是所说的“JVM”;

            JVM 会从操作系统中申请一块大的内存空间,在此基础上分成几个小的区域;

    区域划分如下图:

    注意:上图中每一个虚线框都对应一个线程;程序计数器是每个线程都有一个;

    这些区域分别存放什么?(面试如果问到,如下回答即可)

    1.堆:存放new出来的对象;(成员变量)

    2.方法区:存放的是类对象;(静态变量)

    3.栈(虚拟机栈, 本地方法栈):存放方法之间的调用关系;(局部变量)

    4. 程序计数器:存放的是下一个要执行的指令;

    注意:变量存放在哪一个区域,和变量类型无关!和变量的形态(局部,成员,静态)有关!

    测试:以下变量存放在什么位置?(笔试题中会出现)

     解释:

            变量a是成员变量,所以存放在堆里

            变量b是成员变量,所以存放在堆里

            变量c是静态变量,在类对象里,所以存放在方法区中;

            testHead这个引用是静态的,在方法区中,他new出的对象是在堆里的;

            test这个引用是局部变量,所以就在栈上,他new出的对象在堆中;


    二、类加载

    2.1、类加载是在干什么?

            java程序在运行之前,会先编译,也就是由 .java 编译成 .class文件(二进制字节码文件),运行的时候,java进程(JVM)就会读取到对应的 .class 文件,并解析内容,在内存中构造出类对象进行初始化;

    简而言之就是: 类 从 文件 加载到内存中;

    2.2、类加载的过程

    下图为类的生命周期:

     解释:

            前 5 步是固定的顺序并且也是类加载的过程,其中中间的 3 步我们都属于连接,所以对于类加载来 说总共分为以下五个步骤:

    1. 加载;
    2. 连接 =>(1.验证、2.准备、3.解析);
    3. 初始化;

    对类加载步骤的解释:

    1. 加载:找到 .class 文件,读取文件内容,按照 .class 规范的格式来解析;

    2. 验证:检查当前的 .class 里的内容格式是否符合要求;

    3. 准备:给类里的静态变量分配内存空间;

            例如,static int a = 6; 这段代码在准备阶段就会给a分配4个字节的内存空间,同时这些空间的初始值都是0;

    4. 解析:初始化字符串常量,把符号引用(占位符)替换成直接引用(内存地址)

            例如,String str = "hello!"; 类加载之前,"hello!"这个字符串常量并没有分配内存空间,因此 str 里就无法保存字符串常量的真实地址,只能使用一个占位符标记一下(标记了这里是"hello!"这个字符串常量的内存地址),等到真正给"hello!"分配了内存后(类加载完后),就可以用真正的地址代替之前的占位符;

    5.初始化:针对类进行初始化,初始化顺序如下

            父类(静态变量、静态代码块)–>子类(静态变量、静态代码块)–>父类(变量、代码块)–> 父类构造器–>子类(变量、初始化块)–>子类构造器。

    注意:静态代码和静态变量同级,变量和代码块同级。谁在前先执行谁。类只会初始化一次。

    2.3、何时触发类加载?

            这里并不是程序一启动就加载了,而是类似于[ 懒汉模式 ] ,使用到这个类的时候,才会触发加载;

    怎么算才是使用到这个类?

    1. 创建了这个类的实例;

    2. 使用了类的静态方法/静态属性;

    3.实用类的子类(加载子类会触发加载父类);

    2.4、双亲委派模型(重点考察)

    2.4.1、什么是双亲委派模型?

            一个类加载器收到类加载请求,首先自己不会加载这个类,而是把这个请求委派给父类加载器完成,每一层都是如此,因此所有加载请求最终都会送到最顶层的加载器中,只有父加载器反馈无法加载这个请求,子类才会尝试去加载;

    2.4.2、涉及到的类加载器

    1. Bootstrap ClassLoader :负责加载标准库中的类;

    2. Extension ClassLoader:负责加载JVM扩展的库的类(标准库中没有,但JVM自己实现出了);

    3. Application ClassLoader :负责加载我们自己的项目中的自定义类;

    2.4.3、详细过程图解

             有意思的是,上述过程并未涉及到 “双亲”,只是 “单亲”,这里的 “双亲” 实际上是机翻出来的,更直白其实可以叫 “单亲委派模型”...


    三、GC(垃圾回收机制)

            在学习C语言的过程中,需要通过 malloc 申请内存,最后通过 free 进行释放,这里就容易存在一个问题——忘记free,造成内存泄漏;而GC(垃圾回收)就是一个主流处理方案;

    GC是干什么的呢?

            就是一个自动释放内存的机制;我们只需要负责申请内存,释放内存的工作交给JVM完成,JVM会自动判定当前内存是否不再使用,若不再使用,就自动释放;(类似开车 => 手动挡 升级 自动档)

    3.1、STW问题(Stop The World)

            C++为何不引入GC?因为GC存在一个最大的问题就是会引入额外的 “空间+时间” 开销;

            空间上:消耗额外的CPU / 内存资源; 

            时间上:最大的问题——STW问题(Stop The World);

    什么是STW问题?

            当程序运行到需要GC释放内存的时候,就有需要消耗一定的时间,反应到用户这里,就有可能存在明显的卡顿;

            那那那那...为什么我们还要用他?因为在实际的开发中,开发效率是大于运行效率的~

    3.2、GC回收哪部分内存?

            方法区?类对象加载一次之后便不会卸载;栈?释放时机确定,不用回收;程序计数器?固定内存空间,不必回收;GC主要就是针对堆来回收的;

    如下图:

     3.3、垃圾对象的判定算法

    如何判断一个某个对象是否是垃圾?

            如果一个对象没有任何引用能够指向他,这个对象就是视为垃圾了;

    3.3.1、引用计数法(非JVM采取的办法)

    注意:此方法不是JVM采取的方法,Python、PHP使用这个方法;

            具体的,给每个对象加上一个计数器,这个计数器就表示 “当前的对象有几个引用”;

    如下图:

    解释:

            每多一个引用指向该对象,计数器就+1;

            每少一个引用指向该对象,计数器就-1;

            当计数器的数值为0时,就说明这个对象已经没有人能够再使用了,此时就可以进行释放;

    存在缺点:

            1. 空间利用率低,尤其是小对象(例如:计数器本身大小为int,对象里也只有一个int大小的成员,相当于空间增加了一倍);

            2. 可能出现循环引用的情况,如下:

     解释:

            当前这两个对象引用计数器为1,因为即使t1, t2两个引用被置为空,但实际上时两个对象在相互引用,此时外界代码是无法访问的,但由于引用计数器不是0,所以无法进行释放;

    3.3.2、可达性分析(JVM采取的办法)

            约定一些特定的变量,成为 “GC roots”, 每隔一段时间,从GCroots出发,进行遍历,查询哪些变量是能够被访问到的,能被访问到的变量就称为 “可达”,否则就是 “不可达”;

    GC roots对象可以是以下几种:

            -- 栈上的变量;

            -- 常量池引用的对象;

            -- 方法区中静态属性引用对象;

            -- 方法区中常量引用的对象;

    具体的如下图:

    解释:

            上图中,通过 GC roots就可以找到object1、object2、object3、object4;而object5、6、7则不可以被访问到,所以5、6、7就是垃圾了;

    3.4、垃圾回收算法

    3.4.1、标记-清除算法

            简单来说,就是标记出垃圾后,直接把对象对应的内存空间进行释放;

    如下图:

    解释:

            上图中,黑色部分就是被标记要清除的部分,经过回收后,就剩下了存活对象;

    缺点:

            1.效率:标记和清除这两个过程的效率都不高;

            2.空间(内存碎片):如上图,标记就清除后会产生大量不连续的内存碎片,碎片太多可能会导致之后程序运行中需要分配较大对象时,可能无法找到连续且足够大的空间而无法申请空间;

    3.4.2、复制算法

             这是针对内存碎片问题,引入的办法;

             具体的,将内存分成两块大小相等的空间,但只使用其中的一块,当需要进行垃圾回收时,就把正在使用的那块空间上还存活的对象复制到另一块上,再将使用过的那块内存全部清空;这样做的好处就是不用在考虑内存碎片问题;

    如下图: 

    缺点:

            1. 空间利用率相比标记清除法更低了;

            2. 若一轮GC下来,大部分需要保留,只有极少数要回收,这时候复制的开销就很大了;

    3.4.3、标记整理算法

            标记过程与 “标记-清除算法”一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端连续性的覆盖,然后直接清除掉边界以外的空间;(类似于顺序表的删除元素)

    如下图:

     评价:

            相对于复制算法而言,空间利用率提升了,同时也解决了内存碎片化问题,但是搬运操作比较耗时;

    3.4.4、分代算法

            分代算法总和了上面所说的三种算法,通过区域划分,实现不用区域用不同的垃圾回收策略,从而实现更好的垃圾回收;也就是我们常说的“因地制宜”~

    如下图:

     解释:

            1. 刚创建出来的对象,进入伊甸区;

            2. 若新对象熬过一轮GC,没挂,就通过复制算法,复制到生存区;

            3. 生存区的对象也要经历GC,每熬过一次GC,就会通过复制算法拷贝到另一个生存区(只要这个对象不死亡,就会在两个生存区来回拷贝);

            4. 如果一个对象在生存区中,反复坚持了很多轮还没去世,就会被放到老年代(老年代GC的频率会降低);

            5. 若对象来到了老年代,也会进行定期的GC,只是频率更低了;老年代采取标记整理的方式来处理垃圾;

            特殊处理:若新创建的对象非常大,则直接进入老年代;因为一个大的对象进行复制算法,开销太大;另一个角度考虑,既然是一个很大的对象,费这么大开销创建出来,肯定不是立即就销毁的;


  • 相关阅读:
    【机器学习-周志华】学习笔记-第八章
    【毕业设计】Stm32 WIFI智能家居温湿度和烟雾检测系统 - 单片机 物联网 嵌入式
    Spark第一课
    C语言结构体指针学习
    腾讯mini项目-【指标监控服务重构】2023-08-22
    Bigemap 在土地图利用环境生态行业中的应用
    YOLOR剪枝【附代码】
    HTML期末学生大作业-节日网页作业html+css+javascript
    SSM框架(SpringBoot快速构建)
    LiveData源码分析
  • 原文地址:https://blog.csdn.net/CYK_byte/article/details/128195296