深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制

我的主页:2的n次方_     

在这里插入图片描述

1. JVM 内存区域划分

  1. 程序计数器(空间比较小)。保存了下一条要执行的指令的地址(指向元数据区指令的地址)
  2. 堆。JVM 最大的空间,new 出来的对象都在堆上
  3. 栈。函数中的局部变量,函数的形参,函数之间的调用关系都存放在栈上。栈也可以分为 Java 虚拟机栈和本地方法栈,Java 虚拟机栈主要服务于 Java 方法的执行,而本地方法栈主要服务于本地方法(C/C++实现的方法)
  4. 元数据区(方法区)。存放 Java 程序中的指令(指令都是包含在类的方法中),也就是保存了代码中涉及到类的相关信息,同时也保存了类的 static 属性

在一个 Java 进程中,元数据区和堆是只有一份的,程序计数器和栈可能有多份(当一个 Java 进程中有多个线程的时候,每个线程都有自己的程序计数器和栈,因为每个需要记录自己接下来需要执行什么指令还有其中的函数调用关系),由于堆和元数据区是共用一份的,所以如果是在堆上 new 了一个对象,或是元数据区的一个 static 属性,每一个线程都可以访问。

来看下面创建的变量都处在哪个内存区域:

一个变量处在哪个内存区域和变量是不是“内置类型”无关,而是和变量的形态有关

局部变量:栈 成员变量:堆 静态成员变量:元数据区(方法区)

所以说 a,b在堆区 c,d 在方法区,e,f 在元数据区(方法区)

2. JVM 类加载的过程

我们平时写的 Java 代码是一个 .java 文件,通过 javac 编译成 .class 文件,既然是文件,那么都是存储在硬盘上的,当运行 Java 进程的时候,JVM 就需要读取 .class 中的内容,并且执行里面的指令,这就是类加载的过程,也就是把类涉及到的字节码从硬盘上读取到内存中(元数据区), .class 中的指令转变为类对象,类对象中就包含了 .class 文件的各种信息,比如:类的名字,类中定义的属性,方法,实现的接口和继承关系等。

下面介绍一下类加载的具体过程:

2.1. 加载

  1. 加载。找到对应的 .class 文件,通过类的全限定名打开并读取文件内容,获取定义此类的二进制字节流。

2.2. 验证

  1. 验证。验证是确保 Class 文件的字节流中包含的信息符合当前 JVM 的要求,在 Java 标准文档中,也明确定义了 Class 文件的格式

2.3. 准备

  1. 准备。准备阶段是正式为类中被 static 修饰的变量(类变量)在方法区中分配内存空间的,这些类变量在整个类的生命周期中都是有效的,并且被所有的实例对象所共享,根据上面读取到的内容来确定出需要的内存空间,然后申请内存空间,并把空间内所有的内容都初始化为 0 。

例如,对于 public static int value = 10;,在准备阶段,value 会被初始化为 0,而不是 10。真正的赋值 10 是在初始化阶段进行的。

2.4. 解析

  1. 解析。主要是针对类中的字符串常量进行处理,JVM 将常量池中的符号引用替换为直接引用的过程,也就是初始化常量的过程。

符号引用和内存引用:字符串常量已经在 Class 文件中了,这时是没有“地址”这样的概念的,地址是内存的地址,还没有加载到内存中,这时是用“文件偏移量”来表示的,当加载到内存空间之后就会使用地址来标识,也就是直接引用

2.5. 初始化

  1. 初始化:Java 虚拟机真正开始执⾏类中编写的 Java 程序代码,将主导权移交给应⽤程序。初始
    化阶段就是执⾏类构造器⽅法的过程 。例如创建类的实例,调用类的静态方法,访问类或接口的静态变量(不包括 final 修饰的静态变量,在编译阶段就确定了值),初始化子类时,父类会先被初始化,执行静态代码块。

普通的成员变量(非静态变量)不会在 JVM 类加载过程中的初始化阶段被初始化。类加载过程中的初始化阶段主要是针对类变量(被 static 修饰的变量)以及执行静态代码块。普通成员变量是在对象创建时,即通过构造函数或其他实例初始化方法进行初始化的。当使用 new 关键字创建对象时,会为对象在堆内存中分配空间,并对成员变量进行初始化等操作。

2.6. 双亲委派模型

双亲委派模型是 Java 类加载机制中的一种重要设计模式,也就是上述类加载中的第一步

JVM 默认是有三个类加载器的:

这里的父子关系并不是 Java 中的继承关系,而是类似与指向的一个 parent 引用

双亲委派模型工作原理:

当一个应用程序需要加载一个类时,应用程序类加载器首先会将加载请求委派给它的父类加载器扩展类加载器,扩展类加载器再委派给启动类加载器。如果启动类加载器无法加载该类,扩展类加载器会尝试加载。如果扩展类加载器也无法加载,最后应用程序类加载器才会尝试自己去加载。

那为什么不一开始就给启动类加载器来执行而是又绕了一圈?

  1. 避免类的重复加载:由于父类加载器优先加载,相同的类只会被加载一次,避免了类的重复加载和内存浪费。
  2. 保证核心类库的安全性:防止核心类库被恶意篡改(程序员自己写的类和标准库中的类名字一样,直接从启动类加载器加载会覆盖掉标准库的类),确保了 Java 程序的稳定性和安全性。

就类似与公司中的员工,领导和老板,当员工发现问题是需要上报给领导,领导再上报给老板,如果说这个问题比较重要,老板就亲自解决,如果不是那么重要,但也有一定分量,就交给领导解决,如果只是一些小事,就让员工自己解决

其实类加载器可以程序员自定义,自定义的类加载器可以遵守双亲委派模型也可以不遵守。

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

JVM 的垃圾回收主要是在堆内存进行的,这里存储的都是 new 出来的对象,当这些对象没有引用指向的时候,就成为了垃圾,如果放任不管,就会一直占用着内存,最终造成内存泄露

进行垃圾回收可能会触发 “Stop The World”(STW)问题:在执行垃圾回收的某个阶段,为了保证垃圾回收的准确性和一致性,需要暂停所有应用程序线程,使得整个应用程序出现短暂的停顿。

所以说 C++ 这种追求高性能的语言并没有使用垃圾回收机制

接下来看 JVM 中是如何进行垃圾回收的

首先,进行垃圾回收肯定需要先标记出哪些对象是垃圾

那么怎么去找哪个对象是垃圾呢?

3.1. 识别垃圾

方案一:引用计数。

给每一个对象分配一个计数器,来统计有多少引用指向,每增加一个引用,计数器 + 1,每减少一个引用,计数器 - 1,当计数器减为 0,就变成垃圾了

这个方法看似可行,python 等一些语言采用的就是这个,但是还存在一个问题:

无法解决循环引用的问题。例如,对象 A 和对象 B 相互引用,除此之外没有其他地方引用它们,此时它们的引用计数器都不为 0,但实际上它们已经不再被程序使用,应该被回收。

同时,引入计数器,也会消耗掉额外的空间

方案二:可达性分析

以一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,是垃圾对象,也就是在类似图的结构中从多个根节点遍历一遍,如果发现有的对象没有任何路径可以到达,也就是没有引用指向了,那么就是垃圾

可达性分析有效的解决了循环引用的问题,可以作为根节点的可以是方法区中类静态属性引用的对象(static 修饰的变量引用的对象),常量池中引用的对象,方法中的局部变量引用的对象,本地方法栈引用的对象。

可达性分析由于需要每次都遍历一遍对象,所以是比较消耗时间的

3.2. 垃圾回收的方法

  1. 标记 - 清除法。

在上面遍历的时候标记出哪些是垃圾,然后回收所有被标记的对象所占用的空间,这种方法有很大的缺点:由于每次申请的内存都是一块连续的区域,所以会产生内存碎片,回收后的内存不完整,接下来再申请内存的话可能会申请失败

  1. 复制算法。

将堆空间分为两个大小相等的区域,一般称为 From 区和 To 区。当进行垃圾回收时,将 From 区中仍然被引用的对象(也就是需要继续使用的对象)复制到 To 区,然后清空 From 区。下次垃圾回收时,交换 From 区和 To 区的角色。

这种算法缺点也很明显,就是空间利用率低,一大块区域只有一半能用,同时如果存活下来的对象有很多,复制的成本也比较大

  1. 标记 - 整理算法。

标记阶段就是把哪些对象是垃圾标记出来,接下来整理就是把存活的对象移动到另一端,然后回收边界以外的空间

这种方法的缺点就是移动对象的时候比较消耗时间,特别是对象很多的情况下

3.3. 分代收集算法

根据对象的生命周期将堆空间分为新生代和老年代,也就是在可达性分析的过程中,每经过一轮扫描,对象仍然存活(不是垃圾),年龄就 + 1,新生代中的对象生命周期较短,很多对象短时间内就变成垃圾了,老年代中的对象生命周期较长。

新创建的对象首先在伊甸区分配内存,当伊甸区满时,触发一次新生代垃圾回收,将伊甸区和幸存区 1中仍然存活的对象复制到幸存区 2,然后清空伊甸区和幸存区 1。下次垃圾回收时,交换幸存区 1 和幸存区 2 的角色,继续重复上述操作,幸存区 1 到 幸存区 2 二者的转化会反复进行多次

由于新生代对象大多存活时间少,要复制的也比较少,效率并不会有多大影响,同时空间也没有太大浪费,两个幸存区的空间比较小,也不会有太大空间的浪费。

当一个对象在幸存区反复拷打,年龄不断增加,就进入了老年代,老年代的对象生命周期长,如果还采用复制算法的话,要复制的对象就很多,影响效率,所以就可以采用标记 - 整理算法,需要整理的次数也不多。

上述过程就很好的利用了各个算法的优势来结合,通过分代收集算法,提高了垃圾回收的效率

3.4. 垃圾回收器

JVM 中还提供了多种“垃圾回收器”,对上面介绍的分代回收算法进行了进一步的扩充和具体实现

CMS 垃圾回收器

先大概的进行一个初始标记,然后再通过多线程的方式并发的进行重新标记,标记之后再进行并发的清理,通过这样的方法,能和业务线程并发执行,使 STW 的时间尽可能的缩短

G1 垃圾回收器

G1 是把整个堆内存分层很多小块,进行垃圾回收时,不要求一个周期就把所有的垃圾全都回收,而是一轮周期回收一部分,通过限制每一轮 GC 花的时间在可控范围之内,同时划分为多个区域也是为了更好的使用多线程

在这里插入图片描述

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

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

相关文章

linux服务器部署filebeat

# 下载filebeat curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.17.23-linux-x86_64.tar.gz # 解压 tar xzvf filebeat-7.17.23-linux-x86_64.tar.gz# 所在位置(自定义) /opt/filebeat-7.17.23-linux-x86_64/filebeat.ym…

一篇文章快速学会docker容器技术

目录 一、Docker简介及部署方法 1.1Docker简介 1.1.1什么是docker 1.1.2 docker在企业中的应用场景 1.1.3 docker与虚拟化的对比 1.1.4 docker的优势 二 、部署docker 2.1 容器工作方法 2.2 部署第一个容器 2.2.1 配置软件仓库 2.2.2 安装docker-ce并启动服务 2.2.…

C嘎嘎入门篇:类和对象(2)

前言: 上一篇小编讲了类和对象(1),当然,在看这篇文章之前,读者朋友们一定要掌握好前面的基础内容,因为这篇和前面息息相关,废话不多说,下面小编就加快步伐,开…

css的背景background属性

CSS的background属性是一个简写属性,它允许你同时设置元素的多个背景相关的子属性。使用这个属性可以简化代码,使其更加清晰和易于维护。background属性可以设置不同的子属性。 background子属性 定义背景颜色 使用background-color属性 格式&#x…

ARM Process state -- CPSR

Holds PE status and control information. 保存PE状态和控制信息。 N, bit [31] Negative condition flag. Set to bit[31] of the result of the last flag-setting instruction. If the result is regarded as a twos complement signed integer, then N is set to 1 if…

Docker学习和部署ry项目

文章目录 停止Docker重启设置开机自启执行docker ps命令,如果不报错,说明安装启动成功2.然后查看数据卷结果3.查看数据卷详情结果4.查看/var/lib/docker/volumes/html/_data目录可以看到与nginx的html目录内容一样,结果如下:5.进入…

理解:两横三纵 简述

理解:两横三纵 简述 下图:两横三纵-yellow色线分布图 时间:2013年12月提出的城市化战略格局 所谓“两横三纵”, 即以陆桥通道、沿长江通道为两条横轴, 以沿海、京哈京广、包昆通道为三条纵轴, 以主要的城市群地区为…

C#和数据库高级:虚方法

文章目录 一、抽象方法和抽象类中的思考1.1、回顾抽象方法的特点1.2、针对抽象方法问题的引出 二、虚方法的使用步骤2.1、虚方法重写方法的调用2.2、系统自带的虚方法2.3、重写Equals方法2.4、虚方法和抽象方法的比较 三、虚方法和抽象方法的联系3.1、ToString()方法的应用 一、…

SpringBoot技术栈:打造下一代网上租赁系统

第2章 关键技术简介 2.1 Java技术 Java是一种非常常用的编程语言,在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中,Java的身影无处不在,并且拥有旺盛的生命力。Java的跨平台能力十分强大,只需一次编译&#xf…

Rust语言桌面应用开发GTK3 Gtk3-rs Glade

文章目录 GTK-RSGithub官网Rust 教程Rust 环境安装 GTK安装 Gladedemo.glade 文件完整示例 main.rs创建 Rust 项目Cargo.toml 文件main.rs 文件 编译运行GTK主题 GTK-RS gtk-rs 是一个用于在 Rust 编程语言中使用 GTK 图形用户界面工具包的库。GTK 是一个流行的跨平台 GUI 工具…

SqlAlchemy使用教程(七) 异步访问数据库

SqlAlchemy使用教程(一) 原理与环境搭建SqlAlchemy使用教程(二) 入门示例及编程步骤SqlAlchemy使用教程(三) CoreAPI访问与操作数据库详解SqlAlchemy使用教程(四) MetaData 与 SQL Express Language 的使用SqlAlchemy使用教程(五) ORM API 编程入门SqlAlchemy使用教程(六) – O…

CSS文本格式化

通过 CSS 中的文本属性您可以像操作 Word 文档那样定义网页中文本的字符间距、对齐方式、缩进等等,CSS 中常用的文本属性如下所示: text-align:设置文本的水平对齐方式;text-decoration:设置文本的装饰;te…

【拥抱AIGC】通义灵码扩展管理

通义灵码提供了扩展管理,支持自定义指令,满足企业编码场景的扩展诉求。 适用版本 企业标准版、企业专属版 通义灵码管理员、组织内全局管理员(专属版)在通义灵码控制台-扩展管理中,进行自定义指令的管理、查看自定义…

leetcode|刷算法 线段树原理以及模板

线段树出现的题目特征 线段树使用的题目。每次操作都要得到返回结果的。 比如 699. 掉落的方块 - 力扣(LeetCode) 2286. 以组为单位订音乐会的门票 - 力扣(LeetCode) 1845. 座位预约管理系统 - 力扣(LeetCode&#…

【PHP陪玩系统源码】游戏陪玩系统app,陪玩小程序优势

陪玩系统开发运营级别陪玩成品搭建 支持二开源码交付,游戏开黑陪玩系统: 多客陪玩系统,游戏开黑陪玩,线下搭子,开黑陪玩系统 前端uniapp后端php,数据库MySQL 1、长时间的陪玩APP源码开发经验,始终坚持从客户…

Docker镜像命令和容器命令

目录 镜像命令 镜像命名规范 镜像操作命令 DockerHub拉取镜像 利用docker save将nginx镜像导出磁盘,然后再通过load加载回来 总结 容器命令介绍和案例 容器相关命令 案例:创建运行一个Nginx容器 总结 镜像命令 镜像命名规范 镜像名称一般分两…

uniapp框架中实现文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间

前言 uni-file-picker是uniapp中的一个文件选择器组件,用于选择本地文件并返回选择的文件路径或文件信息。该组件支持选择单个文件或多个文件,可以设置文件的类型、大小限制,并且可以进行文件预览。 提示:以下是本篇文章正文内容,下面案例可供参考 uni-file-picker组件具…

了解华为计算产品线,昇腾的业务都有哪些?

🍉 CSDN 叶庭云:https://yetingyun.blog.csdn.net/ 随着 ChatGPT 的现象级爆红,它引领了 AI 大模型时代的深刻变革,进而造成 AI 算力资源日益紧缺。与此同时,中美贸易战的持续也使得 AI 算力国产化适配成为必然趋势。 …

Temporal Dynamic Quantization for Diffusion Models阅读

文章目录 AbstractIntroductionBackgrounds and Related Works2.1 扩散模型2.2 量化2.3 量化感知训练和训练后量化 TemporalDynamic Quantization3.1 量化方法3.2 扩散模型量化的挑战3.3 TDQ模块的实现3.4 工程细节时间步的频率编码TDQ模块的初始化 Experimental SetupResults5…

基于SpringBoot+Vue+MySQL的美食信息推荐系统

系统展示 用户前台界面 管理员后台界面 系统背景 在数字化时代,随着人们对美食文化的热爱与追求不断增长,美食信息推荐系统成为了连接食客与美食之间的重要桥梁。面对海量的美食信息,用户往往难以快速找到符合个人口味和需求的美食。因此&…