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虚拟机。

虚拟机是指通过软件模拟的具有完整硬件功能的、运⾏在⼀个完全隔离的环境中的完整计算机系统。常⻅的虚拟机:JVM、VMwave、Virtual Box。

JVM 和其他两个虚拟机的区别:

1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;

2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进⾏了裁剪。

JVM 是⼀台被定制过的现实当中不存在的计算机。

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.当扫描老年代中的对象,也被标记为垃圾了,也会进行释放。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【多线程】线程池核心数到底如何配置?

🥰🥰🥰来都来了,不妨点个关注叭! 👉博客主页:欢迎各位大佬!👈 文章目录 1. 前置回顾2. 动态线程池2.1 JMX 的介绍2.1.1 MBeans 介绍 2.2 使用 JMX jconsole 实现动态修改线程池2.2.…

js-对象-JSON

JavaScript自定义对象 JSON 概念: JavaScript Object Notation,JavaScript对象标记法. JSON 是通过JavaScript 对象标记法书写的文本。 由于其语法简单,层次结构鲜明,现多用于作为数据载体,在网络中进行数据传输. json中属性名(k…

基于 SpringBoot3 的 SpringSecurity6 + OAuth2 自定义框架模板

🔖Gitee 项目地址: 基于SpringBoot3的 SpringSecurity6 OAuth2 自定义框架https://gitee.com/MIMIDeK/MySpringSecurityhttps://gitee.com/MIMIDeK/MySpringSecurityhttps://gitee.com/MIMIDeK/MySpringSecurity

大模型综述一镜到底(全文八万字) ——《Large Language Models: A Survey》

论文链接:https://arxiv.org/abs/2402.06196 摘要:自2022年11月ChatGPT发布以来,大语言模型(LLMs)因其在广泛的自然语言任务上的强大性能而备受关注。正如缩放定律所预测的那样,大语言模型通过在大量文本数…

Django视图与URLs路由详解

在Django Web框架中,视图(Views)和URLs路由(URL routing)是Web应用开发的核心概念。它们共同负责将用户的请求映射到相应的Python函数,并返回适当的响应。本篇博客将深入探讨Django的视图和URLs路由系统&am…

位置-速度双闭环PID控制详解与C语言实现

目录 概述 1 控制架构解析 1.1 级联控制结构 1.2 性能对比 2 数学模型 2.1 位置环(外环) 2.2 速度环(内环) 3 C语言完整实现 3.1 控制结构体定义 3.2 初始化函数 3.3 双环计算函数 4 参数整定指南 4.1 整定步骤 4.2 典型参数范围 5 关键优化技术 5.1 速度前馈 …

亚博microros小车-原生ubuntu支持系列:22 物体识别追踪

背景知识 跟上一个颜色追踪类似。也是基于opencv的,不过背后的算法有很多 BOOSTING:算法原理类似于Haar cascades (AdaBoost),是一种很老的算法。这个算法速度慢并且不是很准。MIL:比BOOSTING准一点。KCF:速度比BOOST…

低至3折,百度智能云千帆宣布全面支持DeepSeek-R1/V3调用

DeepSeek-R1和 DeepSeek-V3模型已在百度智能云千帆平台上架 。 出品|产业家 新年伊始,百度智能云又传来新动作 。 2月3日百度智能云宣布, DeepSeek-R1和 DeepSeek-V3模型已在百度智能云千帆平台上架,同步推出超低价格方案,并…

Deepseek技术浅析(四):专家选择与推理机制

DeepSeek 是一种基于**专家混合模型(Mixture of Experts, MoE)**的先进深度学习架构,旨在通过动态选择和组合多个专家网络(Expert Networks)来处理复杂的任务。其核心思想是根据输入数据的特征,动态激活最合…

go运算符

内置运算符 算术运算符关系运算符逻辑运算符位运算符赋值运算符 算术运算符 注意: (自增)和–(自减)在 Go 语言中是单独的语句,并不是运算符 package mainimport "fmt"func main() {fmt.Printl…

分享2款 .NET 开源且强大的翻译工具

前言 对于程序员而言永远都无法逃避和英文打交道,今天大姚给大家分享2款 .NET 开源、功能强大的翻译工具,希望可以帮助到有需要的同学。 STranslate STranslate是一款由WPF开源的、免费的(MIT License)、即开即用、即用即走的翻…

技术书籍写作与编辑沟通指南

引言 撰写技术书籍不仅仅是知识的输出过程,更是与编辑团队紧密合作的协同工作。优秀的技术书籍不仅依赖作者深厚的技术背景,还需要精准的表达、流畅的结构以及符合出版要求的编辑润色。因此,如何高效地与编辑沟通,确保书籍质量&a…

Linux中系统相关指令(一)

一、时间查看指令date 1.1时间显示的格式 1> 默认格式,直接输入: date 回车 会直接展示出来,如: 2> 常用格式:年-月-日 时:分:秒 这种格式更加贴近于我们的习惯,但需要…

C语言:深入了解指针3

1.回调函数是什么? 基本概念 回调函数就是⼀个通过函数指针调⽤的函数。 如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数 时,被调⽤的函数就是回调函数。回调函数不是由该函…

【Uniapp-Vue3】创建DB schema数据表结构

右键uniCloud文件下的database文件,点击“新建DB schema”,选择模板,修改文件名,点击“创建” 创建完成后会出现对应的文件,进入该文件进行配置 对文件中的必填选项,用户权限,字段进行配置 其…

Java基础进阶-水仙花数

/* 功能:求水仙花数,打印并统计总个数。 思路: 水仙花数是定义范围100-999,满足每个位上的数子的3次方相加和等于这个数 第一步:循环遍历数据范围 第二步;取出当前数字的个位,十位,百…

DDD - 领域事件_解耦微服务的关键

文章目录 Pre领域事件的核心概念领域事件的作用领域事件的识别领域事件的技术实现领域事件的运行机制案例领域事件驱动的优势 Pre DDD - 微服务设计与领域驱动设计实战(中)_ 解决微服务拆分难题 EDA - Spring Boot构建基于事件驱动的消息系统 领域事件的核心概念 领域事件&a…

MacBook Pro(M1芯片)Qt环境配置

MacBook Pro(M1芯片)Qt环境配置 1、准备 试图写一个跨平台的桌面应用,此时想到了使用Qt,于是开始了搭建开发环境~ 在M1芯片的电脑上安装,使用brew工具比较方便 Apple Silicon(ARM/M1&#xf…

简单本地部署deepseek(软件版)

Download Ollama on Windows 下载 下载安装 winr 输入 cmd 然后输入ollama -v,出现ollama版本,安装成功 deepseek-r1 选择1.5b 输入 cmd 下面代码 ollama run deepseek-r1:1.5b 删除deepseek的代码如下: ollama rm deepseek-r1:1.5b 使用…

Linux生成自签证书【Nginx】

👨‍🎓博主简介 🏅CSDN博客专家   🏅云计算领域优质创作者   🏅华为云开发者社区专家博主   🏅阿里云开发者社区专家博主 💊交流社区:运维交流社区 欢迎大家的加入&#xff01…