后端(五):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();
}

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

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

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

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

这两个栈也很好区分:

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

程序计数器

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

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

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

JVM 的类加载机制

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

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

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

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

类加载的声明周期如下:

 1.  加载

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

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

2. 验证

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

 源码中 都有一个 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 负责加载项目中自己写的类,以及第三方库 中的类

当具体加载一个类的时候,其过程如下:

双亲?上面明明就一个 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采用了)

借助图片来看看:

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

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

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

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

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

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

此时,这两个引用计数还在相互引用,并不是 0 ,所以不能作为垃圾,但是这个两个对象都不可以再使用 。

2. 可达行分析

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

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

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

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

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

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

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

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

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

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

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

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

释放”垃圾“对象

三种典型的 策略

1. 标记清除

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

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

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

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

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

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

2. 复制算法

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

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

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

 

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

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

3. 标记整理

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

我们将 有效元素往前搬

随后将搬运处的空间释放 

该算法虽然解决了内存碎片问题,但还存在一些问题:

  1.  搬运的开销比较大

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

分代算法

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

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

算法执行步骤:

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

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

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

本篇的八股就到这里,校招应该不会考那么多八股文,一定要把这些背下来!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/29148.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WEB阶段_CSSJS篇(附代码笔记)

&#xff08;一&#xff09;、使用DIVCSS布局首页 1、HTML的块标记 <!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title></head><body><div>div1</div><div>div2</div><…

查看IP地址方法(电脑IP地址方法)

查看IP地址方法 如何识别win7还是win10系统&#xff1f; &#xff08;一&#xff09;Win7系统电脑导航栏如下&#xff1a; &#xff08;二&#xff09;Win10系统电脑导航栏如下&#xff1a; 一、win7系统查看IP地址 方法一&#xff1a;查看网络设置 点击电脑导航栏最右下…

ASEMI快恢复二极管MUR20100CTR在电子工程中的应用

编辑-Z 随着电子技术的日益发展&#xff0c;各种电子元件的使用场景与需求也在逐步扩大。今天&#xff0c;我们将聚焦于一款广泛应用于各类电路的二极管——MUR20100CTR&#xff0c;来详细解读其性能特征及应用。 一、MUR20100CTR二极管的主要特性 MUR20100CTR是一款极高性能的…

Redis远程字典服务

目录 前言 1.NoSQL 1.1NOSQL和关系型数据库比较 1.2非关系型数据库的优势 1.3关系型数据库的优势 ​编辑 2.主流的NOSQL产品 键值(Key-Value)存储数据库 列存储数据库 文档型数据库 图形(Graph)数据库 3.Redis简介 redis的应用场景 4.命令操作 4.1字符串类型 s…

A7二极管-ASEMI迷你封装整流管A7二极管

编辑&#xff1a;ll A7二极管-ASEMI迷你封装整流管A7二极管 型号&#xff1a;A7二极管 品牌&#xff1a;ASEMI 封装&#xff1a;SOD-123 正向电流&#xff1a;1A 反向耐压&#xff1a;1000V 芯片大小&#xff1a;60MIL 芯片个数&#xff1a;1 引脚数量&#xff1a;2 …

git -- SSL certificate problem

SSL certificate problem 1.问题描述 新建一个仓库&#xff0c;在向里面上传文件时&#xff0c;出现SSL证书问题 2.解决方法 这个问题是由于没有配置信任的服务器HTTPS验证。默认&#xff0c;cURL被设为不信任任何CAs&#xff0c;就是说&#xff0c;它不信任任何服务器验证。…

Linux--使用者管理(job control)

Linux–使用者管理(job control) 文章目录 Linux--使用者管理(job control)前言一、任务管理(job control)二、&三、 将目前的任务丢到后台中暂停 -- ctrlz四、jobs -- 查看目前的后台任务状态五、fg -- 将后台任务拿到前台来处理六、bg -- 让任务在后台下的状态变为运行中…

uni-app image加载错误 404 替换为默认图片

双层v-for 使用item修改 aitem.cat_icon || defaultPic绑定图片src属性为aitem.cat_icon 如果aitem.cat_icon的值为空字符串或undefined&#xff0c;那么默认图片defaultPic被显示出来当图片加载错误时,触发handleImageError方法,将aitem传进去 <!-- 页面--><view …

如何解决VScode远程下载插件不了的问题?如何手动安装插件?

当我们在使用VScode进行远程操作时&#xff0c;在安装我们所需要的一些插件时&#xff0c;可能会出现如下图&#xff0c;一直卡在安装中....明明只有小几十MB&#xff0c;却一连好几个小时都一动不动。像这种情况&#xff0c;就需要我们进行手动安装该插件。 插件网站&#xff…

视频做成GIF动图怎么做?分享超简单的制作方法

将视频制作GIF动图的好处在于它可以将原本较长的视频压缩成一个简短、易于分享的图像文件。这使得它们非常适合用于社交媒体、博客、电子邮件等场景&#xff0c;可以当做表情包来使用&#xff0c;尤其是看到一段搞笑的视频&#xff0c;想要把它做成GIF动图该怎么做呢&#xff1…

Flask_使用flask_marshmallow序列化数据

代码如下&#xff1a; from flask import Flask from flask_marshmallow import Marshmallow from flask_sqlalchemy import SQLAlchemy from marshmallow import fieldsapp Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] "mysqlpymysql://root:12…

CentOS7中安装Mysql8并配置远程连接和修改密码等

场景 使用Vmware等虚拟机软件搭建CentOS7系统&#xff0c;需要在其上安装Mysql8版本数据库。 注&#xff1a; 博客&#xff1a;霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主 实现 1、去mysql官网手动下载rpm包并上传到服务器&#xff0c;或者直接通过wget…