面试害怕考到JVM? 看这一篇就够了~
  TEZNKK3IfmPf 2024年03月30日 68 0

前言

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


一、JVM内存划分

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

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

区域划分如下图:

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

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

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

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

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

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

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

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

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

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

 解释:

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

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

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

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

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


二、类加载

2.1、类加载是在干什么?

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

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

2.2、类加载的过程

下图为类的生命周期:

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

 解释:

        前 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、详细过程图解

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

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

 


三、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主要就是针对堆来回收的;

如下图:

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

 3.3、垃圾对象的判定算法

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

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

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

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

 

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

如下图:

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

解释:

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

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

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

存在缺点:

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

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

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

 解释:

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

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

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

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

        -- 栈上的变量;

        -- 常量池引用的对象;

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

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

具体的如下图:

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

解释:

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

3.4、垃圾回收算法

3.4.1、标记-清除算法

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

如下图:

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

解释:

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

缺点:

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

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

3.4.2、复制算法

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

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

如下图: 

 

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

缺点:

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

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

3.4.3、标记整理算法

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

如下图:

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

 评价:

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

3.4.4、分代算法

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

如下图:

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

 解释:

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

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

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

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

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

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

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2024年03月30日 0

暂无评论

推荐阅读
  TEZNKK3IfmPf   2024年04月19日   41   0   0 进程内存
  TEZNKK3IfmPf   2024年05月31日   27   0   0 JMM内存
  TEZNKK3IfmPf   2024年04月19日   30   0   0 内存
TEZNKK3IfmPf