javaEE-8.JVM(八股文系列)

目录

一.简介

二.JVM中的内存划分

JVM的内存划分图:

堆区:​编辑

栈区:​编辑

程序计数器:​编辑

元数据区:​编辑

经典笔试题:

三,JVM的类加载机制

1.加载:

2.验证:

3.准备:

4.解析:

5.初始化:

双亲委派模型

概念:

 JVM的类加载器 默认有三种:

双亲委派模型的工作流程:

四.JVM的垃圾回收机制(GC)

 垃圾回收步骤:

1.识别出垃圾

1)引用计数

2)可达性分析

2.把标记为垃圾的对象的内存空间进行释放

1)标记-清除:

2)复制算法

3)标记整理

分带回收


一.简介

JVM : java Virtual Machine 的简称,意为Java虚拟机。

java的执行流程是:先通过javac 将.java文件转为.class(字节码文件)文件,之后在某个平台执行;然后 通过JVM 将.class文件转换为CPU能识别的机器指令。

因此,编写一个java程序,只需要发布.class文件就行了。JVM拿到.class文件,就知道该如何转换了.

二.JVM中的内存划分

JVM也相当于一个进程,在启动一个java程序后,需要个JVM分配资源空间.

JVM从系统中申请的内存,会根据java程序中不同的使用途径,为其分配空间.这就是内存划分.

JVM会将申请到的空间划分成几个区域,每个区域有不同的功能,

JVM的内存划分图:

堆区:

存放的是代码中new出来的对象,对象中的非静态成员也在堆区.

栈区:

包含了一些方法调用关系和局部变量.

由本地方法栈和虚拟机栈组成,本地方法栈是JVM内部,是由C++写的;虚拟机栈保存了一些java的方法调用和局部变量。

平时所说的栈区,指的是虚拟机栈,

程序计数器

这个区域比较小,专门用来保存下一条要执行的java指令的地址。

元数据区:

包含了一些辅助性质的,描述性质的属性。元数据区也叫做方法区。

元数据是计算机中的一个常见术语(Meta data)。

对于硬盘来说,不仅要存储文件的数据本体,还要存储一些辅助信息,像文件的大小,文件的位置,文件的使用权限,文件的拥有者....这些都称为“元数据”。

一个程序中,有哪些类,有哪些方法,每个方法中有哪些指令,....这些信息都会保存在JVM的元数据区.

对于堆区和元数据区,整个进程中只有一份;而对于栈区和程序计数区,在内存中是有很多份的.

经典笔试题:

class Test {private int n;private static int m;
}
public static void main(String args[]){Test t = new Test();
}

问: n,m,t 都在哪块JVM的哪个内存区域中?

n属于局部变量,在作用域中生效,出作用域就销毁了,存在栈区.

m:属于静态变量,存在元数据区。

t:是new出来了一个Test对象,t中保存的是Test的地址,属于局部变量,保存在栈区;而Test对象则保存在堆区.

区分变量在内存的哪个区域上,最重要的就是确定该变量的"形态",是 局部变量/成员变量/静态变量....

三,JVM的类加载机制

类加载指的是JVM把.class文件从硬盘读取到内存,进行一系列的校验解析的过程.转换成类对象的过程.

类加载过程大致分为五步:

1.加载:

把.class文件找到并打开,读取到文件中的内容.

2.验证:

需要确定当前读到的文件是合法的.class文件(字节码文件).否则若读到错误的文件,后面的工作就白费了.

具体的验证依据是在java的虚拟机规范中,有明确的格式说明:

左面这一列是类型,右面这一列是名字.

:

也叫做:magic number 魔幻数字,用来标识二进制文件中的格式的类型.

:

这两个都是版本号,u4 是主版本,u2 是次版本.属于JVM内部的版本,JVM会验证.class文件的版本号是否符合要求.

一般来说 高版本的JVM可以运行低版本的.class文件,反之不行.

3.准备:

为类对象申请内存空间.此时申请到的内存空间都为默认值 为全0的.

4.解析:

主要是针对类中的字符串常量进行处理.

将常量池中的 符号引用 替换为 直接引用 的过程,也就是初始化常量的过程.

我们知道,在.class文件中,是不存在地址的,而对于创建的字符串常量,变量中保存的是常量的地址,这又是怎样记录的呢?

class Test{private String s="hello";
}

这个hello在.class文件中,是否会保存呢?

当然是要保存的,只不过s中保存的是一个字符串常亮的"偏移量".

在文件中,不存在地址这样的概念,地址是内存的地址,而文件是在硬盘中的.

为了保存字符串常来那个,可以存储一个"偏移量"的概念, 这里的偏移量就认为是符号引用.

之后,把.class文件加载到内存中,就有地址了,s中的值就能根据偏移量来转换为真正地址了,也就是直接引用.

5.初始化:

针对类对象,完成后续的初始化操作.

执行静态代码块,构造方法,还可能触发父类加载.....

双亲委派模型

在类加载过程的第一步:加载环节中使用 双亲委派模型 描述如何查找.class文件的策略.

JVM在进行类加载的时候,有一个专门的模块,称为"类加载器".(ClassLoader)

概念:

双亲委派模型: 如果一个类加载器收到一个类加载的请求,他首先不会自己加载该类,而是将这个类委派给父类加载器,让父类加载器去完成对类的加载.每层次的类加载器都是这样委派,最终所有的加载请求都会到达 最顶层的类加载器,直到当父类加载器反馈自己无法完成这个类加载请求时,子类加载器就会尝试自己完成加载.

 JVM的类加载器 默认有三种:

BootstrapClassLoader: 负责查找标准库目录.

ExtensionClassLoader: 负责查找扩展库目录.

ApplicationClassLoader: 负责查找当前项目的代码目录,以及第三方库.

 这三个类加载器存在父子类(二叉树关系)关系.

ApplicationClassLoader的父类是ExtensionClassLoader;

ExtensionClassLoader的父类是BootstrapClassLoader,BootstrapClassLoader属于顶层父类。

双亲委派模型的工作流程:

1.类加载任务先从ApplicationClassLoader为入口,开始工作;

2.ApplicationClassLoader自己不会立即搜索自己负责的目录,会将搜索的任务向上传递给父类;

3.代码进入ExtensionClassLoader的范畴,同样,ExtensionClassLoader 也不是立即搜索自己负责的目录,继续将搜索的任务向父类传递;

4.代码进入BootstrapClassLoader的范畴,由于BootstrapClassLoader是顶级父类了,就会真正进行负责搜索目录(标准库目录),尝试在标准库目录中找到符合要求的.Class文件;

5.若是找到了,就会进入打开文件,读文件流程了,此时类加载步骤就结束了;若是没有找到,就会返回到子类的类加载器中,继续尝试加载。

6.若是在ExtensionClassLoader类加载器中找到符合要求的.Class文件,此时类加载步骤就结束了;若还未找到,就会返回给子类加载器ApplicationClassLoader继续尝试加载.

7.若在ApplicationClassLoader类加载器中搜索到了,此时类加载就结束了,就会进入后续流程;若是没有找到,就会继续向子类寻找,由于ApplicationClassLoader是底层了,就表示类加载失败了.

这一系列的列加载机制,目的是为了保证这几个类加载器的优先级顺序.

这个类加载器是系统默认的类加载机制,也可以自己实现类加载机制,可以与默认机制不同.

四.JVM的垃圾回收机制(GC)

垃圾回收指的是让程序自动回收内存,JVM中的内存分为好几种,要回收的是堆区的内存;

元数据区和程序计数区的内存不需要回收,栈区中存放的都是局部变量申请的内存,在代码结束后,会自动销毁(属于栈区自己的特点,和垃圾回收没有关系)。

回收内存其实就是回收对象,垃圾回收时,将堆区上的若干个对象释放掉。

堆区内存根据垃圾回收,又分为三类区间:

 垃圾回收步骤:

1.识别出垃圾

要判定哪些对象是垃圾,哪些对象不是垃圾。就是判断该对象是否还需要使用。

在java中,使用对象,一定是通过引用指向使用对象的方式使用,若该对象没有引用指向,则表示该对象不再被使用,就可以进行垃圾回收了。

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

这个代码中,执行结束后,t属于局部变量,存在于栈区,会被直接释放掉,Test对象在执行完后,由于没有对象指向了,也就属于垃圾了,就会被垃圾回收。

对于一些更复杂的代码,判定过程也就更加复杂。

Test t1 = new Test();
Test t2 = t1;
Test t3 = t2;
Test t4=t3;
....

很多引用都指向了同一个对象Test,只有当所用的引用都结束了,才能释放Test对象,但每个引用的生命周期又不一样,就很难判断了。

于是又设计一些方法来记录对象的引用:

1)引用计数

给每个对象再分配一个额外的空间,保存当前对象引用个数,当有一个引用指向了该对象,引用计数就+1,一个引用结束后,引用计数就-1.

此时的垃圾回收机制就是:有一个专门的扫描线程,取获取每个对象的引用计数的情况,当引用计数为0时,就表示该对象没有引用指向了,不再使用了,也就可以释放了。

class Test {....
}
void func() {
Test t1 = new Test();
Test t2 = t1;
}

这个代码的内存分配:

引用计数 存在的问题:

1)耗费额外的空间

引用计数需要耗费一个额外的空间,若对象本身占用的内存就比较小,总的对象数目有很多,那么总的消耗空间就会非常多。

2)可能出现“循环引用问题”:

class Test{Test t;
}
Test t1 = new Test();
Test t2 = new Test();
t1.t = t2;
t2.t = t1;
t1 = null;
t2 = null;

当t1和t2还未被置为null的时候,此时的内存是这样的情况:

 当t1和t2都被置为null后,t1,t2内存被释放,但Test对象中的t还未被释放:

此时,Test的引用计数还都不是0,不能被GC回收,但又无法使用,就产生了循环引用问题,这种情况下的引用计数就无法被正常使用了。

引用计数 这种思想 并未在java中使用,在别的语言的垃圾回收机制中有使用到。

2)可达性分析

(JVM的垃圾回收机制 识别垃圾 采用的是这种思想)

可达性分析本质上是采用“时间”换“空间”的方法。

相较于 引用计数,可达性分析要消耗更多的时间去“遍历”,不会存在上面 引用计数 中的问题。

可达性分析:一个java代码中,会定义很多变量,从这些变量为起点,向下“遍历”:从这些变量中持有的引用类型的成员,再向下遍历,所有能被访问到的对象,一定不是垃圾了,而未被访问到的对象,就是垃圾了,要被就行回收。

JVM自身有扫描线程,会不停地扫描代码,看是否有对象无法被遍历到;JVM本身是知道一共有多少个对象的。

class Node{char root;Node left;Node right;
}
Node BuildNode{
Node a = new Node();
Node b = new Node();
Node c = new Node();
Node d = new Node();
Node e = new Node();
Node f = new Node();
Node g = new Node();
a.left = b;
a.right = c;
b.left = d;
b.right = e;
c.right = f;
e.left = g;
}
public static void main(String args[]){
Node root = BuildNode();
}

代码中的树是这个样子,

在这个代码中,虽然只有一个root这样的引用,但实际上有7个对象都是可达的,

若代码中出现: c.right=null;此时f就是不可达的,f就属于垃圾了.要进行回收.

若a=null;那么整个二叉树都是不可达的了.都要进行垃圾回收.

2.把标记为垃圾的对象的内存空间进行释放

具体的释放方法有三种.

1)标记-清除:

把标记为垃圾的对象,直接进行释放。(最直接的方法)

这种做法可能会产生大量的“内存碎片”,会存在很多小的,离散的可用空间。

可能导致后续申请内存空间失败,申请内存空间都是一次申请一个连续的内存空间,此时可能内存中总得空间是够当前要申请的内存空间的,但内有连续的内存空间够分配,就可能申请失败。

2)复制算法

先把申请的内存分成两部分,申请内存时都在一半的内存中创建;进行释放时,把不是垃圾的对象的内存复制到另一半内存中,然后把带有垃圾的半个内存全部释放掉。

将不是垃圾的对象都复制到另半个内存中:

再把左半部分的内存中的对象都释放掉

这个方法也存在一些问题:

1、每次释放内存,要释放一半的内存,总的可用内存减少了很多。

2、若引用的对象很多,对对象的复制也要消费很大的开销。

3)标记整理

类似于顺序表中,删除元素的方法。

遍历整个内存,若有遍历到的对象标记为垃圾,不用管,后面遍历到不是垃圾的对象内存就覆盖垃圾的内存空间,这样既不会存在“内存碎片”,又不会一次释放很多的内存。

这个方法的缺点是搬运内存会有很大的开销。

上面的方法都有一定的缺点和问题,因此,JVM并没有直接使用上面的方法,而是对上面的方法思想,采用了一个·“综合性”方案:“分带回收”。

分带回收

依据不同种类的对象,采用不同的回收方式。

JVM引入了一个概念:年龄。

JVM的扫描线程会不断的扫描内存,若该对象是可达的,年龄就+1;

JVM根据对象年龄的不同,将内存分为两个区域:新生代 和 老年代。

新生代中又划分了三个大小不等的区域:其中一个大的区域叫伊甸区 和 两个小的等大的生存区(幸存区)。

回收过程:

1.当创建出一个对象后,该对象会先被创建到伊甸区,(伊甸区的对象大多都被第一轮GC扫描到了,就会被回收掉)

2.第一轮GC后,少数存活的对象通过复制算法被送到其中一个生存区,扫描还在继续,生存区中被标记的对象就会被清除掉,极少数的生存区的对象会再次通过复制算法,从一个生存区复制到另一个生存区,这样循环扫描复制,每经过一轮GC的扫描,年龄就会+1.

3.当这个对象在生存区经过了若干轮扫描,年龄已经很大了,说明这个对象的生命周期可能很长,就将这个对象拷贝到老年代,老年代中的对象经过GC扫描的频率要比新生代低很多。

4.当扫描老年代中的对象,也被标记为垃圾了,也会进行释放。

这个分带回收就类似于找工作一样:

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

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

相关文章

【01】共识机制

BTF共识 拜占庭将军问题 拜占庭将军问题是一个共识问题 起源 Leslie Lamport在论文《The Byzantine Generals Problem》提出拜占庭将军问题。 核心描述 军中可能有叛徒,却要保证进攻一致,由此引申到计算领域,发展成了一种容错理论。随着…

AI大模型开发原理篇-1:语言模型雏形之N-Gram模型

N-Gram模型概念 N-Gram模型是一种基于统计的语言模型,用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说,N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意:这…

【汽车电子软件架构】AutoSAR从放弃到入门专栏导读

本文是汽车电子软件架构:AutoSAR从放弃到入门专栏的导读篇。文章延续专栏文章的一贯作风,从概念与定义入手,希望读者能对AutoSAR架构有一个整体的认识,然后对专栏涉及的文章进行分类与链接。本文首先从AutoSAR汽车软件架构的概念&…

python-UnitTest框架笔记

UnitTest框架的基本使用方法 UnitTest框架介绍 框架:framework,为了解决一类事情的功能集合 UnitTest框架:是python自带的单元测试框架 自带的,可以直接使用,不需要格外安装 测试人员用来做自动化测试,作…

【数据结构】_链表经典算法OJ:复杂链表的复制

目录 1. 题目链接及描述 2. 解题思路 3. 程序 1. 题目链接及描述 题目链接:138. 随机链表的复制 - 力扣(LeetCode) 题目描述: 给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,…

Linux——进程间通信之SystemV共享内存

前言 SystemV通信一般包括三种:共享内存、消息队列和信号量。共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来…

Linux网络 | 网络层IP报文解析、认识网段划分与IP地址

前言:本节内容为网络层。 主要讲解IP协议报文字段以及分离有效载荷。 另外, 本节也会带领友友认识一下IP地址的划分。 那么现在废话不多说, 开始我们的学习吧!! ps:本节正式进入网络层喽, 友友们…

SQLGlot:用SQLGlot解析SQL

几十年来,结构化查询语言(SQL)一直是与数据库交互的实际语言。在一段时间内,不同的数据库在支持通用SQL语法的同时演变出了不同的SQL风格,也就是方言。这可能是SQL被广泛采用和流行的原因之一。 SQL解析是解构SQL查询…

Windows程序设计10:文件指针及目录的创建与删除

文章目录 前言一、文件指针是什么?二、设置文件指针的位置:随机读写,SetFilePointer函数1.函数说明2.函数实例 三、 目录的创建CreateDirectory四、目录的删除RemoveDirectory总结 前言 Windows程序设计10:文件指针及目录的创建与…

线程互斥同步

前言: 简单回顾一下上文所学,上文我们最重要核心的工作就是介绍了我们线程自己的LWP和tid究竟是个什么,总结一句话,就是tid是用户视角下所认为的概念,因为在Linux系统中,从来没有线程这一说法,…

DRM系列七:Drm之CREATE_DUMB

本系列文章基于linux 5.15 DRM驱动的显存由GEM(Graphics execution management)管理。 一、创建流程 创建buf时,user层提供需要buf的width,height以及bpp(bite per pixel),然后调用drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &…

我们信仰AI?从神明到人工智能——信任的进化

信任的进化: 信任是我们最宝贵的资产。而现在,它正像黑色星期五促销的廉价平板电视一样,被一点点拆解。在过去,世界很简单:人们相信晚间新闻、那些满是灰尘书籍的教授,或者手持病历、眉头紧锁的医生。而如…

数据分析系列--[11] RapidMiner,K-Means聚类分析(含数据集)

一、数据集 二、导入数据 三、K-Means聚类 数据说明:提供一组数据,含体重、胆固醇、性别。 分析目标:找到这组数据中需要治疗的群体供后续使用。 一、数据集 点击下载数据集 二、导入数据 三、K-Means聚类 Ending, congratulations, youre done.

1-刷力扣问题记录

25.1.19 1.size()和.length()有什么区别 2.result.push_back({nums[i], nums[left], nums[right]});为什么用大括号? 使用大括号 {} 是 C11 引入的 初始化列表 语法,它允许我们在构造或初始化对象时直接传入一组值。大括号的使用在许多情况下都能让代码…

神经网络参数量和运算量的计算- 基于deepspeed库和thop库函数

引言 最近需要对神经网络的参数量和运算量进行统计。找到一个基于deepspeed库函数计算参数量和运算量的例子。而我之前一直用thop库函数来计算。 看到有一篇勘误博文写道使用thops库得到的运算量是MACs (Multiply ACcumulate operations,乘加累积操作次数&#xf…

读书笔记--分布式架构的异步化和缓存技术原理及应用场景

本篇是在上一篇的基础上,主要对分布式应用架构下的异步化机制和缓存技术进行学习,主要记录和思考如下,供大家学习参考。大家知道原来传统的单一WAR应用中,由于所有数据都在同一个数据库中,因此事务问题一般借助数据库事…

无用知识研究:std::initializer_list的秘密

先说结论,用std::initializer_list初始化vector,内部逻辑是先生成了一个临时数组,进行了拷贝构造,然后用这个数组的起终指针初始化initializer_list。然后再用initializer_list对vector进行初始化,这个动作又触发了拷贝…

Jupyterlab和notebook修改文件的默认存放路径的方法

文章目录 1.缘由2.操作流程2.1找到默认的路径2.2创建配置文件2.3修改配置文件内容2.4注意事项 1.缘由 我自己使用jupyterlab的时候,打开是在这个浏览器上面打开的,但是这个打开的文件路径显示的是C盘上面路径,所以这个就很麻烦,因…

HarmonyOS:ArkWeb进程

ArkWeb是多进程模型,分为应用进程、Web渲染进程、Web GPU进程、Web孵化进程和Foundation进程。 说明 Web内核没有明确的内存大小申请约束,理论上可以无限大,直到被资源管理释放。 ArkWeb进程模型图 应用进程中Web相关线程(应用唯一) 应用进程为主进程。包含网络线程、Vi…

基于Spring Security 6的OAuth2 系列之九 - 授权服务器--token的获取

之所以想写这一系列,是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级…