“深入JVM内部:揭秘Java程序运行的神秘黑盒“(二)

news2024/11/15 8:24:26

 一.双亲委派模型(在加载环节) 

简单描述了如何查找 .class 文件的策略.

概念:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。扩展类加载

器。加载 lib/ext 目录下的类。

应用程序类加载器:加载我们写的应用程序。

自定义类加载器:根据自己的需求定制类加载器。

JVM 中进行类加载的操作,是有一个专门的模块,称为“类加载器"(ClassLoader)

JVM 中的类加载器默认是有 三个 的.(也可以自定义)

类加载器的作用:(问百度文心)

上述的三个类加载器,存在"父子关系”(不是 面向对象中的父类,子类继承关系)
而是类似于“二叉树”,有一个指针(引用) parent, 指向自己的“父”类加载器。

双亲委派模型的工作过程

1.从 ApplicationClassLoader 作为入口,先开始工作.


2.ApplicationClassLoader 不会立即搜索自己负责的目录,会把搜索的任务交给自己的父亲

3.代码就进入到 ExtensionClassLoader 范畴了,ExtensionClassLoader 也不会立即搜索自己负责的目录,也要把搜索的任务交给自己的父亲


4.代码就进入到 BootstrapClassLoader 范畴了,BootstrapClassLoader 也不想立即搜索自己负责的目录也要把搜索的任务交给自己的父亲


5.BootstrapClassLoader 发现自己没有父亲,才会真正搜索负责的目录(标准库目录)
通过全限定类名,尝试在标准库目录中找到符合要求的 .class 文件

6.ExtensionClassLoader 收到父亲交回给他的任务之后。自己进行搜索负责目录(扩展库的目录)

如果找到了,接下来进入到后续流程。
如果没找到,也是回到孩子这一辈的类加载器中继续尝试加载


7.ApplicationClassLoader 收到父亲交回给他的任务之后,自己进行搜索负责的目录 (当前项目目录/第三方库目录),如果找到了,接下来进入后续流程。

如果没找到,也是回到孩子这一辈的类加载器中继续尝试加载
由于默认情况下 ApplicationClassLoader 没有孩子了,
此时说明类加载过程失败了! 就会抛出 ClassNotFoundException 异常

优点:

1.避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。

2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模

型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object

类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户

自己提供的因此安全性就不能得到保证了。

补充:

上述这一系列规则,只是JVM 自带的类加载器,遵守的默认规则如果咱们自己写类加载器,也可以打破上述规则。
比如,自己写类加载器,指定这个加载器就在某个目录中尝试加载.此时如果类加载器的 parent 不去和已有的这些类加载器连到一起,此时就是独立的,不涉及到双亲委派了。


二.垃圾回收机制(GC)

举个栗子~

能否让释放内存的操作,由程序自动负责完成?而不是依赖程序猿手工释放呢?

Java 就属于早期就支持垃圾回收这样的语言。

引入这样的机制之后,就不需要靠手动来进行释放了
程序会自动判定,某个内存是否会继续使用。
如果内存后续不用了,就会自动释放掉 

垃圾回收中的一个很重要的问题: STW (stop the world) 问题:触发垃圾回收的时候,很可能会使当前程序的其他的业务逻辑被暂停。
Java 发展这么多年,GC 这块的技术积累也越来越强大,有办法把 STW 的时间控制到 1ms 之内
一个服务器请求/响应 处理时间,典型的时间 几毫秒 - 几十毫秒。

 垃圾回收,具体步骤:

2.1识别垃圾 

(1)识别垃圾,哪些对象是垃圾(不再使用),哪些对象不是垃圾

识别出垃圾:判定你这个对象后是否继续要使用。

即在 Java 中使用对象,一定需要通过引用的方式来使用.(当然,有一个例外,匿名对象)

匿名对象类似如下:

如果一个对象没有任何引用指向他,就视为是无法被代码中使用,就可以作为垃圾了。

举个栗子~

void fun(){
  {
    Test t =  new Test();
    t.xxx();//调用xxx方法
  }

如上,通过new Test()在堆上创建了对象。

执行到这个之后,此时局部变量 t 就直接被释放了
此时再进一步,上述 new Test 对象,也就没有引用指向他了
此时,这个代码就无法访问使用这个对象,这个对象就是垃圾了

 如果代码更复杂些,情况又不一样了

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

此时就会有很多引用指向 new Test 同一个对象(此时有很多引用,都保存了 Test 对象的地址)
此时通过任意的引用都能访问 Test 对象需要确保所有的指 Test 对象的引用都销毁了,才能把 Test 对象视为垃圾.
如果代码比较复杂,上述这些引用的生命周期各不相同的,此时情况就不好办.

我们用两种方法去处理


1.引用计数

概念:给每个对象安排一个额外的空间,空间里要保存当前这个对象有几个引用。

引用计数这种思想方法,并没有在JVM 中使用,但是广泛应用于其他主流语言的垃圾回收机制中.(Python,PHP)

举一个栗子~

 根据以上过程:

此时垃圾回收机制(有专门的扫描线程,去获取到当前每个对象的引用计数的情况)
发现对象的引用计数为 0,说明这个对象就可以释放了(就是垃圾了)。

引用计数机制,是一个简单有效的机制,存在两个关键的问题

问题一:消耗额外的内存空间。

要给每个对象都安排一个计数器.(如果计数器按照 2 个字节算
如果整个程序中对象数目很多,总的消耗的空间也会非常多
尤其是如果每个对象体积比较小(假设每个对象 4个字节)
计数器消耗的空间,已经达到对象的空间的一半

问题二: 引用计数可能会产生“循环引用的问题”此时,引用计数就无法正确工作了。

例如:

 2.可达性分析

举个栗子~

class Node {
char val;
Node left;
Node right;
}


Node buildTree() {
    Node a = new Node0;
    Node b new Node0:
    Node c = new Node();
    Node d = new Node0;
    Node e = new Node()
    Node f = new Node();
    Node g = new Node();
    a.left = b;
    a.right = c;
    b.left = d;
    bright = e;
    e.left = g;
    c.right = f;
    return a;//返回根节点
}
Node root = buildTree ();

 根据代码画出二叉树的图。

 虽然这个代码中,只有一个 root 这样的引用了,但是实际上上述 7 个节点对象都是“可达的“。

JVM 中存在扫描线程,会不停的尝试对代码中已有的这些变量进行遍历,尽可能多的去访问到对象。

上述的代码,如果执行这个代码;

root.right.right = null;

出现断开之后,此时 f 这个对象就被"孤立"了.按照上述从 root 出发进行遍历的操作
就也无法访问到 f了,f 这个节点对象就称为"不可达"。此时的f就作为垃圾。

上述的代码,如果执行这个代码;

root.right = null;

此时 c 就不可达了,由于 f的访问必须通过 c,c 不可达也就会导致 f不可达,那 c 和 f 都是垃圾了 。

2.2处理垃圾

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

主要释放的方式有三种。

1.标记-清除

把标记为垃圾的对象,直接释放掉.(最朴素的做法)。

黑色部分当作垃圾回收了。

此时就是把标记为垃圾的对象对应的内存空间直接释放。
但是会有比较致命的问题。上述释放方式,就可能会产生很多的小的内存碎片!!

内存碎片:离散的空闲内存空间

因为内存申请,都是一次申请一个连续的内存空间,就可能会导致后续申请内存失败。
比如申请 1M 内存空间,此时,1M 字节都是连续的。
如果存在很多内存碎片,就可能导致总的空闲空间,远远超过 1MB,但是并不存在比 1MB 大的连续的空间。

此时,去申请内存空间就会失败!!

2.复制算法

复制算法,核心就是不直接释放内存,而是把不是垃圾的对象复制到内存的另一半里
接下来就把另外一侧空间整体释放掉。

 确实能够规避内存碎片问题,但是也有缺点.
1.总的可用内存变少了。比如买两个饼果子,吃一个丢一个
2.如果每次要复制的对象比较多,此时复制开销也就很大了。
需要是当前这一轮 GC 的过程中,大部分对象都释放,少数对象存活,这个时候适合使用复制。

3.标记-整理

用法类似与顺序表删除中间元素。

下面是一个例子~,打红色x部分是要回收的垃圾

通过这个过程,也能有效解决内存碎片问题
并且这个过程也不像复制算法一样,需要浪费过多的内存空间
但是,这里的搬运内存开销很大。

因此,JVM 中没有直接使用上述的方案,而是结合上述思想,搞出了一个“综合性”方案,取长补短。

4.分代回收

分代回收:(依据不同种类的对象,采取不同的方式)

先引入一个概念:对象的年龄。

JVM 中有专门的线程负责周期性扫描/释放。
一个对象,如果被线程扫描了一次,可达了(不是垃圾),年龄就 + 1 (初始年龄相当于是 0)
JVM 中就会根据对象年龄的差异, 把整个堆内存分成两个大的部分
新生代(年龄小的对象)/ 老年代(年龄大的对象)。

如下图

1.当代码中 new 出个新的对象,这个对象就是被创建在伊甸区的,伊甸区中就会有很多的对象
个经验规律,伊甸区中的对象,大部分是活不过第一轮 GC,这些对象都是“朝生夕死”,生命周期非常短!!


2.第一轮 GC 扫描完成之后,少数伊甸区中幸存的对象,就会通过复制算法,拷贝到 生存区
后续 GC 的扫描线程还会持续进行扫描.不仅要扫描伊区,也要扫描生存区的对象
生存区中的大部分对象也会在扫描中被标记为垃圾,少数存活的,就会继续使用复制算法,拷贝到另外一个生存区中!!只要这个对象能够在生存区中继续存活,就会被复制算法继续拷贝到另一半的生存区中.每次经历一轮 GC的扫描,对象的年龄都会 + 1


3.如果这个对象在生存区中,经过了若干轮 GC 仍然健在
JVM 就会认为,这个对象生命周期大概率很长,就把这个对象从生存区,拷贝到老年代


4.老年代的对象,当然也要被 GC 扫描但是扫描的频次就会大大降低了
老年代的对象,要 G 早 G 了~~ 既然没 G 说明生命周期应该是很长的
频繁 GC 扫描意义也不大,白白浪费时间.不如放到老年代,降低扫描频率


5.对象在老年代寿终正寝,此时JVM 就会按照标记整理的方式释放内存。

上述分代回收是JVM GC 中的核心思想
但是JVM 实际的 垃圾回收 的实现细节上还会存在一些变数和优化。


三.垃圾收集器

如果说上面我们讲的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。

以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:

上图展示了7种作用于不同分代的收集

之间可以搭配使用。所处的区域,表示它是属于新生代收集器还是老年代收集器。在讲具体的收集器之前我们先来明确三个概念:

并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态

并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程

序继续运行,而垃圾收集程序在另外一个CPU上。

吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

为什么会有这么多垃圾收集器?

自从有了 Java 语言就有了垃圾收集器,这么多垃圾收集器其实是历史发展的产物。最早的垃圾收集器 为 Serial,也就是串行执行的垃圾收集器,Serial Old 为串行的老年代收集器,而随着时间的发展,为 了提升更高的性能,于是有了 Serial 多线程版的垃圾收集器 ParNew。后来人们想要更高吞吐量 的垃圾收集器,吞吐量是指单位时间内成功回收垃圾的数量,于是就有了吞吐量优先的垃圾收集器 Parallel Scavenge(吞吐量优先的新生代垃圾收集器)和 Parallel Old(吞吐量优先的老年代垃圾收集器)。随着技术的发展后来又有了 CMS(Concurrent Mark Sweep)垃圾收集器,CMS 可以兼顾吞吐量和以获取最短回收停顿时间为目标的收集器,在 JDK 1.8(包含)之前 BS 系统的主流垃圾收集器,而在 JDK 1.8 之后,出现了第一个既不完全属于新生代也不完全属于老年代的垃圾收集器 G1(Garbage First),G1 提供了基本不需要停止程序就可以收集垃圾的技术。

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

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

相关文章

C++ | Leetcode C++题解之第238题除自身以外数组的乘积

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<int> productExceptSelf(vector<int>& nums) {int length nums.size();// L 和 R 分别表示左右两侧的乘积列表vector<int> L(length, 0), R(length, 0);vector<int> answer(l…

RK3568平台(文件系统篇)Buildroot文件系统

一.Buildroot文件系统概述 Buildroot 是Linux平台上一个开源的嵌入式Linux系统自动构建框架。整个Buildroot是由Makefile脚本和Kconfig配置文件构成的。可通过Buildroot配置&#xff0c;编译出一个完整的可以直接烧写到机器上运行的Linux系统软件。 获取buildroot官方源码&am…

人工智能 (AI) 应用:一个异常肺呼吸声辅助诊断系统

关键词&#xff1a;深度学习、肺癌、多标签、轻量级模型设计、异常肺音、音频分类 近年来&#xff0c;流感对人类的危害不断增加&#xff0c;COVID-19疾病的迅速传播加剧了这一问题&#xff0c;导致大多数患者因呼吸系统异常而死亡。在这次流行病爆发之前&#xff0c;呼吸系统…

【Vue3】4个比较重要的设计模式!!

大家好,我是CodeQi! 一位热衷于技术分享的码仔。 在我投身于前端开发的职业生涯期间,曾有一次承接了一个大型项目的维护工作。此项目运用的是 Vue 框架,然而其代码结构紊乱不堪,可维护性极度糟糕😫。 这使我深刻领会到,理解并运用 Vue 中的重要设计模式是何等关键! …

Codeforces Round 958 (Div. 2)(A~C)题

A. Split the Multiset 思路: 最优的策略是每次操作分出 k−1&#x1d458;−1 个 1&#xff0c;然后考虑最后是否会剩下一个单独的 1。 代码: #include<bits/stdc.h> using namespace std; #define N 1000005 typedef long long ll; typedef unsigned long long ull;…

VGMShield:揭秘视频生成模型滥用的检测与追踪技术

人工智能咨询培训老师叶梓 转载标明出处 视频生成模型&#xff0c;如 Stable Video Diffusion 和 Videocrafter&#xff0c;已经能够生成合理且高分辨率的视频。但这些技术进步也带来了被恶意利用的风险&#xff0c;比如用于制造假新闻或进行政治宣传。因此&#xff0c;来自弗…

BUUCTF逆向wp [HDCTF2019]Maze

第一步 查壳&#xff0c;本题是32位&#xff0c;有壳&#xff0c;进行脱壳。 第二步 这里的 jnz 指令会实现一个跳转&#xff0c;并且下面的0EC85D78Bh被标红了&#xff0c;应该是一个不存在的地址&#xff0c;这些东西就会导致IDA无法正常反汇编出原始代码&#xff0c;也称…

Kafka 高并发设计之数据压缩与批量消息处理

《Kafka 高性能架构设计 7 大秘诀》专栏第 6 章。 压缩&#xff0c;是一种用时间换空间的 trade-off 思想&#xff0c;用 CPU 的时间去换磁盘或者网络 I/O 传输量&#xff0c;用较小的 CPU 开销来换取更具性价比的磁盘占用和更少的网络 I/O 传输。 Kafka 是一个高吞吐量、可扩展…

python课设——宾馆管理系统

python课设——宾馆管理系统 数据库课设-宾馆管理系统-python3.7pyqt5 简介 大二数据库课程设计&#xff08;3-4天工作量&#xff09;的项目&#xff0c;登录界面的ui设计参考了他人成果&#xff0c;其余ui以及所有后端部分全部独立完成&#xff0c;详细功能见功能模块图使用…

国内新能源汽车芯片自给,承认差距,任重道远

【科技明说 &#xff5c; 科技热点关注】 据近日工信部电子五所元器件与材料研究院高级副院长罗道军表示&#xff0c;中国拥有最大的新能源车产能&#xff0c;芯片用量也是越来越多。但是芯片的自给率目前不到10%&#xff0c;是结构性的短缺。 中国拥有最大新能源车产能&#…

入门【消息队列】这一篇就够了

消息队列 消息队列的模型为什么要用消息队列分布式消息队列应用场景分布式消息队列选型RabbitMQ入门实战singleWorkFanoutDirectTopic核心特性消息过期机制消息确认机制死信队列消息队列的模型 生产者:Producer,发送消息的人(客户端) 消费者:Consumer,接受消息的人(客…

NLCISBNPlugin,从“中国国家图书馆”获取图书信息Calibre插件

NLCISBNPlugin可以从中国国家图书馆获取图书信息&#xff0c;包括 ISBN、书名、作者、出版日期等信息&#xff0c;然后将这些信息添加Calibre中。 插件安装:NLCISBNPlugin.zip 安装说明&#xff1a; 在 Calibre官方网站 上下载并安装Calibre。下载最新版本的 NLCISBNPlugin …

JUC 包中的 Atomic 原子类总结

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

RFID涉密载体管控系统|DW-S402功能介绍

文件载体管控系统DW-S402是用于对各种载体进行有效管理的智能柜&#xff08;智能管理系统&#xff09;&#xff0c;实现对载体的智能化、规范化、标准化管理&#xff0c;广泛应用于保密、机要单位以及企事业单位等有载体保管需求的行业。 区域监控管理 主要是通过在需要监控的…

el-table表格操作列错行处理

解决方法&#xff1a; <style>::v-deep .el-table th.el-table__cell > .cell {white-space: nowrap !important;} </style>

ArkUI状态管理

State装饰器 在声明式UI中&#xff0c;是以状态驱动试图更新 状态 (State) 指驱动视图更新的数据(被装饰器标记的变量) 试图(View) 基于UI描述渲染得到用户界面 说明 1.State装饰器标记的变量必须初始化&#xff0c;不能为空 2.State支持Object、classstring、number、b…

Self-Attention 自注意力机制(二)——实例过程说明

一、自注意力机制核心过程 自注意力机制&#xff08;Self-Attention Mechanism&#xff09;&#xff0c;也称为内部注意力机制&#xff0c;是一种在序列模型中用于捕捉序列内部不同位置之间依赖关系的技术。这种机制允许模型在处理序列时&#xff0c;对序列中的每个元素分配不…

基于 Web 的家校联系系统的设计与实现

目录 基于 Web 的家校联系系统的设计与实现 一、绪论 &#xff08;一&#xff09;研究背景 &#xff08;二&#xff09; 研究目的 &#xff08;三&#xff09; 研究意义 二、需求分析 &#xff08;一&#xff09; 功能需求 &#xff08;二&#xff09; 性能需求 &#…

【C++】类和对象的基本概念与使用

本文通过面向对象的概念以及通俗易懂的例子介绍面向对象引出类和对象。最后通过与之有相似之处的C语言中的struct一步步引出C中的类的定义方式&#xff0c;并提出了一些注意事项&#xff0c;最后描述了类的大小的计算方法。 一、什么是面向对象&#xff1f; 1.面向对象的概念 …

解决一下git clone失败的问题

1&#xff09;.不开梯子&#xff0c;我们用https克隆 git clone https://github.com 报错&#xff1a; Failed to connect to github.com port 443 after 2091 ms: Couldnt connect to server 解决办法&#xff1a; 开梯子&#xff0c;然后# 注意修改成自己的IP和端口号 gi…