JVM 核心知识点总结
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,
15年
工作经验,精通Java编程
,高并发设计
,Springboot和微服务
,熟悉Linux
,ESXI虚拟化
以及云原生Docker和K8s
,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea
文章目录
- JVM 核心知识点总结
- 一、基本概念
- 1.1 OpenJDK
- 1.2 OracleJDK
- 1.3 HotSpot VM
- 二、Java 内存区域
- 2.1 程序计数器
- 2.2 虚拟机栈
- 2.3 本地方法栈
- 2.4 堆
- 2.5 方法区
- 三、对象
- 3.1 对象的创建
- 1. 类加载过程
- 2. 分配内存
- 3. 对象头设置
- 4. 对象初始化
- 3.2 对象的内存布局
- 1. 对象头 (Header)
- 2. 实例数据 (Instance Data)
- 3. 对齐填充 (Padding)
- 3.3 对象的访问定位
- 四、垃圾收集机制
- 1. 引用计数法
- 2. 可达性分析
- 3. 对象引用
- 强引用 (Strongly Reference)
- 软引用 (Soft Reference)
- 弱引用 (Weak Reference)
- 虚引用 (Phantom Reference)
- 4. 对象真正死亡
- 4.3 垃圾收集算法
- 1. 分代收集理论
- 2. 回收类型
- 3. 标记-清除算法
- 4. 标记-复制算法
- 5. 标记-整理算法
- 五、垃圾收集器
- 5.1 Serial 收集器
- 5.2 ParNew 收集器
- 5.3 Parallel Scavenge 收集器
- 5.4 Serial Old 收集器
- 5.5 Paralled Old 收集器
- 5.6 CMS 收集器
- 5.7 Garbage First 收集器
- 5.8 内存分配原则
- 1. 对象优先在 Eden 分配
- 2. 大对象直接进入老年代
- 3. 长期存活的对象将进入老年代
- 4. 动态年龄判断
- 5. 空间担保分配
- 六、类加载机制
- 6.1 类加载时机
- 6.2 类加载过程
- 1. 加载
- 2. 验证
- 3. 准备
- 4. 解析
- 5. 初始化
- 6.3 类加载器
- 6.4 双亲委派模型
- 6.5 模块化下的类加载器
- 七、程序编译
- 7.1 编译器分类
- 7.2 解释器与编译器
- 7.3 分层编译
- 7.4 热点探测
- 八、代码优化
- 8.1 方法内联
- 8.2 逃逸分析
- 8.3 公共子表达式消除
- 8.4 数组边界检查消除
- 小结
JVM 核心知识点总结
JVM 核心知识点总结,包括基本概念、Java 内存区域、对象、垃圾收集机制、类加载机制、字节码、JIT、JVM 调优等。
一、基本概念
1.1 OpenJDK
自 1996 年 JDK 1.0
发布以来,Sun 公司在大版本上发行了 JDK 1.1
、JDK 1.2
、JDK 1.3
、JDK 1.4
、JDK 5
,JDK 6
,这些版本的 JDK 都可以统称为 SunJDK 。
之后在 2006 年的 JavaOne 大会上,Sun 公司宣布将 Java 开源,在随后的一年多里,它陆续将 JDK 的各个部分在 GPL v2(GNU General Public License,version 2)协议下开源,并建立了 OpenJDK 组织来对这些代码进行独立的管理,这就是 OpenJDK 的来源,此时的 OpenJDK 拥有当时 sunJDK 7 的几乎全部代码。
1.2 OracleJDK
在 JDK 7
的开发期间,由于各种原因的影响,Sun 公司市值一路下跌,已无力推进 JDK 7
的开发,于是 JDK 7
的发布一直被推迟。
之后在 2009 年 Sun 公司被 Oracle
公司收购,为解决 JDK 7
长期跳票的问题,Oracle 将 JDK 7
中大部分未能完成的项目推迟到 JDK 8
,并于 2011 年发布了JDK 7
,在这之后由 Oracle 公司正常发行的 JDK 版本就由 SunJDK 改称为 Oracle JDK
。
在 2017 年 JDK 9 发布后,Oracle 公司宣布:以后 JDK 将会在每年的 3 月和 9 月各发布一个大版本,即半年发行一个大版本,目的是为了避免众多功能被捆绑到一个 JDK 版本上而引发的无法交付的风险。
在 JDK 11 发布后,Oracle 同步调整了 JDK 的商业授权,宣布从 JDK 11 起,将以前的商业特性全部开源给 OpenJDK ,这样 OpenJDK 11 和 OracleJDK 11 的代码和功能,在本质上就完全相同了。
同时还宣布以后会发行两个版本的 JDK :
- 一个是在 GPLv2 + CE 协议下由 Oracle 开源的 OpenJDK;
- 一个是在 OTN 协议下正常发行的 OracleJDK。
两者共享大部分源码,在功能上几乎一致。唯一的区别是 Oracle OpenJDK 可以在开发、测试或者生产环境中使用,但只有半年的更新支持;而 OracleJDK 对个人免费,但在生产环境中商用收费,可以有三年时间的更新支持。
目前最新的长期支持的 JDK 是 JDK 21(LTS)
。
1.3 HotSpot VM
它是 Sun/Oracle JDK
和 OpenJDK
中默认的虚拟机,也是目前使用最为广泛的虚拟机。
最初由 Longview Technologies
公司设计发明,该公司在 1997 年被 Sun 公司收购,随后 Sun 公司在 2006 年开源 SunJDK
时也将 HotSpot 虚拟机一并进行了开源。
Oracle
收购 Sun 以后,建立了 HotRockit 项目,并将其收购的另外一家公司(BEA)的 JRockit 虚拟机中的优秀特性集成到 HotSpot 中。
HotSpot
在这个过程里移除掉永久代,并吸收了 JRockit
的 Java Mission Control
监控工具等功能。
到 JDK 8
发行时,采用的就是集两者之长的 HotSpot VM
。
我们可以在自己的电脑上使用 java -version
来获得 JDK 的信息:
二、Java 内存区域
Java 内存区域我们之前讲过,这里再盘一盘。
2.1 程序计数器
程序计数器(Program Counter Register
)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
字节码解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要该计数器来完成。
每个线程都拥有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储。
2.2 虚拟机栈
虚拟机栈(Java Virtual Machine Stack
)也是线程私有,它描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
方法从调用到结束就对应着一个栈帧从入栈到出栈的过程。在《Java 虚拟机规范》中,对该内存区域规定了两类异常:
- 如果线程请求的栈深度大于虚拟机所允许的栈深度,将抛出
StackOverflowError
异常; - 如果 Java 虚拟机栈的容量允许动态扩展,当栈扩展时如果无法申请到足够的内存会抛出
OutOfMemoryError
异常。
2.3 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈类似,其区别在于:Java 虚拟机栈是为虚拟机执行 Java 方法(也就是字节码)服务的,而本地方法栈则是为 JVM 使用到的本地(Native)方法服务。
2.4 堆
堆(Java Heap)是虚拟机所管理的最大一块内存空间,它被所有线程所共享,用于存放对象实例。
Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为是连续的。Java 堆可以被实现成固定大小的,也可以是可扩展的。
当前大多数主流的虚拟机都是按照可扩展来实现的,即可以通过最大值参数 -Xmx
和最小值参数 -Xms
进行设定。
如果 Java 堆中没有足够的内存来完成对象实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError
异常。
2.5 方法区
方法区(Method Area)也是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、JIT 编译后的代码缓存等数据。
方法区也被称为 “非堆”,目的是与 Java 堆进行区分。《Java 虚拟机规范》规定,如果方法区无法满足新的内存分配需求时,将会抛出 OutOfMemoryError
异常。
JDK 8 以后的方法区实现已经不再是永久代(Permanent Generation)了,而是使用元空间(Metaspace)来实现。
运行时常量池(Runtime Constant Pool)也是方法区的一部分,用于存放常量池表(Constant Pool Table),常量池表中存放了编译期生成的各种符号字面量和符号引用。
JDK 8 以后的运行时常量池在元空间中。
三、对象
3.1 对象的创建
当我们在代码中使用 new
关键字创建一个对象时,其在 JVM 中需要经过以下步骤:
1. 类加载过程
当虚拟机遇到一条字节码指令 new
时,首先将去检查这个指令的参数是否能在常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程。
2. 分配内存
在类加载检查通过后,虚拟机需要给新生对象分配内存空间。根据 Java 堆是否规整,可以有以下两种分配方案:
①、指针碰撞:假设 Java 堆中内存是绝对规整的,所有使用的内存放在一边,所有未被使用的内存放在另外一边,中间以指针作为分界点指示器。
此时内存分配只是将指针向空闲方向偏移出对象大小的空间即可,这种方式被称为指针碰撞。
②、空闲列表:如果 Java 堆不是规整的,此时虚拟机需要维护一个列表,记录哪些内存块是可用的,哪些是不可用的。在进行内存分配时,只需要从该列表中选取出一块足够的内存空间划分给对象实例即可。
注:Java 堆是否规整取决于其采用的垃圾收集器是否带有空间压缩整理能力,前面讲过了。
除了分配方式外,由于对象创建在虚拟机中是一个非常频繁的行为,此时需要保证在并发环境下的线程安全:如果一个线程给对象 A 分配了内存空间,但指针还没来得及修改,此时就可能出现另外一个线程使用原来的指针来给对象 B 分配内存空间的情况。
想要解决这个问题有两个方案:
①、方式一:采用同步锁定,或采用 CAS 配上失败重试的方式来保证更新操作的原子性。
②、方式二:为每个线程在 Java 堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。
线程在进行内存分配时优先使用本地缓冲,当本地缓冲使用完成后,再向 Java 堆申请分配,此时 Java 堆采用同步锁定的方式来保证分配行为的线程安全。
3. 对象头设置
将对象有关的元数据信息、对象的哈希码、分代年龄等信息存储到对象头中。
可以和 JIT 那节的内容关联起来。
4. 对象初始化
调用对象的构造方法,即 Class 文件中的 <init>()
来初始化对象,为相关字段赋值。
3.2 对象的内存布局
在 HotSpot 中,对象在堆内存中的存储布局可以划分为以下三个部分:
1. 对象头 (Header)
对象头包括两部分信息:
- Mark Word:对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,官方统称为 Mark Word,我们曾在 synchronized 的四种锁状态讲过。
- 类型指针:对象指向它类型元数据的指针,Java 虚拟机通过这个指针来确定该对象是哪个类的实例。需要说明的是,并非所有的虚拟机都必须要在对象数据上保留类型指针,这取决于对象的访问定位方式。
2. 实例数据 (Instance Data)
即我们在代码中定义的各种类型的字段,无论是从父类继承而来,还是子类中定义的都需要记录。
3. 对齐填充 (Padding)
主要起占位符的作用。HotSpot 要求对象起始地址必须是 8 字节的整倍数,即间接要求了任何对象的大小都必须是 8 字节的整倍数。对象头部分在设计上就是 8 字节的整倍数,如果对象的实例数据不是 8 字节的整倍数,则由对齐填充进行补全。
3.3 对象的访问定位
对象创建后,Java 程序就可以通过栈上的 reference
(也就是引用)来操作堆上的具体对象。
《Java 虚拟机规范》规定 reference
是一个指向对象的引用,但并未规定其具体实现方式。主流的方式方式有以下两种:
- 句柄访问:Java 堆将划分出一块内存来作为句柄池,
reference
中存储的是对象的句柄地址,而句柄则包含了对象实例数据和类型数据的地址信息。 - 指针访问:
reference
中存储的直接就是对象地址,而对象的类型数据则由上文介绍的对象头中的类型指针来指定。
通过句柄访问对象:
通过直接指针访问对象:
句柄访问的优点在于对象移动时(垃圾收集时移动对象是非常普遍的行为)只需要改变句柄中实例数据的指针,而 reference
本身并不需要修改;
指针访问则反之,由于其 reference
中存储的直接就是对象地址,所以当对象移动时, reference
需要被修改。但针对只需要访问对象本身的场景,指针访问则可以减少一次定位开销。由于对象访问是一项非常频繁的操作,所以这类减少的效果会非常显著,基于这个原因,HotSpot 主要使用的是指针访问的方式。
四、垃圾收集机制
在 JVM 内存模型中,程序计数器、虚拟机栈、本地方法栈这 3 个区域都是线程私有的,会随着线程的结束而销毁,因此在这 3 个区域当中,无需过多考虑垃圾回收问题。垃圾回收问题主要发生在 Java 堆上。
在 Java 堆上,垃圾回收的主要内容是死亡的对象(不可能再被任何途径使用的对象)。
判断对象是否死亡有以下两种方法:
1. 引用计数法
在对象中添加一个引用计数器,对象每次被引用时,该计数器加一;当引用失效时,计数器的值减一;只要计数器的值为零,则代表对应的对象不可能再被使用。该方法的缺点在于无法避免相互引用的问题:
objA.instance = objB
objB.instance = objA
objA = null;
objB = null;
System.gc();
如上所示,此时两个对象已经不能再被访问,但其互相持有对对方的引用,如果采用引用计数法,则两个对象都无法被回收。
2. 可达性分析
但上面的代码在大多数虚拟机中都能被正确的回收,因为大多数主流的虚拟机都是采用的可达性分析方法来判断对象是否死亡。
可达性分析是通过一系列被称为 GC Roots
的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为引用链(Reference Chain),如果某个对象到 GC Roots
间没有任何引用链相连,这代表 GC Roots
到该对象不可达, 此时证明该对象不可能再被使用。
在 Java 语言中,固定可作为 GC Roots
的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
- 在方法区(元空间)中类静态变量引用的对象,譬如 Java 类中引用类型的静态变量;
- 在方法区(元空间)中常量引用的对象,譬如字符串常量池(String Table)里的引用;
- 在本地方法栈中的 JNI(Native 方法)引用的对象;
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(如 NullPointException,OutOfMemoryError 等)及系统类加载器;
- 所有被同步锁(synchronized 关键字)持有的对象;
除了这些固定的 GC Roots
集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域的不同,还可能会有其他对象 “临时性” 地加入,共同构成完整的 GC Roots
集合。
3. 对象引用
可达性分析是基于引用链进行判断的,在 JDK 1.2 之后,Java 将引用关系分为以下四类:
强引用 (Strongly Reference)
最传统的引用,如 Object obj = new Object()
。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用 (Soft Reference)
用于描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出之前,会被列入回收范围内进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
下面是一个使用 Java 中 SoftReference 类的示例代码:
class SoftReferenceExample {public static void main(String[] args) {// 创建一个强引用的对象String strongReference = new String("二哥,我是个强引用");// 创建一个软引用,指向上面的对象SoftReference<String> softReference = new SoftReference<>(strongReference);// 干掉强引用strongReference = null;// 现在只有软引用指向 "二哥,我是个强引用" 对象// 尝试通过软引用获取对象String retrievedString = softReference.get();System.out.println(retrievedString); // 输出 "二哥,我是个强引用"// 强制进行垃圾回收,可能会清除软引用的对象System.gc();// 再次尝试通过软引用获取对象retrievedString = softReference.get();if (retrievedString != null) {System.out.println(retrievedString);} else {System.out.println("软引用的对象已被垃圾回收");}}
}
这个例子中,我们首先创建了一个字符串对象的强引用,然后通过 SoftReference 创建了这个对象的软引用。在取消了强引用后,这个对象只剩下软引用。当我们尝试通过软引用获取对象时,如果对象还存在,软引用会返回它;如果对象已被垃圾收集器回收,则返回 null。
需要注意的是,第二次回收时,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。这里的 “足够” 是指在抛出内存溢出异常之前,系统会进行最后一次尝试,如果这次回收后还是没有足够的内存,才会抛出内存溢出异常。
通常情况下,上面代码在执行 gc 后软引用不会被回收,因为此时内存还是足够的。
二哥,我是个强引用
二哥,我是个强引用
弱引用 (Weak Reference)
用于描述那些非必须的对象,强度比软引用弱。被弱引用关联的对象只能生存到下一次垃圾收集发生时,无论当前内存是否足够,弱引用对象都会被回收。
来看这段代码:
class WeakReferenceExample {public static void main(String[] args) {// 创建一个强引用的对象String strongReference = new String("二哥,我是强引用");// 创建一个弱引用,指向上面的对象WeakReference<String> weakReference = new WeakReference<>(strongReference);// 取消强引用strongReference = null;// 强制进行垃圾回收System.gc();// 尝试通过弱引用获取对象String retrievedString = weakReference.get();if (retrievedString != null) {System.out.println(retrievedString);} else {System.out.println("弱引用的对象已被垃圾回收");}}
}
这个例子中,我们首先创建了一个字符串对象的强引用,然后通过 WeakReference 创建了这个对象的弱引用。在取消了强引用后,这个对象只剩下弱引用。当我们尝试通过弱引用获取对象时,如果对象还存在,弱引用会返回它;如果对象已被垃圾收集器回收,则返回 null。
运行结果就和软引用不一样了,gc 后弱引用被回收了。
弱引用的对象已被垃圾回收
虚引用 (Phantom Reference)
最弱的引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被回收时收到一个系统通知。
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾收集器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入到与之关联的引用队列中。
来看这个示例代码:
class PhantomReferenceExample {public static void main(String[] args) {// 创建一个强引用的对象String strongReference = new String("二哥,我是强引用");// 创建一个引用队列ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();// 创建一个虚引用,指向上面的对象,并与引用队列关联PhantomReference<String> phantomReference = new PhantomReference<>(strongReference, referenceQueue);// 取消强引用strongReference = null;// 强制进行垃圾回收System.gc();// 检查引用队列,看是否有通知if (referenceQueue.poll() != null) {System.out.println("虚引用的对象已被垃圾回收,且收到了通知");} else {System.out.println("虚引用的对象未被垃圾回收,或未收到通知");}}
}
这个例子中,我们首先创建了一个字符串对象的强引用,然后通过 PhantomReference 创建了这个对象的虚引用,并与引用队列关联。在取消了强引用后,这个对象只剩下虚引用。当我们强制进行垃圾回收时,如果对象还存在,虚引用不会返回它;如果对象已被垃圾收集器回收,则返回 null。同时,如果对象被回收,虚引用会被加入到引用队列中。
运行结果如下:
虚引用的对象已被垃圾回收,且收到了通知
4. 对象真正死亡
要真正宣告一个对象死亡,需要经过至少两次标记过程:
①、如果对象在进行可达性分析后发现 GC Roots
不可达,将会进行第一次标记;
②、随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalized()
方法。
如果对象没有覆盖 finalized()
方法,或者 finalized()
已经被虚拟机调用过,这两种情况都会视为没有必要执行。
如果判定结果是有必要执行,此时对象会被放入名为 F-Queue
的队列,等待 Finalizer 线程执行其 finalized()
方法。
在这个过程中,收集器会进行第二次小规模的标记,如果对象在 finalized()
方法中重新将自己与引用链上的任何一个对象进行了关联,如将自己(this 关键字)赋值给某个类变量或者对象的成员变量,此时它就实现了自我拯救,则第二次标记会将其移除 “即将回收” 的集合,否则该对象就将被真正回收,走向死亡。
4.3 垃圾收集算法
1. 分代收集理论
当前大多数虚拟机都遵循 “分代收集” 的理论进行设计,它建立在强弱两个分代假说下:
- 弱分代假说 (Weak Generational Hypothesis) :绝大多数对象都是朝生夕灭的。
- 强分代假说 (Strong Generational Hypothesis) :熬过越多次垃圾收集过程的对象就越难以消亡。
- 跨带引用假说 (Intergenerational Reference Hypothesis) :基于上面两条假说还可以得出的一条隐含推论:存在相互引用关系的两个对象,应该倾向于同时生存或者同时消亡。
强弱分代假说奠定了垃圾收集器的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄就是对象经历垃圾收集的次数)分配到不同的区域中进行存储。
之后如果一个区域中的对象都是朝生夕灭的,那么收集器只需要关注少量对象的存活而不是去标记那些大量将要被回收的对象,此时就能以较小的代价获取较大的空间。
最后再将难以消亡的对象集中到一块,根据强分代假说,它们是很难消亡的,因此虚拟机可以使用较低的频率进行回收,这就兼顾了时间和内存空间的开销。
2. 回收类型
根据分代收集理论,收集范围可以分为以下几种类型:
①、部分收集 (Partial GC) :具体分为:
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 整堆收集 (Full GC) :收集整个 Java 堆和方法区。
3. 标记-清除算法
它是最基础的垃圾收集算法,收集过程分为两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象;也可以反过来,标记存活对象,统一回收所有未被标记的对象。
它主要有以下两个缺点:
- 执行效率不稳定:如果 Java 堆上包含大量需要回收的对象,则需要进行大量标记和清除动作;
- 内存空间碎片化:标记清除后会产生大量不连续的空间,从而导致无法为大对象分配足够的连续内存。
4. 标记-复制算法
标记-复制算法基于 ”半区复制“ 算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存使用完了,就将还存活着的对象复制到另外一块,然后再把已经使用过的那块内存空间一次性清理掉。其优点在于避免了内存空间碎片化的问题,其缺点如下:
- 如果内存中多数对象都是存活的,这种算法将产生大量的复制开销;
- 浪费内存空间,内存空间变为了原有的一半。
基于新生代 “朝生夕灭” 的特点,大多数虚拟机都不会按照 1:1 的比例来进行内存划分,例如 HotSpot 会将内存空间划分为一块较大的 Eden
和两块较小的 Survivor
空间,它们之间的比例是 8:1:1 。
每次分配时只会使用 Eden
和其中的一块 Survivor
,发生垃圾回收时,只需要将存活的对象一次性复制到另外一块 Survivor
上,这样只有 10% 的内存空间会被浪费掉。
当 Survivor
空间不足以容纳一次 Minor GC
时,此时由其他内存区域(通常是老年代)来进行分配担保。
5. 标记-整理算法
标记-整理算法是在标记完成后,让所有存活对象都向内存的一端移动,然后直接清理掉边界以外的内存。
其优点在于可以避免内存空间碎片化的问题,也可以充分利用内存空间;其缺点在于根据所使用的收集器的不同,在移动存活对象时可能要全程暂停用户程序:
五、垃圾收集器
并行与并发是并发编程中的专有名词,在谈论垃圾收集器的上下文语境中,它们的含义如下:
①、并行 (Parallel) :并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,此时通常默认用户线程是处于等待状态。
②、并发 (Concurrent) :并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。但由于垃圾收集器线程会占用一部分系统资源,所以程序的吞吐量依然会受到一定影响。
HotSpot 中一共存在七款经典的垃圾收集器:
注:收集器之间存在连线,代表它们可以搭配使用。
5.1 Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器,它是一个单线程收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到收集结束,这是其主要缺点。
它的优点在于单线程避免了多线程复杂的上下文切换,因此在单线程环境下收集效率非常高,由于这个优点,迄今为止,其仍然是 HotSpot 虚拟机在客户端模式下默认的新生代收集器:
5.2 ParNew 收集器
它是 Serial 收集器的多线程版本,可以使用多条线程进行垃圾回收:
5.3 Parallel Scavenge 收集器
Parallel Scavenge 也是新生代收集器,基于 标记-复制 算法进行实现,它的目标是达到一个可控的吞吐量。这里的吞吐量指的是处理器运行用户代码的时间与处理器总消耗时间的比值:
吞吐量 = 运行用户代码时间 \ (运行用户代码时间 + 运行垃圾收集时间)
Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量:
①、-XX:MaxGCPauseMillis
:控制最大垃圾收集时间,假设需要回收的垃圾总量不变,那么降低垃圾收集的时间就会导致收集频率变高,所以需要将其设置为合适的值,不能一味减小。
②、-XX:MaxGCTimeRatio
:直接用于设置吞吐量大小,它是一个大于 0 小于 100 的整数。假设把它设置为 19,表示此时允许的最大垃圾收集时间占总时间的 5%(即 1/(1+19) );默认值为 99 ,即允许最大 1%( 1/(1+99) )的垃圾收集时间。
5.4 Serial Old 收集器
从名字也能看出来,它是 Serial 收集器的老年代版本,同样是一个单线程收集器,采用 标记-整理 算法,主要用于给客户端模式下的 HotSpot 使用:
5.5 Paralled Old 收集器
Paralled Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,采用 标记-整理 算法实现:
5.6 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于 标记-清除 算法实现,整个收集过程分为以下四个阶段:
- 初始标记 (inital mark) :标记
GC Roots
能直接关联到的对象,耗时短但需要暂停用户线程; - 并发标记 (concurrent mark) :从
GC Roots
能直接关联到的对象开始遍历整个对象图,耗时长但不需要暂停用户线程; - 重新标记 (remark) :采用增量更新算法,对并发标记阶段因为用户线程运行而产生变动的那部分对象进行重新标记,耗时比初始标记稍长且需要暂停用户线程;
- 并发清除 (inital sweep) :并发清除掉已经死亡的对象,耗时长但不需要暂停用户线程。
其优点在于耗时长的 并发标记 和 并发清除 阶段都不需要暂停用户线程,因此其停顿时间较短,其主要缺点如下:
- 由于涉及并发操作,因此对处理器资源比较敏感。
- 由于是基于 标记-清除 算法实现的,因此会产生大量空间碎片。
- 无法处理浮动垃圾(Floating Garbage):由于并发清除时用户线程还是在继续,所以此时仍然会产生垃圾,这些垃圾就被称为浮动垃圾,只能等到下一次垃圾收集时再进行清理。
5.7 Garbage First 收集器
Garbage First(简称 G1)是一款面向服务端的垃圾收集器,也是 JDK 9 服务端模式下默认的垃圾收集器,它的诞生具有里程碑式的意义。
G1 虽然也遵循分代收集理论,但不再以固定大小和固定数量来划分分代区域,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region)。每一个 Region 都可以根据不同的需求来扮演新生代的 Eden
空间、Survivor
空间或者老年代空间,收集器会根据其扮演角色的不同而采用不同的收集策略。
上面还有一些 Region 使用 H 进行标注,它代表 Humongous,表示这些 Region 用于存储大对象(humongous object,H-obj),即大小大于等于 region 一半的对象。
G1 收集器的运行大致可以分为以下四个步骤:
①、初始标记 (Inital Marking) :标记 GC Roots
能直接关联到的对象,并且修改 TAMS(Top at Mark Start)指针的值,让下一阶段用户线程并发运行时,能够正确的在 Reigin 中分配新对象。
G1 为每一个 Reigin 都设计了两个名为 TAMS 的指针,新分配的对象必须位于这两个指针位置以上,位于这两个指针位置以上的对象默认被隐式标记为存活的,不会纳入回收范围;
②、并发标记 (Concurrent Marking) :从 GC Roots
能直接关联到的对象开始遍历整个对象图。遍历完成后,还需要处理 SATB 记录中变动的对象。
SATB(snapshot-at-the-beginning,开始阶段快照)能够有效的解决并发标记阶段因为用户线程运行而导致的对象变动,其效率比 CMS 重新标记阶段所使用的增量更新算法效率更高;
③、最终标记 (Final Marking) :对用户线程做一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的少量的 STAB 记录。虽然并发标记阶段会处理 SATB 记录,但由于处理时用户线程依然是运行中的,因此依然会有少量的变动,所以需要最终标记来处理;
④、筛选回收 (Live Data Counting and Evacuation) :负责更新 Regin 统计数据,按照各个 Regin 的回收价值和成本进行排序,在根据用户期望的停顿时间进行来指定回收计划,可以选择任意多个 Regin 构成回收集。
然后将回收集中 Regin 的存活对象复制到空的 Regin 中,再清理掉整个旧的 Regin 。此时因为涉及到存活对象的移动,所以需要暂停用户线程,并由多个收集线程并行执行。
5.8 内存分配原则
1. 对象优先在 Eden 分配
大多数情况下,对象在新生代的 Eden
区中进行分配,当 Eden
区没有足够空间时,虚拟机将进行一次 Minor GC。
2. 大对象直接进入老年代
大对象就是指需要大量连续内存空间的 Java 对象,最典型的就是超长的字符串或者元素数量很多的数组,它们将直接进入老年代。
主要是因为如果在新生代分配,因为其需要大量连续的内存空间,可能会导致提前触发垃圾回收;并且由于新生代的垃圾回收本身就很频繁,此时复制大对象也需要额外的性能开销。
3. 长期存活的对象将进入老年代
虚拟机会给每个对象在其对象头中定义一个年龄计数器。对象通常在 Eden
区中诞生,如果经历第一次 Minor GC 后仍然存活,并且能够被 Survivor 容纳的话,该对象就会被移动到 Survivor 中,并将其年龄加 1。
对象在 Survivor 中每经过一次 Minor GC,年龄就加 1,当年龄达到一定程度后(由 -XX:MaxTenuringThreshold
设置,默认值为 15)就会进入老年代中。
4. 动态年龄判断
如果在 Survivor 空间中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,而无需等待年龄到达 -XX:MaxTenuringThreshold
设置的值。
5. 空间担保分配
在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果条件成立,那么这一次的 Minor GC 可以确认是安全的。
如果不成立,虚拟机会查看 -XX:HandlePromotionFailure
的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于或者 -XX:HandlePromotionFailure
的值设置不允许冒险,那么就要改为进行一次 Full GC 。
六、类加载机制
Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的类加载机制。
6.1 类加载时机
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、卸载、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接:
《Java 虚拟机规范》严格规定了有且只有六种情况必须立即对类进行初始化:
①、遇到 new
、 getstatic
、 putstatic
、 invokestatic
这四条字节码指令,能够生成这四条指令码的典型 Java 代码场景有:
- 使用
new
关键字实例化对象时; - 读取或设置一个类型的静态字段时(被 final 修饰,已在编译期把结果放入常量池的静态字段除外);
- 调用一个类的静态方法时。
②、使用 java.lang.reflect
包的方法对 Class 进行反射调用时,如果类型没有进行过初始化、则需要触发其初始化;
③、当初始化类时,如发现其父类还没有进行过初始化、则需要触发其父类进行初始化;
④、当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
⑤、当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle
实例最后解析的结果为 REF_getStatic
, REF_putStatic
, REF_invokeStatic
, REF_newInvokeSpecial
四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化;
⑥、当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。
6.2 类加载过程
1. 加载
在加载阶段,虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流 ;
- 将这个字节流所代表的静态存储结构转换为运行时数据结构;
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为这个类的各种数据的访问入口。
《Java 虚拟机规范》并没有限制从何处获取二进制流,因此可以从 JAR 包、WAR 包获取,也可以从 JSP 生成的 Class 文件等处获取。
2. 验证
这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,从而保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
验证阶段大致会完成下面四项验证:
- 文件格式验证:验证字节流是否符合 Class 文件格式的规范;
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java 语言规范》的要求(如除了
java.lang.Object
外,所有的类都应该有父类); - 字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的(如允许把子类对象赋值给父类数据类型,但不能把父类对象赋值给子类数据类型);
- 符号引用验证:验证类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。如果无法验证通过,则会抛出一个
java.lang.IncompatibleClassChangeError
的子类异常,如java.lang.NoSuchFieldError
、java.lang.NoSuchMethodError
等。
3. 准备
准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。
4. 解析
解析是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程:
- 符号引用:符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用:直接引用是指可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。
整个解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行解析。
5. 初始化
初始化阶段就是执行类构造器的 <clinit>()
方法的过程,该方法具有以下特点:
<clinit>()
方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生,编译器收集顺序由语句在源文件中出现的顺序决定。<clinit>()
方法与类的构造方法(即在虚拟机视角中的实例构造器<init>()
方法)不同,它不需要显示的调用父类的构造器,Java 虚拟机会保证在子类的<clinit>()
方法执行前,父类的<clinit>()
方法已经执行完毕。- 由于父类的
<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类变量的赋值操作。 <clinit>()
方法对于类或者接口不是必须的,如果一个类中没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成<clinit>()
方法。- 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
<clinit>()
方法。 - Java 虚拟机必须保证一个类的
<clinit>()
方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待。
6.3 类加载器
能够通过一个类的全限定名来获取描述该类的二进制字节流的工具称为类加载器。
每一个类加载器都拥有一个独立的类名空间,因此对于任意一个类,都必须由加载它的类加载器和这个类本身来共同确立其在 Java 虚拟机中的唯一性。
这意味着要想比较两个类是否相等,必须在同一类加载器加载的前提下;如果两个类的类加载器不同,则它们一定不相等。
6.4 双亲委派模型
从 Java 虚拟机角度而言,类加载器可以分为以下两类:
- 启动类加载器 :启动类加载器(Bootstrap ClassLoader)由 C++ 语言实现(以 HotSpot 为例),它是虚拟机自身的一部分;
- 其他所有类的类加载器 :由 Java 语言实现,独立存在于虚拟机外部,并且全部继承自
java.lang.ClassLoader
。
从开发人员角度而言,类加载器可以分为以下三类:
- 启动类加载器 (Boostrap Class Loader) :负责把存放在
<JAVA_HOME>\lib
目录中,或被-Xbootclasspath
参数所指定的路径中存放的能被 Java 虚拟机识别的类库加载到虚拟机的内存中; - 扩展类加载器 (Extension Class Loader) :负责加载
<JAVA_HOME>\lib\ext
目录中,或被java.ext.dirs
系统变量所指定的路径中的所有类库。 - 应用程序类加载器 (Application Class Loader) :负责加载用户类路径(ClassPath)上的所有的类库。
JDK 9 之前的 Java 应用都是由这三种类加载器相互配合来完成加载:
上图所示的各种类加载器之间的层次关系被称为类加载器的 “双亲委派模型”,“双亲委派模型” 要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,需要注意的是这里的加载器之间的父子关系一般不是以继承关系来实现的,而是使用组合关系来复用父类加载器的代码。
双亲委派模型的工作过程如下:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
基于双亲委派模型可以保证程序中的类在各种类加载器环境中都是同一个类,否则就有可能出现一个程序中存在两个不同的 java.lang.Object
的情况。
6.5 模块化下的类加载器
JDK 9 之后为了适应模块化的发展,类加载器做了如下变化:
- 仍维持三层类加载器和双亲委派的架构,但扩展类加载器被平台类加载器所取代;
- 当平台及应用程序类加载器收到类加载请求时,要首先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载;
- 启动类加载器、平台类加载器、应用程序类加载器全部继承自
java.internal.loader.BuiltinClassLoader
,BuiltinClassLoader 中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。
七、程序编译
7.1 编译器分类
- 前端编译器:把
*.java
文件转变成.class
文件的过程;如 JDK 的 Javac,Eclipse JDT 中的增量式编译器。 - 即时编译器:常称为 JIT 编译器(Just In Time Complier),在运行期把字节码转变成本地机器码的过程;如 HotSpot 虚拟机中的 C1、C2 编译器,Graal 编译器。
- 提前编译器:直接把程序编译成目标机器指令集相关的二进制代码的过程。如 JDK 的 jaotc,GUN Compiler for the Java(GCJ),Excelsior JET 。
7.2 解释器与编译器
在 HotSpot 中,Java 程序最初都是通过解释器(Interpreter)进行解释执行的,其优点在于可以省去编译时间,让程序快速启动。
当程序启动后,如果虚拟机发现某个方法或代码块的运行特别频繁,就会使用编译器将其编译为本地机器码,并使用各种手段进行优化,从而提高执行效率,这就是即时编译器。
HotSpot 内置了两个(或三个)即时编译器:
- 客户端编译器 (Client Complier) :简称 C1;
- 服务端编译器 (Servier Complier) :简称 C2,在有的资料和 JDK 源码中也称为 Opto 编译器;
- Graal 编译器 :在 JDK 10 时才出现,长期目标是替代 C2。
在分层编译的工作模式出现前,不管是采用客户端编译器还是服务端编译器完全取决于虚拟机是运行在客户端模式还是服务端模式下,可以在启动时通过 -client
或 -server
参数进行指定,也可以让虚拟机根据自身版本和宿主机性能来自主选择。
7.3 分层编译
要编译出优化程度越高的代码通常都需要越长的编译时间,为了在程序启动速度与运行效率之间达到最佳平衡,HotSpot 在编译子系统中加入了分层编译(Tiered Compilation):
- 第 0 层:程序纯解释执行,并且解释器不开启性能监控功能;
- 第 1 层:使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能;
- 第 2 层:仍然使用客户端编译执行,仅开启方法及回边次数统计等有限的性能监控;
- 第 3 层:仍然使用客户端编译执行,开启全部性能监控;
- 第 4 层:使用服务端编译器将字节码编译为本地代码,其耗时更长,并且会根据性能监控信息进行一些不可靠的激进优化。
以上层次并不是固定不变的,根据不同的运行参数和版本,虚拟机可以调整分层的数量。各层次编译之间的交互转换关系如下图所示:
实施分层编译后,解释器、客户端编译器和服务端编译器就会同时工作,可以用客户端编译器获取更高的编译速度、用服务端编译器来获取更好的编译质量。
7.4 热点探测
即时编译器编译的目标是 “热点代码”,它主要分为以下两类:
- 被多次调用的方法。
- 被多次执行循环体。这里指的是一个方法只被少量调用过,但方法体内部存在循环次数较多的循环体,此时也认为是热点代码。但编译器编译的仍然是循环体所在的方法,而不会单独编译循环体。
判断某段代码是否是热点代码的行为称为 “热点探测” (Hot Spot Code Detection),主流的热点探测方法有以下两种:
- 基于采样的热点探测 (Sample Based Hot Spot Code Detection) :采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那么就认为它是 “热点方法”。
- 基于计数的热点探测 (Counter Based Hot Spot Code Detection) :采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是 “热点方法”。
八、代码优化
即时编译器除了将字节码编译为本地机器码外,还会对代码进行一定程度的优化,它包含多达几十种优化技术,这里选取其中代表性的四种进行介绍:
8.1 方法内联
最重要的优化手段,它会将目标方法中的代码原封不动地 “复制” 到发起调用的方法之中,避免发生真实的方法调用,并采用名为类型继承关系分析(Class Hierarchy Analysis,CHA)的技术来解决虚方法(Java 语言中默认的实例方法都是虚方法)的内联问题。
8.2 逃逸分析
逃逸行为主要分为以下两类:
- 方法逃逸:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,此时称为方法逃逸;
- 线程逃逸:当一个对象在方法里面被定义后,它可能被外部线程所访问,例如赋值给可以在其他线程中访问的实例变量,此时称为线程逃逸,其逃逸程度高于方法逃逸。
public static StringBuilder concat(String... strings) {StringBuilder sb = new StringBuilder();for (String string : strings) {sb.append(string);}return sb; // 发生了方法逃逸
}public static String concat(String... strings) {StringBuilder sb = new StringBuilder();for (String string : strings) {sb.append(string);}return sb.toString(); // 没有发生方法逃逸
}
如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可以对这个对象实例采取不同程序的优化:
- 栈上分配 (Stack Allocations) :如果一个对象不会逃逸到线程外,那么将会在栈上分配内存来创建这个对象,而不是 Java 堆上,此时对象所占用的内存空间就会随着栈帧的出栈而销毁,从而可以减轻垃圾回收的压力。
- 标量替换 (Scalar Replacement) :如果一个数据已经无法再分解成为更小的数据类型,那么这些数据就称为标量(如 int、long 等数值类型及 reference 类型等);反之,如果一个数据可以继续分解,那它就被称为聚合量(如对象)。如果一个对象不会逃逸外方法外,那么就可以将其改为直接创建若干个被这个方法使用的成员变量来替代,从而减少内存占用。
- 同步消除 (Synchronization Elimination) :如果一个变量不会逃逸出线程,那么对这个变量实施的同步措施就可以消除掉。
8.3 公共子表达式消除
如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生过变化,那么 E 这次的出现就称为公共子表达式。对于这种表达式,无需再重新进行计算,只需要直接使用前面的计算结果即可。
8.4 数组边界检查消除
对于虚拟机执行子系统来说,每次数组元素的读写都带有一次隐含的上下文检查以避免访问越界。如果数组的访问发生在循环之中,并且使用循环变量来访问数据,即循环变量的取值永远在 [0,list.length)
之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断。
小结
这篇文章我们系统地总结了 JVM 最重要的知识点,比如说 JVM 的内存结构、垃圾回收算法、垃圾回收器、类加载机制、类加载器、程序编译、代码优化等等,希望能对大家在学习 JVM 的时候有所帮助。
相关文章:
JVM 核心知识点总结
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,…...
无人机数据链技术详解,无人机图传数传技术,无人机数据传输技术原理
以下是对无人机数据链技术、无人机图传数传技术以及无人机数据传输技术原理的详细解释: 无人机数据链技术 无人机数据链是任务机、地面控制站之间,以及任务机与中继机、武器系统或其它操作平台之间,按照约定的通信协议和信息传输方式&#…...
【Linux】同步原理剖析及模拟BlockQueue生产消费模型
📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…...
【AVRCP】GOEP互操作性深度解析:蓝牙封面艺术传输的技术实现与演进
目录 一、技术基础:协议架构与核心概念 1.1 GOEP协议体系解析 1.2 IrOBEX协议关键技术 1.3 版本强制性要求 1.4 关键特性对比(GOEP v2.0 vs v1.1) 1.5 关键技术实现细节 1.6 GOEP v2.0互操作性要求 1.7 IrOBEX v1.5互操作性要求 二、…...
三分钟读懂微服务
一、什么是微服务 微服务,简单来说,就是把一个庞大复杂的软件系统,拆分成一个个小型的、独立的服务模块。打个比方,一个大型商场就如同传统的单体架构软件系统,里面所有的店铺、设施都紧密关联在一起。而微服务架构下…...
《Oracle DBA入门实战:十大高频问题详解与避坑指南》
Oracle DBA 入门作业十问十答 本文为 Oracle DBA 入门作业整理,涵盖工具使用、配置管理及权限控制等核心知识点,适合新手快速上手。 如有疑问或补充,欢迎评论区交流! 1. DBA 常用工具有哪些? Oracle Universal Instal…...
深入剖析 Android Compose 框架的自动动画:AnimatedVisibility 与 AnimatedContent(二十四)
深入剖析 Android Compose 框架的自动动画:AnimatedVisibility 与 AnimatedContent 引言 在 Android 应用开发中,动画是提升用户体验的重要手段。它能够让界面元素的显示与隐藏、状态的切换变得更加自然和流畅,避免生硬的变化给用户带来不佳…...
【线程安全问题的原因和方法】【java形式】【图片详解】
在本章节中采用实例图片的方式,以一个学习者的姿态进行描述问题解决问题,更加清晰明了,以及过程中会发问的问题都会一一进行呈现 目录 线程安全演示线程不安全情况图片解释: 将上述代码进行修改【从并行转化成穿行的方式】不会出…...
Cocos Creator Shader入门实战(六):使用setProperty动态设置材质属性,以及材质常用接口
引擎:3.8.5 您好,我是鹤九日! 回顾 上篇文章,我们主要讲解了关于材质的使用,主要有这么几点: 一、没有Effect资源,材质无从说起。 二、材质的构建,支持编译器和代码的动态构建 三…...
编程题记录3
九宫幻方 题目链接:https://www.lanqiao.cn/problems/100/learning/?page1&first_category_id1&second_category_id3&tags%E7%9C%81%E8%B5%9B&tag_relationintersection 先旋转、镜像得到所有的情况,可以发现情况是可以暴力得出的。…...
Geotools自动识别SLD并生成图例图片实战-以Polygon数据为例
目录 前言 一、Geotools与SLD制图基础 1、SLD是什么 2、SLD有什么用 二、SLD文件的解析与读取 1、SLD结构介绍 2、SLD实例展示 3、SLD读取方法 三、图例生成与展示 1、图例生成流程 2、图例生成实战 3、图例生成展示 四、结论 前言 在地理信息系统(GIS&…...
windows docker如何修改 默认的Container memory usage
参考:https://forums.docker.com/t/docker-on-windows-11-with-wsl2-does-not-use-the-memory-i-set-in-wslconfig/144404/3 参考:https://learn.microsoft.com/en-us/windows/wsl/wsl-config...
LabVIEW液压传动系统教学仿真平台
本文介绍了一种基于LabVIEW的液压传动系统教学仿真平台,该平台采用“老师讲解、线上仿真、线下操作”的复合实验模式,旨在提高实验教学的效率与安全性。通过实例验证,展示了该平台在教学和实际操作中的应用效果,同时也为液压传动系…...
Java实习生面试题(2025.3.23 be)
一、v-if与v-show的区别 v-show 和 v-if 都是 Vue 中的条件渲染指令,它们的主要区别在于渲染策略:v-if 会根据条件决定是否编译元素,而 v-show 则始终编译元素,只是通过改变 CSS 的 display 属性来控制显示与隐藏。 二、mybatis-…...
OpenCV第2课 OpenCV的组成结构与图片/视频的加载及展示
1.OpenCV 的组成结构 2.OpenCV 的具体模块 3. 图像的读取 4. 视频的读取 1.OpenCV 的组成结构 OpenCV 是由很多模块组成的,这些模块可以分成很多层: 最底层是基于硬件加速层(HAL)的各种硬件优化。再上一层是opencv_contrib 模块所包含的OpenCV 由其他开发人员所贡献的代…...
Blender导出fbx到Unity找不到贴图的问题
fbx导入Unity材质能不能找到贴图是一件玄学的事情。常见的情况是有些材质能找到,有些找不到: 可能有用的方法 解决方法1:把贴图文件复制过去,模型reimport; 解决方法2:导出时路径模式选复制,内…...
kafka的文章
1.面试的问题 要点 至多一次、恰好一次数据一致性超时重试、幂等消息顺序消息挤压延时消息 1.1 kafaka 生产消息的过程。 在消息发送的过程中,涉及到了两个线程,一个是main 线程,一个是sender 线程。在main 线程中创建了一个双端队列 Reco…...
Go常见问题与回答(下)
文章目录 1、通过指针变量 p 访问其成员变量 name,有哪几种方式?2、代码,说出结果3、扩容提,代码,说出结果4、指出下面这段代码的错误之处5、是否通过编译6、关于字符串连接,下面语法正确的是7、关于iota&a…...
vue3中如何缓存路由组件
在 Vue3 中缓存路由组件,主要借助<keep-alive>组件来实现,具体方法如下: 1. 全局缓存路由组件 在 App.vue 等根组件中,直接将<router-view>包裹在<keep-alive>标签内,这样所有的路由组件都会被缓存…...
云服务器怎么防御ddos攻击呢?
防御DDoS攻击是保障云服务器稳定运行的关键措施,以下是综合多种防护策略的详细方案: 1. 启用云服务商提供的DDoS防护服务 高防IP/流量清洗: 将业务流量接入云服务商的高防IP,由专业清洗中心过滤恶意流量,仅放行正常请求…...
Log4j2 的核心实现和源码分析
Log4j2 的核心实现和源码分析 1. 核心组件 1.1 Logger 功能:负责记录日志信息。实现:org.apache.logging.log4j.Logger 接口,org.apache.logging.log4j.core.Logger 类。1.2 Appender 功能:负责将日志信息输出到不同的目的地,如文件、控制台等。实现:org.apache.loggin…...
【深度学习】【目标检测】【OnnxRuntime】【C++】YOLOV3模型部署
【深度学习】【目标检测】【OnnxRuntime】【C】YOLOV3模型部署 提示:博主取舍了很多大佬的博文并亲测有效,分享笔记邀大家共同学习讨论 文章目录 【深度学习】【目标检测】【OnnxRuntime】【C】YOLOV3模型部署前言Windows平台搭建依赖环境模型转换--pytorch转onnxONNXRuntime推…...
四种跨模态行人重识别可视化方法
1.Gradcam 2.检索可视化 3.tsne图 4.距离分布 需要的私聊,代码需要付费...
8个DeepSeek文章润色指令
今天道叔给各位文字工作者安利DeepSeek的8个神仙级润色指令(附真实案例拆解),建议搭配冰美式食用更佳↓↓↓ 一、【学术黑话翻译器】 适用场景:给投资人看的BP/行业白皮书/专家访谈实录 指令公式:"将以下内容转化为通俗易懂的行业洞察…...
解决PowerShell下Git中文乱码问题
解决PowerShell下Git中文乱码问题 在使用Git进行版本控制时,许多开发者可能会遇到中文乱码的问题,尤其是在Windows环境下使用PowerShell时。这不仅影响代码的阅读和提交,还可能导致一些不可预见的错误。本文将详细探讨如何在PowerShell下解决…...
oracle数据库(数据库启动关闭/sqlplus登录及基本操作/设置字符集/distinct去重)
目录 1. Oracle数据库启动 2. Oracle数据库关闭 3. sqlplus登录Oracle数据库 3.1 使用sqlplus登录Oracle数据库 3.2 使用sqlplus登录Oracle数据库 3.3 远程登录 3.4 解锁用户 3.5 修改用户密码 3.6 查看当前语言环境 4. sqlplus基本操作 4.1 显示当前用户 4.2 查看当前用户…...
mapreduce时,客户端做哪些事
在MapReduce过程中,客户端(Client)是用户提交作业的入口,负责作业的初始化、配置、资源提交和作业监控。以下是客户端在整个流程中的具体职责和操作步骤: 1. 作业配置与参数解析 设置作业属性: 定义MapRed…...
DeepBI:重构流量逻辑,助力亚马逊广告实现高效流量增长
在日益激烈的跨境电商竞争环境中,广告投放早已从“粗放撒网”走向“精细化运营”。尤其是在亚马逊这样一个成熟且竞争白热化的平台,如何在广告预算有限的前提下实现高效曝光、精准触达、稳定转化,成为众多卖家和运营团队面临的核心挑战。 De…...
Linux内核的页面错误:原因与解决方案
当程序访问虚拟内存中的一个页面时,如果该页面当前不在物理内存中,就会触发一个称为"page fault"(页异常)的异常。操作系统需要处理这个异常,并将所需页面从磁盘加载到内存中。实现虚存管理的一个关键是page…...
LORA 中的 梯度外积是什么意思; 方差和协方差的实际含义:衡量变量的离散程度和变量间的线性相关性
LORA 中的 梯度外积是什么意思 目录 LORA 中的 梯度外积是什么意思**一、梯度外积的定义****二、示例说明****步骤1:计算单样本梯度****步骤2:计算梯度外积****三、梯度外积的作用****四、总结**方差和协方差的实际含义:衡量变量的离散程度和变量间的线性相关性**一、方差(…...
XSS复现漏洞简单前八关靶场
靶场不需要安装任意环境 链接如下:XSS Game - Learning XSS Made Simple! | Created by PwnFunction 目录 XSS Game 第一关:Ma Spaghet! 第二关:Jefff 第三关:Ugandan Knuckles 第四关:Ricardo Milos 第五关&am…...
3.24-3 接口测试断言
一.postman 断言 1.断言再test中 #状态码是否等于200 tests["Status code is 200"] responseCode.code 200; #断言响应时间小于200ms tests["Response time is less than 200ms"] responseTime < 200; #断言响应体包含内容 tests["Body…...
《鸿蒙携手AI:解锁智慧出行底层逻辑》
在科技飞速发展的当下,智慧出行成为人们对未来交通的美好期许,而鸿蒙系统与人工智能的深度融合,正为这一愿景的实现提供强大助力。从技术原理角度深入剖析,鸿蒙系统究竟如何支撑人工智能在智慧出行场景中的应用呢?这背…...
【AVRCP】探寻AVRCP控制互操作性:连接、命令与设备交互
目录 一、AVCTP连接管理 1.1 AVCTP连接建立 1.2 AVCTP连接释放 二、AV/C命令的操作流程 2.1 AV/C命令交换流程 2.2 AV/C命令类型 三、AVRCP特定命令 四、AVRCP浏览命令 五、OBEX连接管理 5.1 OBEX连接建立 5.2 OBEX连接释放 六、总结 七、参考资料 AVRCP对于实现设…...
Mybatis-Plus知识点详解
Mybatis-plus(简称MP),基于Mybatis的增强工具,保留了Mybatis的所有功能,同时增加了通用的CRUD,条件构造器,分页插件等等实用工具 特性 即拿即用:通过通用Mapper和Service,无需编写XML既可以完成单表CURE操作 Lambda支持:使用Lambda表达式构建查询条件,避免硬编码字段名,提升代…...
紧凑交叉引用表
嗯,用户问的是“compact xref table”,也就是紧凑型交叉引用表。我之前在回答中提到过交叉引用流(XRef Stream),但可能需要更详细地解释两者的区别和联系。根据搜索结果中的网页1,传统的Xref表以文本形式存…...
CMake 详解:跨平台构建系统的入门与进阶
目录 一、相关知识点 1. 什么是cmake,为什么使用? 2. 构建过程 二、CMake使用流程 1. 创建 CMakeLists.txt 文件 2. 配置构建目录 3. 运行cmake 4. 运行make编译 一、相关知识点 1. 什么是cmake,为什么使用? CMake 是一个开…...
【架构设计】学习路径
掌握前置知识后,学习架构设计需要从理论认知到实践落地逐步推进。以下是系统化的学习路径,结合具体案例与实操建议,帮助你快速进阶: 一、构建架构思维基础 1. 理解架构设计的核心目标 关键问题驱动设计: 每个架构决策…...
14、Python 枚举与类型注解进阶
Python 枚举与类型注解进阶 文章概述 本文深入探讨Python中枚举(Enum)与类型注解的高级应用场景。通过剖析Enum类的核心特性、dataclass装饰器的工程实践、静态类型检查工具mypy的集成使用,结合状态机等实际案例,系统性地提升代…...
C语言 【实现电脑关机小游戏】非常好玩
引言 在时间限制内做出正确的回答,时间一到,电脑自动关机,听起来是不是很有意思,下面来看看怎么实现吧。 注意:该游戏只在windows系统下可以玩, 一、游戏原理: 在Windows系统下,通…...
【蓝桥杯速成】| 11.回溯 之 子集问题
题目一:子集 问题描述 78. 子集 - 力扣(LeetCode) 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例…...
统计矩的高阶推广:经验还是理论推导?
矩的发展既是经验总结的结果,也是数学理论推导的产物。研究者们在分析数据、描述物理现象的过程中,发现了低阶矩与日常物理概念(如质心、惯性)之间的紧密联系,而高阶矩的应用往往出现在更复杂的数学体系中,…...
SpringBoot2集成Elasticsearch8(使用spring-boot-starter-data-elasticsearch)
写在前面 使用spring-boot-starter-data-elasticsearch集成Elasticsearch8? What? 官方写的不支持啊?让我们来看下官方给出的版本建议。 官方地址: https://docs.spring.io/spring-data/elasticsearch/reference/elasticsearch/versions.…...
Postgresql源码(142)子查询提升pull_up_sublinks
1 案例 drop table t_fun01; create table t_fun01 (image_id numeric primary key, content_id varchar(50), file_code varchar(20)); create index idx3 on t_fun01(content_id); create index idx4 on t_fun01(file_code); insert into t_fun01 select t.i, t.i%10, t.i%1…...
sonar代码检测研究及平台搭建
为了实现提交代码自动检测代码缺陷,本文介绍了一种将jenkins与gitlab集成的自动检测机制,如需应用于生产级开发流程,可在此基础上进行功能丰富和扩展,本文仅进行了原理性搭建。 一、基础环境准备 与sonar配合使用的jenkins和gitlab基础软件…...
清华大学:DeepSeek从入门到精通系列教程1-9讲(持续更新中)|大礼包免费下载
导 读INTRODUCTION 今天分享由清华大学新闻与传播学院、人工智能学院双聘教授沈阳老师团队倾力打造的《DeepSeek从入门到精通系列教程1-9讲(持续更新中)》,包含:《DeepSeek:从入门到精通》《DeepSeek如何赋能职场应用》…...
使用Python可视化图结构:从GraphML文件生成节点关系图(lightrag 生成)
引言 在数据可视化领域,图结构(Graph)常用于展示实体间的复杂关系。例如,文学分析中的角色关系、社交网络中的用户互动等。本文将通过一个实际案例,演示如何使用 NetworkX 和 Matplotlib 从 GraphML 文件生成节点关系…...
排序复习_代码纯享
头文件 #pragma once #include<iostream> #include<vector> #include<utility> using std::vector; using std::cout; using std::cin; using std::endl; using std::swap;//插入排序 //1、直接插入排序(稳定) void InsertSort(vecto…...
Docker Hub Mirror 终极解决方案——0成本,超高速!
CNB Docker Mirror (cdm) CNB Docker Mirror 是一个基于 CNB 的 Docker 镜像加速工具,提供本地镜像加速功能。 功能特性 镜像加速:在本地启动连接到 CNB 环境的 Docker 镜像加速服务,然后通过配置 Docker 客户端实现镜像加速下载自动重连&…...
2000-2019年各省地方财政车船税数据
2000-2019年各省地方财政车船税数据 1、时间:2000-2019年 2、来源:国家统计局、统计年鉴 3、指标:行政区划代码、地区、年份、地方财政车船税 4、范围:31省 5、指标说明:车船税作为地方财政的重要组成部分&#x…...