后端(五):JVM

这篇具有很好参考价值的文章主要介绍了后端(五):JVM。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

JVM 中的内存区域划分

JVM 的类加载机制

 1.  加载

2. 验证

3. 准备

4. 解析

5. 初始化

JVM 中的垃圾回收策略

找,确认垃圾

1. 引用计数

2. 可达行分析

释放”垃圾“对象

1. 标记清除

2. 复制算法

3. 标记整理

分代算法


JVM也就是我们俗称的八股文,作为一个普通的程序员工作中是基本不可能用到的;因为它设计的初衷就是为了让程序员能够比较简单的,感知不到系统层面的一些内容(很多时候,程序员只关注业务逻辑,不需要关注底层实现细节)。

数年前开始,JVM成为了Java程序猿必考的内容。

JVM里的内容非常多,大部分内容咱叶看不懂,JVM本来是写给C/C++ 程序员看的,它的底层实现都是 C/C++ 的代码,研究JVM 的也都是那一批人。

我们这里针对JVM的面试题,作出以下一些讨论。

本篇主要在于以下三点:

  1. JVM 中的内存区域划分
  2. JVM 的类加载机制
  3. JVM 中的垃圾回收策略

本章比的不是你了解多少,比的是你能背下来多少

JVM 中的内存区域划分

JVM 其实是一个 Java 进程, Java 进程 会从操作系统中申请一大块内存区域,给 java代码使用

而这一大块内存区域,就有我们之前 SE 阶段提到的:栈区、堆区、方法区(新版的也叫 元数据区)。

内存区域划分最最核心的部分:

  1. 堆区: new 出来的对象(成员变量)
  2. 栈区: 维护方法之间的调用关系(局部变量)
  3. 方法区/元数据区: 放的是类加载之后的类对象(静态变量)

一般这里的考点就是 给你一段代码,问你某个变量在哪个区。

我们主要就是看这个变量是个什么变量(局部变量,成员变量【注意被 final 修饰过的局部变量】,静态变量)

这里简单举个例子:

void func() {
    Test t = new Test();
}

后端(五):JVM,JavaEE(初阶),jvm

 这里的 t 对象是个引用类型,t是个局部变量,所以是在栈上的的,而 new Test(),这个对象本体是在堆上的。至于func() 这个方法是存在方法区的,方法在内存中都是以二进制的方式存储的。

我们来看一张更细节的图:

后端(五):JVM,JavaEE(初阶),jvm

我们在SE 阶段认识的在做细分 。

其中将栈区分为  本地方法栈 和 虚拟机栈 。

这两个栈也很好区分:

  1. 虚拟机栈,是给 Java 代码使用的
  2. 本地方法栈,是个JVM 内部的本地方法使用的,我们说 JVM 源码都是 C++ 实现的(可以简单理解为是给 C++ 使用的)

程序计数器

这个程序计数器是用来记录当前程序指令到了哪一个指令了。

上述的 堆区 和 元数据区 在一个 JVM 进程中只有一份。

而栈(本地方法栈和 虚拟机栈)和 程序计数器 是存在多份的,每一个 线程 都存在一份

JVM 的类加载机制

类加载其实就是 把 .class 文件,加载到内存中,得到 类对象 这个过程。

咱们 祖师爷(冯诺依曼)提出:程序运行,就需要把依赖的"指令和数据" 加载到内存中。

这个类加载的过程非常复杂(一般不需要我们理解)

《深入理解 Java虚拟机(第三版)》这本书中,将类加载总结成了 5 个词 (必备)

后端(五):JVM,JavaEE(初阶),jvm

类加载的声明周期如下:

后端(五):JVM,JavaEE(初阶),jvm

 1.  加载

加载其实就是找到 .class 文件,并且读取文件内容

这里涉及到一个非常经典的考点,双亲委派模型,这个等到类加载的最后来讲;

2. 验证

.class 文件有明确的数据格式(二进制的)

后端(五):JVM,JavaEE(初阶),jvm

 源码中 都有一个 ClassFile 来体现一个类有哪些信息

3. 准备

给类对象 分配内存空间(这里还没有走到 初始化阶段,所以这里的内存空间都是全 0 的)

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

比如此时有这样一行代码:
        public static int value = 123;
它是初始化 value 的 int 值为 0,而非 123。

4. 解析

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
这里涉及到 将 符号引用 替换为 直接引用 的过程

我们先来说说符号引用

符号引用就是 字符串常量,我们在 验证 阶段不是加载好了 .class 文件嘛,.class 文件存在了 字符串常量,我们这里就能直到它的相对位置(偏移量),但是并不知道它的实际位置。

这个时候的字符串常量就是 符号引用。

直接引用

当真正加载到内存中,就会把字符串常量填充到内存中的特定地址上;

字符串常量之间的相对位置还是一样的,但是这些字符串有了自己真正的内存地址,此时的字符串就是直接引用。

5. 初始化

其实 4 和 5 可以并做一步,但是人家作者规定了 将其分为两步。

解析是初始化了 字符串常量,而这一步是 初始化 对象【除字符串常量部分】(初始化静态成员,执行静态代码块、如果类存在父类,还需要加载父类)

ok,到这里 上述 五步 也就讲完了。

类加载这个步骤,啥时候会触发呢?

并非 jvm 一启动,就会把所有的 .class 都加载了,整体采纳了 (懒汉设计模式)的策略,非必要不加载。

啥叫必要

  1. 创建了这个类的实例
  2. 使用了这个类的静态  方法/属性
  3. 使用了这个类的子类

总之就是与这个类相关的类、属性、方法、实例化对象...

这里还有一个最关键的考点:双亲委派模型

这个模型所做的就是 加载这个步骤中,找一个 .class 文件这个过程

事实上,这个模型并非是类加载中一个很重要的步骤,但是是个非常重要的考点

JVM 中,加载类,需要用到一组特殊的模块:类加载器

在 JVM 中,内置了三个类加载器

BootStrap ClassLoader 负责加载 Java标准库中的类

Extension ClassLoader 负责加载一些非标准的类,但是 是sun/Oracle 扩展的库的类

Application ClassLoader 负责加载项目中自己写的类,以及第三方库 中的类

当具体加载一个类的时候,其过程如下:后端(五):JVM,JavaEE(初阶),jvm

双亲?上面明明就一个 null ,哪里来的双亲?

这个就是个翻译问题,parent 表示双亲中的一个,我们翻译为双亲罢了。

这个双亲委派模型也是可以打破的,自己实现的类加载器,是否遵守上述规则,看个人设计。

JVM 中的垃圾回收策略

垃圾回收机制 也可以叫 GC 回收机制

这个可以帮助程序员自动释放内存

在 C语言中,malloc 需要手动 free,否则就会出现内存泄漏(光申请,不释放,内存用完了,程序也就完了)

Java 等后继编程语言,都采用了 GC 来解决上述问题,能够有限减少内存泄漏出现的概率。

内存的泄露是个比较纠结的问题:

申请的时机是明确的 => 使用到了必须申请;

释放的时机是模糊的 => 彻底不使用了才能释放

啥叫彻底不使用了:没有引用指向它

JVM 中的内存有好几个区,我们要释放的就是 (new 出来的)

我们也来随手聊聊其他区的释放:

  1. 程序计数器,就是个单纯存地址的整数,不要了就直接随着线程一起销毁。
  2. 栈,也是随着线程一起销毁的,方法调用完毕,方法的局部变量自然随着出栈的操作销毁了。
  3. 元数据区/方法区,村的类对象,很少会“卸载”。
  4. 堆,是GC的主要目标,GC也是以对象为单位进行释放的(说是释放内存,本质就是释放对象)

GC 中主要分为两个阶段:

  1. 找,谁是垃圾
  2. 释放,将垃圾对象释放掉

上述两个阶段,主要是了解清楚垃圾回收的算法,这些算法 并不代表 JVM真实的实现方法,现在的JVM 在这些基本算法之上又做出了很多细节的调整和优化。

找,确认垃圾

什么是垃圾,后续不会再使用到的,就是垃圾,Java中使用一个对象,只能通过引用,如果一个对象没了引用就可以确定它是垃圾了。

Java中只是单纯的通过引用没有指向这个操作,来判定垃圾。

单纯的通过引用可能会照成 垃圾释放不及时,但这都是小事;就怕释放太快,将还有引用的对象释放掉,这是大事。

具体来说说,Java怎么样直到一个对象是否还有引用呢?

1. 引用计数

给对象安排一个而外的的空间,保存一个整数,表示该对象有几个引用

(事实上,Java并没有采用这个方案,反倒是 python和PHP采用了)

借助图片来看看:

后端(五):JVM,JavaEE(初阶),jvm

随着引用的增加,计数器就增加,引用的销毁计数器就销毁。

当,计数器为0,就认为没有了引用,于是就是垃圾了

每个计数器都开辟一个空间, 上述之开辟一个空间,是因为都是同一类型 Test,我要是有其他类型,例如Cat 那么还需要开辟一个空间。

因此,这种方法会出现两个问题:

  1. 对象类型过多,那么就会非常浪费空间
  2. 存在循环引用的情况,那么就会导致引用计数的判定的逻辑出错

后端(五):JVM,JavaEE(初阶),jvm

假设,由上述这个栗子,此时,a 和 b 销毁了,这个时候,两个对象的引用计数各自减 1 :

后端(五):JVM,JavaEE(初阶),jvm此时,这两个引用计数还在相互引用,并不是 0 ,所以不能作为垃圾,但是这个两个对象都不可以再使用 。

2. 可达行分析

可达行分析将对象之间的引用关系,理解成了一棵树形结构,从一些特殊的起点出发,进行遍历,只要能遍历访问到的对象,就是“可达的“,其余”不可达“ 的就作为垃圾处理掉。

后端(五):JVM,JavaEE(初阶),jvm

 此时,通过 root 这个引用,就可以访问到整个树的任意结点,通过上述方式,root就能引用到所有结点。

如果这里的 e 突然为 null ,g是否就是不可达呢?

不对,既是e 为 null 了,还会存在其他指向指向g,g 任然是可达的。

可达性分析关键要点,进行上述遍历,需要有”起点“

  1. 栈上的局部变量(每个栈的每个局部变量,都是起点)
  2. 常量池中的引用的对象
  3. 方法区中,静态成员引用的对象

可达性分析,总的来说,就是从所有的 gcroots 的起点触发,看看该对象里又通过引用能访问哪些对象,顺腾摸瓜,把所有可以访问的对象都遍历一遍(遍历的同时把对象标记成为”可达“)

可达性分析,克服了引用计数的两个缺点,但是它也存在自身的问题

  1. 这个树有高度,搜易搜索会消耗更多的时间,因此某个对象成为垃圾,不一定能够第一时间发现,扫描会消耗时间
  2. 再进行可达性分析的时候,要顺着树进行搜索,一旦这个过程中,当前代码出现了变化,就更复杂了

因此,为了更准确的完成这个 扫描过程,还需要其他业务暂停工作(STW问题)

所谓的 STW 也就是 stop the world ; 这个是可达性分析最最被诟病的问题,Java发展了这么多年,垃圾回收这个也在不断的更新优化,STW 这个问题,现在以及能够比较好的应对了,但是并不能完全消除,已经可以让 STW 的时间大大缩短了。

上述已经解决了理论的找垃圾了,接下来就是释放垃圾

释放”垃圾“对象

三种典型的 策略

1. 标记清除

"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象:

后端(五):JVM,JavaEE(初阶),jvm

如图所示, 画了勾的就是我们需要释放的;

我们这块空间很难被重复利用,我们申请的”整块的连续的空间“现在这里空闲的空间都是离散的,有自己独立的空间。

总的空闲的空间 可能超过 1个 G,但是你想申请 500M 都不一定能够申请到(因为他们都是散的)。

"标记-清除"算法的不足主要有两个 :

  1. 效率问题 : 标记和清除这两个过程的效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集

2. 复制算法

我们把整个内存空间,分为两段,一次只使用一半:

后端(五):JVM,JavaEE(初阶),jvm

还是一样,花了勾的属于垃圾;

我们将不属于垃圾的复制到另一半空间中去,剩下的全部释放掉:

后端(五):JVM,JavaEE(初阶),jvm

复制算法虽然解决了内存碎片的问题,但是也存在相关的缺点:

  1. 内存利用率低
  2. 如果当前的对象大部分都是要保留的,垃圾很少,此时复制成本很高

3. 标记整理

类似于顺序表删除中间的元素:

后端(五):JVM,JavaEE(初阶),jvm

我们将 有效元素往前搬

后端(五):JVM,JavaEE(初阶),jvm

随后将搬运处的空间释放 

后端(五):JVM,JavaEE(初阶),jvm该算法虽然解决了内存碎片问题,但还存在一些问题:

  1.  搬运的开销比较大

 事实上, JVM 的实现思路,是上述集中的结合,针对不同情况下取长补短:

分代算法

基本思想:给每个对象设置”年龄“ ,这个概念,描述对象存在多久了,如果是一个对象刚刚诞生,认为是 0 岁,经过每一轮扫描(可达性分析),没被标记成垃圾,就长大一岁

通过描述年龄来表示对象存活时间 

后端(五):JVM,JavaEE(初阶),jvm

算法执行步骤:

  1. 1. 新创建的对象,放到伊甸区,当垃圾回收扫描到伊甸区之后,绝大部分对象都会在第一轮 GC 中被干掉
  2. 如果伊甸区的对象,熬过第一轮,通过复制算法,拷贝到生存区,生存去分为两半,一次只是用一半,垃圾回收扫描伊甸区的对象,也是发现垃圾就淘汰,不是垃圾就复制到另一半
  3. 当这个对象熬过多轮,年龄增长到一定程度,通过复制算法拷贝到老年代
  4. 进入老年代,年龄都挺大了,再消亡的概率小于前面的新生代,针对老年代的 gc 扫描概率小很多,如果发现老年代某个对象是垃圾,使用标记整理的方式清楚
  5. 特殊情况:某个对象很大,直接存放到老年代,因为这个对象搬运起来的开销很大,不适合多次搬运

这些都只是基本思想,具体的实现再这些基础之上,还做出了很多改进和优化;

Java的版本在变,垃圾回收器也会不断变化。

本篇的八股就到这里,校招应该不会考那么多八股文,一定要把这些背下来!!!!!文章来源地址https://www.toymoban.com/news/detail-558216.html

到了这里,关于后端(五):JVM的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【JAVAEE】JVM中垃圾回收机制 GC

      博主简介:想进大厂的打工人 博主主页: @xyk: 所属专栏: JavaEE初阶   上篇文章我们讲了java运行时内存的各个区域。 传送门:【JavaEE】JVM的组成及类加载过程_xyk:的博客-CSDN博客 对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线

    2024年02月16日
    浏览(37)
  • 【JavaEE】JVM的组成及类加载过程

    博主简介:想进大厂的打工人 博主主页: @xyk: 所属专栏: JavaEE初阶   本文我们主要讲解一下面试中常见的问题,如果想深入了解,请看一下《Java虚拟机规范》这本书 目录 文章目录 一、JVM简介 二、JVM整体组成 2.1 运行时数据区组成 2.2 小结 三、JVM类加载 3.1 类加载过程 四、

    2024年02月13日
    浏览(29)
  • 后端(五):JVM

    目录 JVM 中的内存区域划分 JVM 的类加载机制  1.  加载 2. 验证 3. 准备 4. 解析 5. 初始化 JVM 中的垃圾回收策略 找,确认垃圾 1. 引用计数 2. 可达行分析 释放”垃圾“对象 1. 标记清除 2. 复制算法 3. 标记整理 分代算法 JVM也就是我们俗称的八股文,作为一个普通的程序员工作中

    2024年02月15日
    浏览(25)
  • Java后端开发面试题——JVM虚拟机篇

    目录 什么是程序计数器? 你能给我详细的介绍Java堆吗? 什么是虚拟机栈 1. 垃圾回收是否涉及栈内存? 2. 栈内存分配越大越好吗? 3. 方法内的局部变量是否线程安全? 4.什么情况下会导致栈内存溢出? 5.堆栈的区别是什么? 能不能解释一下方法区(元空间)? 常量池 运行

    2024年02月09日
    浏览(30)
  • JAVA后端开发面试基础知识(一)——JVM

    Class loader(类装载) 根据给定的全限定名类名(如: java.lang.Object)来装载class文件到 Runtime data area中的method area。 Execution engine(执行引擎) 执行classes中的指令。 Native Interface(本地接口) 与native libraries交互,是其它编程语言交互的接口。 Runtime data area(运行时数据区域) 这就是我们常说

    2024年03月10日
    浏览(47)
  • 第一次后端复习整理(JVM、Redis、反射)

    文章仅为自身笔记 详情查看一篇文章掌握整个JVM,JVM超详细解析!!! jvm是Java虚拟机 程序员编写代码形成.java文件 经过javac编译成.class文件 再通过JVM的类加载器进入运行时数据区 就是我们java运行时的东西的存放区域 其中分为五个模块 线程共享的: 方法区 堆 线程私有的

    2024年02月15日
    浏览(26)
  • 深入理解JVM后端优化技术-锁粗化(Lock Coarsening)

    锁粗化(Lock Coarsening/Lock Merging)是JIT编绎器对内部锁的具体实现所做的一种优化。 深入理解jvm执行引擎-CSDN博客 深入理解JVM后端优化技术-方法内联-CSDN博客 深入理解JVM后端优化技术-逃逸分析(Escape Analysis)-CSDN博客 深入理解JVM后端优化技术-锁消除(Lock Elision)-CSDN博客 锁粗化默认

    2024年04月10日
    浏览(32)
  • Java后端开发中Java 8,JVM和JDK的关系

    Java8(也就是Java1.8)是Java编程语言的一个主要版本,正式名称为Java Platform, Standard Edition 8 (Java SE 8)。Java 8在2014年3月发布,引入了许多新特性,如Lambda表达式、新的日期时间API、接口中的默认和静态方法等。Java 8的引入使得Java程序可以更加简洁、易读,同时提高了编程效率。

    2024年04月08日
    浏览(34)
  • JavaEE初阶 - 文件/IO

    我们先来认识狭义上的文件(file)。针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时,往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份份真实的文件一般。 文件除了有数据内容之外,

    2024年02月02日
    浏览(23)
  • 【JavaEE初阶】 线程安全

    线程安全是多线程编程是的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。上述是百度百科给出的一个概念解释。换言之,线程安全就是某

    2024年02月08日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包