JVM相关知识点
文章目录
1、什么是JVM?
JVM是JRE的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的。JVM有自己完善的硬件架构,如处理器,堆栈,寄存器等,还有相应的指令系统。JVM是Java实现跨平台的本质,JVM运行在操作系统之上,java程序之下,在不同平台安装不同版本的JVM,由JVM来解释class文件,翻译成不同的机器码。
JVM由四大部分组成:
- 本地库接口: Native Interface:与native lib(本地方法库)交互,融合其他编程语言为Java所用,是与其它编程语言交互的接口
- 运行时数据区: Runtime data area 即常说的JVM内存;
- 类加载子系统: Class loader :根据全限定类名,装载class文件到运行时数据区的方法区中。
- 执行引擎子系统: Execution engine :也叫解释器,负责解释class指令为机器码,再提交给操作系统执行
2、JVM内存结构图

- 类装载子系统:将class文件加载到运行时数据区。
- 字节码执行器:将运行时数据区的字节码进行执行,将字节码翻译/编译为本地机器码。
- 堆:new出来的对象存放在这里,线程共享。
- 虚拟机栈:存放局部变量(基本数据类型和值,引用类型和地址),方法的执行在栈中,每个方法被调用都会创建一个个的栈帧用来保存方法执行时的信息,是线程私有。
- 方法区:存放类元信息、常量、静态变量,线程共享,运行时常量池。方法区属于堆的逻辑部分,物理上可以不和堆连续在一起。
- 程序计数器:记录线程下一次执行的指令的行号,线程私有。
- 本地方法栈:Java调用本地方法,为本地方法的运行开辟了一块空间,来为本地方法服务。
补充:
- jdk7方法区的实现是永久代,jdk8方法区实现是元空间(本地内存,不在虚拟机中)。
- jdk6,永久代存在
字符串常量池,运行时常量池,静态变量。 - jdk7,提出去永久代,
字符串常量池被移到堆中,运行时常量池、静态变量还在永久代。 - jdk8,不存在永久代,方法区的实现为元空间,元空间不在虚拟机中,而是在本地内存中,逻辑上与堆相连,物理上不连续,字符串常量池仍然在堆中,而
运行时常量池、静态变量、常量、类型信息、字段、方法被移动到了元空间。
采用元空间而不是永久代的原因:
- 字符串存在永久代中,容易出现性能问题和内存溢出;
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出;
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低;
- 将 HotSpot 与 JRockit 合二为一;
虚拟机栈进一步划分:
- 局部变量表:存放局部变量。
- 操作数栈:方法执行时,存放操作数的一块空间,操作数就是用来计算的数。
- 动态链接:
每一个栈帧内部都包含一个指向运行时常量池中的该栈帧所属方法的引用。而包含这个引用就是为了支持当前方法的代码能够实现动态链接。
在字节码文件中,所有变量和方法的引用都作为符号引用,保存在class文件的常量池。
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
简单理解为:由于类信息包括方法的信息被加载在方法区中,方法的调用需要知道方法的信息在方法区中的地址,动态链接可以找到方法在方法区的位置。 - 方法出口:存放方法结束之后从哪一行代码继续执行的地址。
常量池:GOGO
方法区包含的信息:

1、类型信息:
类的完整名称
类的直接父类的完整名称
类的直接实现接口的有序列表
类型标志(类类型还是接口类型)
类的修饰符(public private defautl abstract final static)
2、类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(字符串、整数、浮点数)和对其他类型、字段、方法的符号引用。
3、字段信息(该类声明的所有字段)
字段修饰符(public、peotect、private、default)
字段的类型
字段名称
4、方法信息
方法信息中包含类的所有方法。
方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表
5、类变量(静态变量)
6、指向类加载器的引用
7、指向Class实例的引用
8、方法表
9、运行时常量池(Runtime Constant Pool)
总结:
HotSpot JVM中,永久代中用于存放类和方法的元数据以及常量池。每当一个类初次被加载的时候,它的元数据都会被放到永久代中。
方法区 —》运行时常量池 —》字符串常量池的关系:
https://blog.csdn.net/m0_50370837/article/details/124029060
3、类加载的过程

类的加载有三个步骤:
- 加载:通过类装载系统将.class文件加载到运行时数据区,生成class对象
- 链接:链接的过程就是将java的二进制代码合并到jvm的运行状态之中。
- 验证:确保加载的类信息符合jvm的规范,没有安全方面的问题等。
- 准备:为
类变量分配内存,并赋予初始值 - 解析:将常量池中的符号引用(常量名)替换为直接引用(地址)的过程
- 初始化:类的初始化就是执行
<clinit>方法的过程,<clinit>方法由静态变量显式赋值代码和静态代码块组成,类变量显式赋值代码和静态代码块从上到下顺序执行。
什么时候会发生类初始化?
- 类的主动引用(一定会发生类的初始化)
- 当虚拟机启动,先初始化main方法所在的类
- new 一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
- 类的被动引用(不会发生类的初始化)
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化。
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
字面量:
- 文本字符串
- 八种基本数据类型的值
- 被声明为final的常量
符号引用:
- 类和方法的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
4、什么是双亲委派机制
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。最后没有找到会抛出ClassNotFoundException异常。
5、类加载器的类别
BootstrapClassLoader(启动类加载器)
c++编写,加载JAVA_HOME/lib目录下的jar包,java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
ExtClassLoader (标准扩展类加载器)
java编写,加载扩展库,如classpath中的jre ,javax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
AppClassLoader(系统类加载器)
java编写,加载程序所在的目录,如user.dir所在的位置的class,classpath下的类
CustomClassLoader(用户自定义类加载器)
java编写,用户自定义的类加载器,可加载指定路径的class文件
如何打破双亲委派模型?
- 自定义类加载器,继承ClassLoader类,重写loadClass方法。双亲委派逻辑都写在loadClass中。
如何自定义类加载器:
所有用户自定义类加载器都应该继承ClassLoader类。
在自定义ClassLoader的子类是,我们通常有两种做法:
- 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
- 重写findClass方法 (推荐)

使用场景:
- 字节码加密与解密(经典场景)
class 文件加密后,普通 ClassLoader 读出来是乱码无法加载。
自定义类加载器读取加密字节,在 findClass 里解密,再调用 defineClass 生成 Class。
防止 class 反编译,保护业务代码。
- 热部署 / 热加载
不停服务,加载新版本 class,替换旧类。
原理:同一个类名,用新的类加载器加载,生成全新 Class。
例子:SpringBoot DevTools、JRebel、很多中间件热更新。
重点:不能用同一个类加载器,同一个加载器类只会加载一次。
- 类隔离(非常高频,Tomcat 核心)
不同模块 /war 包,存在全类名相同,但版本不同的类。
比如:web1 用 guava18,web2 用 guava30。
自定义类加载器,每个应用一个独立类加载器,互相隔离,不会冲突。
Tomcat 的 WebappClassLoader 就是破坏双亲委派做类隔离。
- 从非本地文件来源加载字节码
不只是磁盘上的 class 文件:
- 从网络(http)下载字节码
- 数据库读取字节数组
- 内存中直接生成字节码(ASM、Javassist 动态生成 class)
比如一些动态脚本框架、规则引擎。
- 扩展 JVM 类加载机制,做自定义增强
在加载类的时候做拦截:
- 字节码修改(Agent 也可以,但类加载器也能做)
- 加载日志、权限校验,控制哪些类允许加载
6、为什么要设计双亲委派机制?
- 避免重复加载,父加载器已经加载了该类,子加载器就没必要再加载了
- 避免核心类库被篡改,自己写的全限定类和核心类相同,不会被记载。
7、堆的描述
在HotSpot JVM中,堆被分为三个部分:
- 新生代(默认比:8:1:1)
- 伊甸园区(Eden)
- 幸存0区(from)
- 幸存1区(to):这个区永远都是空的。
- 老年代
- 方法区(永久代/元空间)(逻辑上属于堆,物理上跟堆是独立开的,所以有叫非堆内存)
先看下java的堆内存是如何划分的:
-
初始堆大小(-Xms):指定JVM堆的初始大小,一般设置成最大堆一样。
-
最大堆大小(-Xmx):指定JVM堆的最大大小,默认为物理内存的60%。
-
持久代大小(-XX:MaxPermSize):指定JVM持久代的大小。
-
堆中年轻代与老年代的比例 = 1:2
-
年轻代中的eden区与幸存0区与幸存1区的比例 = 8:1:1

-
JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
-
年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
-
堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
-
非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
为什么需要suvivor区呢?
survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
对象的分代年龄存储在对象头的分代年龄字段中。
为什么survivor分为两块相等大小的幸存空间?
设置两个 Survivor 区最大的好处就是解决内存碎片化。
我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。
这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。
8、对象分配的流程

正常流程:
- 首先新创建的对象会被放到Eden区。
- 当Eden区满后,会进行一次Minor GC,清理Eden区的无引用对象,将存活的对象放到幸存to区,Minor GC也会清除幸存from区的无引用对象,存活的幸存from区的对象有两个地方可以去,如果幸存from区的对象经历了16次Minor GC还存活者,会被放到老年区,否则放到幸存to区,然后to区和from区职责对调。保证to区永远是空的。
注意:幸存区满了,不会进行gc
特殊流程:
- 超大对象,当Eden区满后进行Minor GC之后,该对象还放不下,说明该对象的大小比Eden区的大小还大,然后判断老年区是否放的下该对象,如果放得下就放,放不下就进行一次Major GC,回收老年代的无用对象,如果放的下就放,放不下就OOM。
- 当Eden区满了,会进行Minor GC,而Servior区会对无用对象进行回收,Servior区的回收是被动的,是由Eden区满后,进行Minor GC被动触发的。当Eden的存活对象放到幸存区时,会判断幸存区是否满了,如果没满就放到to区,满了就直接晋升老年区。如果老年区空间不足会进行Major GC,再次判断不足会OOM。
9、JVM堆内存常用参数和调优工具
| 参数 | 描述 |
|---|---|
| -Xms | 堆内存初始大小,单位m、g |
| -Xmx(MaxHeapSize) | 堆内存最大允许大小,一般不要大于物理内存的80% |
| -XX:PermSize | 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了 |
| -XX:MaxPermSize | 非堆内存最大允许大小 |
| -XX:NewSize(-Xns) | 年轻代内存初始大小 |
| -XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
| -XX:SurvivorRatio=8 | 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1 |
| -Xss | 堆栈内存大小 |
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
- jconsole:用于对 JVM 中的内存、线程和类等进行监控;
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
10、一次GC的流程
https://www.jianshu.com/p/0eeca7df3355
11、Minor GC、Major GC、Full GC的触发时机
Minor GC:
- 当年轻代空间不足时,会触发Minor GC,这里的年轻代指的是Eden区满了,survivor区满不会引发GC。
- 因为Java对象大多是朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
Major GC:
- 指老年代的空间不足,出现Major GC,经常会伴随至少一次Major GC,但并不是绝对的。
- Major GC的速度一般比Minor GC慢10倍以上,STW的时间更长。
- 如果Major GC之后,内存还不足,就报OOM。
Full GC:
- 整堆回收,会回收年轻代,老年代,方法区的空间。
- 调用System.gc()时,系统建议执行Full GC,但是不一定执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 有Eden区,survivor0区向survivor1区复制时,对象大小大于survivor1区可用内存,则把对象存到老年代,且老年代的可用内存大于该对象大小
说明:
- Major GC一般指Full GC,目前只有CMS收集器会单独收集老年代的空间。
- full gc是开发和调优中尽量要避免的,这样暂停时间会短一些。
12、怎么判断一个对象是否是可回收的,有哪些方法?
1. 引用计数法
每个对象有一个引用计数器,当对象被引用则计数器+1,当对象引用失效,则计数器-1,当引用计数器的值为0,意味着该对象是垃圾对象,可以回收。但是目前主流的虚拟机没有采用这个算法,因为它很难解决对象之间的相互引用问题。对象之间的相互引用问题如下图:

2. GC Roots方法
将“GC Roots”对象作为起始点,从这些节点开始向下搜索引用对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象,GC Roots根节点:虚拟机栈的本地变量、方法区的静态变量、方法区的常量、本地方法栈的变量等等。

13、垃圾回收算法有哪些?
1. 标记-清除算法
标记清除算法分为两部分:标记和清除,首先标记可回收的对象,在标记完成之后统一回收被标记的对象,同时会产生不连续的内存空间,这将导致以后有大对象需要分配内存空间时,无法找到足够大的内存空间,而不得已触发GC。会产生内存碎片。

2. 复制算法
复制算法是将内存划分为相等的两块,每次只使用一块,当一块内存用完了,就将存活的对象复制到另一块上,然后把原来使用的那块内存空间一次性清理掉,这样每次对半块内存进行回收,不用考虑内存碎片,简单高效。缺点是总有一半的内存空间没被使用。浪费内存空间。

3. 标记-整理算法
标记-整理算法也分为两部分:标记和整理,首先标记存活的对象,再将存活的对象移到一端,然后清除掉边界以外的内存。此方法避免了标记-清除算法的碎片问题,也避免了复制算法空间浪费的问题。但是效率低。

一般年轻代中执行GC后,会有少量对象的存活,就会选用复制算法,只需付出少量存活对象的复制成本,就可以完成垃圾回收。而老年代的对象存活率高,而且没有额外过多的内存空间分配,就使用标记-整理算法来进行垃圾回收。
14、垃圾收集器


- 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
- 其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
- (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
- (绿色虚线)JDK14中:弃用Parallel Scavenge和Serialold GC组合(JEP366)
- (绿色虚框)JDK14中:删除CMS垃圾回收器(JEP363)


串行回收器:Serial、Serial old
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G1
https://blog.csdn.net/shlgyzl/article/details/95041113
CMS收集器:
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
cms垃圾收集器主要四阶段:初始标记—>并发标记—>重新标记—>并发清除
- 初始标记(会STW):找出并标记GCRoot对象和GCRoot直接关联对象,会STW
- 并发标记(不会STW):采用三色标记法对对象进行标记,区分出哪些是垃圾对象,哪些是非垃圾对象,会存在漏标,不会STW
- 重新标记(会STW):对并发标记阶段漏标对象进行修正,会STW
- 垃圾对象 —> 非垃圾对象:随着应用程序的运行,垃圾对象被已经扫描的黑色非垃圾对象重新引用,此时该对象不应该被回收
- 非垃圾对象 —> 垃圾对象:随着应用程序的运行,非垃圾对象的引用被断开了,成为垃圾对象,这种对象称为浮动垃圾,下次gc回收即可
- 并发清除(不会STW):对标记的垃圾对象进行清除,不会STW
cms的优点:
- 低延迟
- 并发收集
cms的缺点:
- 产生内存碎片,当内存空间不足时,不得不触发FullGC
- cms收集器对cpu资源非常敏感,并发阶段,虽然不会卡顿,但是会占用一部分线程导致用户程序变慢,吞吐量下降
- 会产生浮动垃圾
G1收集器:
G1垃圾收集器追求在可控的延迟停顿时间下,尽可能高的吞吐量。
G1回收器采用面向局部收集的设计思路和基于Region的内存布局形式,实现停顿时间模型,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换CMS,成为一种全功能收集器。G1在JDK9之后称为服务端模式下的默认垃圾回收器,取代Parallel Scavenge+Parallel Old的默认组合,而CMS被声明为不推荐使用的垃圾回收器。G1从整体来看是基于【标记-整理】算法实现的回收器,但从局部(两个Region之间)上看又是基于【标记-复制】算法实现的。
G1收集器的收回过程大致分为4步:
- 初始标记(STW):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(不会STW):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(STW):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(STW(多线程)):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。(标记-整理)
从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的。
优点:
可控的垃圾回收停顿时间
缺点:
- 每个region都要维护卡表,占用内存高,更适合高内存的机器。
- CMS收集器使用写后屏障来更新维护卡表;而G1收集器除了使用写后屏障维护卡表,为了实现SATB(原始快照)的算法,还需要使用写前屏障来跟踪并发时指针变化情况。带来额外的负担。
G1在处理大对象时:如果对象大小 > Region大小的50%,G1会分配连续的多个Region来存放这个大对象,这些Region会被特殊标记为Humongous Region,Humongous Region只能在Full GC时被回收。
除了跨代引用外,对象引用关系改变,如何解决?
对象引用关系改变
解决的办法我们之前在讲「三色标记算法」的时候提过,G1使用「原始快照」来解决这一问题。
垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建。
G1为每一个Region设计了两个名为「TAMS(Top at Mark Start)」的指针。
把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。
与CMS中的「Concurrent Mode Failure」失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间Stop The World。
G1可以通过-XX:MaxGCPauseMillis参数设置垃圾收集的最大停顿时间的JVM参数,单位为毫秒。
在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。
然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
G1收集器会根据这个设定值进行自我调整以尽量达到这个暂停时间目标。例如,如果设定了-XX:MaxGCPauseMillis=200,那么JVM会尽力保证大部分(但并非全部)的GC暂停时间不会超过200毫秒。
三色标记:
我们知道cms和G1在并发标记阶段采用的是三色标记法:黑色、灰色、白色
- 黑色:当前对象和孩子对象都已经GC访问过了
- 灰色:当前对象被GC访问过,但是孩子对象还没有完全被GC访问过
- 白色:没有被GC访问过,如果全部标记已完成,还是白色,那么就是垃圾对象
由于一些垃圾回收器存在垃圾回收线程和用户线程并发的情况(例如CMS的并发阶段),那么三色标记会有两个问题:
- 一种是把原本消亡的对象错误标记为存活,这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好,问题不大。
- 另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误。
第一点无伤大雅,所以我们解决问题的重心放到第二点上。
1994年理论上被证明了,「当且仅当以下两个条件同时满足时」,会产生「对象消失」的问题,即原本应该是黑色的对象被误标为白色:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用。
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
其实一句话说白了就是:「跟灰色对象断开连接,跟黑色对象建立连接」。
因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件中的任意一个即可。
由此分别产生了两种解决方案:「增量更新(Incremental Update)」和「原始快照(Snapshot At The Beginning,SATB)」。
这两个解决方案各破坏一个条件。
增量更新(CMS)
增量更新要破坏的是第一个条件。
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,(重新标记阶段)再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
原始快照(SATB)(G1)
原始快照要破坏的是第二个条件。
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,(最终标记阶段)再将这些记录过的引用关系中的白色对象为根,重新扫描一次。
CMS使用增量更新,G1使用原始快照解决标记的问题。CMS和G1都使用三色标记法进行可达性分析遍历对象图过程中遇到的对象。
https://www.cnblogs.com/meixiaoyu/articles/16669367.html
跨代引用:
跨代引用是指新生代中存在对老年代对象的引用,或者老年代中存在对新生代的引用。
假如现在要进行一次只局限于新生代区域的YGC,但新生代中的对象是完全有可能被老年代所引用的,为了找到新生代中的存活对象,不得不遍历整个老年代来确保可达性分析结果的正确性。这种跨代引用是很少见的,但是为了这点跨代引用的对象,要遍历整个非收集堆,那么就得不偿失了,所以JVM为了解决这个问题,引入了记忆集。
记忆集
记忆集是抽象概念:记录【非收集区域 → 收集区域】的跨区域指针集合,作用是避免把整个非收集区域全部纳入 GC Roots 扫描范围。记忆集是一种逻辑上的概念,并没有规定具体的实现,类似方法区。
- CMS 场景:记忆集这个抽象,依托全局卡表实现,没有按区域拆分的 RSet;
- G1 场景:每一个 Region(Eden/Survivor/ 老年代 / Humongous)都拥有自己独立的 RSet(RSet 就是 G1 里记忆集的具体实现)。
卡表
卡表(Card Table):是实现记忆集的一种底层方案,CMS、G1 都只有一张全局卡表
卡表本质是一个字节数组。数组里每一个元素,对应堆中一块固定大小内存,这块内存叫卡页(Card Page,固定 512 字节)。
一个卡页里可以存放多个对象。只要卡页内任意一个对象的字段存在跨区域引用,这个卡表元素标记为 1,代表脏卡(Dirty Card);没有跨区引用则标记为 0。
卡表元素如何维护?何时变脏?谁来把它们变脏?
如何变脏?即如何在对象赋值的那一刻去更新维护卡表。
引用字段赋值时,依靠**写屏障(Write Barrier)**实现,可以理解为虚拟机层面给「引用赋值」增加的 AOP 切面。
写屏障分为:
- 写前屏障(Pre-Write Barrier):赋值之前执行逻辑
- 写后屏障(Post-Write Barrier):赋值之后执行逻辑
- CMS:只有写后屏障,用来标记脏卡,维护卡表,处理跨代引用;CMS 没有 RSet。
- G1:
- 维护脏卡(卡表、给 RSet 喂数据):写后屏障;
- SATB 原始快照并发标记(记录要删除的旧引用):写前屏障。
只要开启写屏障,每次引用赋值都会带来一点点额外开销;但对比 YGC 扫描整个老年代的开销,这个代价可以接受。
G1 的全局卡表用来做轻量无锁的脏卡标记、充当异步缓冲,避免业务线程在写屏障里直接操作 RSet 产生激烈锁竞争,再由 Refine 线程消费脏卡,整理构建出各个 Region 的 RSet。
15、内存溢出与内存泄露?
内存溢出:程序,对象在申请内存空间时,没有足够的内存空间可以使用,导致OOM。
内存泄露:在程序申请内存空间后,没有及时的释放,导致内存被占用,一次内存泄露无关紧要,但是内存泄露的堆积结果严重,会导致内存被占用的越来越多,最后导致没有内存可用,而发生内存溢出。
16、深拷贝与浅拷贝?
浅拷贝(shallowCopy)在复制对象时,只复制引用地址,将新的引用指原对象所在的内存空间而已。
深拷贝(deepCopy)在复制对象时,会开辟一个空间,将对象内存空间的东西复制一份到新的内存空间中,然后新的引用指向新的内存空间。
java克隆对象的四种方式:
- 手动创建对象 -深拷贝
- 反射 -深拷贝
- 重写clone方法 -浅拷贝
- 序列化 -深拷贝
17、Java中都有哪些引用?
- 强引用:发生 gc 的时候不会被回收。
- 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
- 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

18、Java父类与子类的加载顺序
(1) 父类静态代码块(包括静态初始化块,静态属性,但不包括静态方法)
(2) 子类静态代码块(包括静态初始化块,静态属性,但不包括静态方法 )
(3) 父类非静态代码块( 包括非静态初始化块,非静态属性 )
(4) 父类构造函数
(5) 子类非静态代码块 ( 包括非静态初始化块,非静态属性 )
(6) 子类构造函数
https://blog.csdn.net/w893932747/article/details/86180500
19、JVM性能调优
JVM调优调的是什么?
JVM 调优 = 在“堆大小、收集器、并发度、停顿目标、堆外内存”五个旋钮之间反复权衡,直到把「GC 停顿时长」「GC 频率」「OOM 风险」三条曲线压进业务的限制矩形框里。
JVM参数的三种类型:
- 标准参数(-)
- 特点:所有 JVM 实现都必须支持的参数,相对稳定
- 示例:
-version # 显示JVM版本信息 -help # 显示帮助信息 -server # 使用服务器模式(默认) -client # 使用客户端模式(Java 8及之前) -cp <classpath> # 指定类路径 -D<name>=<value> # 设置系统属性
- 非标准参数(-X)
- 特点:特定于 HotSpot JVM,不保证所有 JVM 都支持
- 示例:
-Xms512m # 初始堆大小 -Xmx2g # 最大堆大小 -Xss256k # 线程栈大小 -Xmn1g # 年轻代大小 -Xloggc:gc.log # GC日志输出路径
- 高级/非稳定参数(-XX)
- 特点:用于 JVM 调优和调试,可能会随版本变化
- 两种形式:
- 布尔类型:
-XX:+<option>启用,-XX:-<option>禁用 - 键值类型:
-XX:<option>=<value>
- 布尔类型:
常用调优参数详解
1. 内存相关参数
# 堆内存设置
-Xms4g # 初始堆大小(推荐与-Xmx相同,避免动态调整)
-Xmx4g # 最大堆大小(不超过物理内存的50%-80%)
-Xmn2g # 年轻代大小(通常为堆的1/3到1/2)
# 元空间设置
-XX:MetaspaceSize=256m # 初始元空间大小
-XX:MaxMetaspaceSize=512m # 最大元空间大小
# 直接内存
-XX:MaxDirectMemorySize=256m
# 栈大小
-Xss256k # 每个线程栈大小(默认1M,调小可创建更多线程)
2. 垃圾收集器参数
G1 收集器(JDK 9+ 默认)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间
-XX:G1HeapRegionSize=4m # Region大小(1-32M)
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用率
并行收集器(吞吐量优先)
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=8 # GC线程数(默认=CPU核心数)
-XX:MaxGCPauseMillis=100 # 最大停顿时间目标
-XX:GCTimeRatio=99 # 吞吐量目标(GC时间:应用时间=1:99)
CMS 收集器(低延迟,JDK 14+ 已移除)
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75 # 触发CMS的堆占用率
-XX:+UseCMSInitiatingOccupancyOnly # 禁用JVM自动调整
3. GC日志与分析参数
# 详细GC日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=10M
# 或使用传统格式(Java 8及之前)
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:gc.log
# 内存溢出时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps
-XX:OnOutOfMemoryError="kill -9 %p" # 内存溢出时执行脚本
4. JIT编译器参数
-XX:+TieredCompilation # 启用分层编译(默认开启)
-XX:CompileThreshold=10000 # 方法调用多少次后编译
-XX:+PrintCompilation # 打印编译信息
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining # 打印方法内联信息
5. 其他重要参数
# 性能监控
-XX:+PrintFlagsFinal # 打印所有参数最终值
-XX:+PerfDisableSharedMem # 禁用共享内存,解决perf.data占用/tmp空间
# 大页面支持(需要系统配置)
-XX:+UseLargePages
-XX:+UseTransparentHugePages
# 类加载相关
-XX:+TraceClassLoading # 跟踪类加载
-XX:InitialCodeCacheSize=32m # 初始代码缓存大小
-XX:ReservedCodeCacheSize=256m # 代码缓存大小
# 字符串去重
-XX:+UseStringDeduplication # 减少重复字符串内存占用
常见调优场景与配置
| 场景 | 典型现象 | 立竿见影的旋钮(JDK8/11/17 通用) |
|---|---|---|
| 1. 高并发网关/微服务 P99 偶发 500 ms | 日志出现 “User=0.20s, Sys=0.02s, Real=0.25s” 且对应 GC 日志 “Pause Young 240 ms” | 换 G1:-XX:+UseG1GC -XX:MaxGCPauseMillis=100 |
| 2. 大内存(32 G+)压测吞吐上不去 | CPU 90 % 仍嫌慢,GC 日志显示 每秒一次 Young GC,每次 20 ms | 换 Parallel:-XX:+UseParallelGC -Xmn24g |
| 3. 网关 4 GB 容器一启动就被 OOM-kill | RSS 瞬间飙 5 GB,容器 -m 4 G | 降堆:-Xmx2g 直接内存:-XX:MaxDirectMemorySize=1g |
| 4. Netty 文件下载 Direct buffer OOM | 异常 Direct buffer memory 且 -XX:MaxDirectMemorySize 未设置 | 显式加:-XX:MaxDirectMemorySize=2g |
| 5. 类反射/脚本引擎 Metaspace OOM | java.lang.OutOfMemoryError: Metaspace | 放大:-XX:MaxMetaspaceSize=512m |
| 6. 启动 5 min 后 JIT 编译变慢 | 日志 CodeCache is full | 代码缓存:-XX:ReservedCodeCacheSize=512m |
| 7. 大对象数组分配失败 | GC overhead limit exceeded 或 Java heap space | 直接升堆:-Xmx8g 或换 ZGC:-XX:+UseZGC |
| 8. 离线跑批 64 G 机器 Young GC 太频繁 | 每 2 s 一次,总停顿 8 % | 放大新生代:-Xmn48g |
| 9. 云函数 512 MB 容器冷启动慢 | 初始化要 3 s | 降堆+即时编译:-Xmx256m -XX:TieredStopAtLevel=1 |
| 10. 系统监控想动态看 GC | 不想重启加日志 |
调优建议与最佳实践
- 调优步骤

- 监控工具
- 命令行工具:jstat, jmap, jstack, jcmd
- 图形化工具:VisualVM, JConsole, JMC(Java Mission Control)
- 生产级:APM(Application Performance Monitoring)工具,如Pinpoint, SkyWalking, Prometheus + Grafana
- 参数设置黄金法则
- 堆大小:-Xms 和 -Xmx 设置相同值,避免运行时调整,占物理内存60%左右。
- 年轻代:占总堆的1/3到1/2,根据对象生命周期调整
- 幸存区:足够大以减少过早晋升,但也不宜过大
- 监控先行:先收集足够监控数据,再针对性调优
- 循序渐进:一次只调整1-2个参数,观察效果后再继续
- 常见问题与解决
- 频繁Full GC:检查内存泄漏,调整老年代大小或GC策略
- 长GC停顿:考虑使用G1或ZGC,调整停顿时间目标
- CPU使用率高:检查是否频繁GC,或JIT编译占用资源
- 内存溢出:分析堆转储,检查大对象或内存泄漏
| 部署模式 | 物理内存 | 推荐堆大小范围 | 关键参数示例 | 注意事项 |
|---|---|---|---|---|
| 物理机/VM | 16 GB | 8 GB - 12 GB | -Xms10g -Xmx10g | 为OS文件缓存留足2-4G |
| 容器 | 容器限制 4 GB | 2 GB - 2.8 GB | -XX:MaxRAMPercentage=70.0 | 必须设置容器内存限制,JDK 8u191+ |
| 大内存机器 | 64 GB | 40 GB - 50 GB | -Xms45g -Xmx45g | 考虑使用G1GC的-XX:G1HeapRegionSize=16m |
| 内存敏感型 | 32 GB | 16 GB - 20 GB | 小堆+大年轻代+低延迟GC | 优先保证GC停顿时间,而非堆大小 |
- JDK版本选择建议
- JDK 8:使用G1需要手动启用(-XX:+UseG1GC)
- JDK 11:G1成为默认,支持ZGC(实验性)
- JDK 17+:ZGC和Shenandoah成熟,适合大内存低延迟场景

https://www.yuque.com/u21195183/jvm/pgrt7e
JVM常见问题:
一、OOM 内存溢出
- 堆 OOM (Java heap space)
原因:对象过多、内存泄漏、堆上限太小
方案:dump 堆快照,MAT 定位泄漏对象,合理调大 Xmx,避免一次性加载大量数据 - 直接内存 OOM (Direct buffer memory)
原因:Netty/NIO ByteBuffer 未释放
方案:规范 ByteBuf 的 release,监控直接内存,合理配置 MaxDirectMemorySize - 元空间 OOM (Metaspace)
原因:动态生成类多、类加载器泄漏
方案:排查 CGLIB/ASM/ 热部署,检查自定义类加载器,按需设置 MaxMetaspaceSize - 线程 OOM (unable to create new native thread)
原因:线程数量超限、Xss 栈过大
方案:使用线程池,限制线程数量,调小 - Xss,检查系统 ulimit 线程上限
二、GC 异常
- FullGC 频繁、STW 停顿长
原因:老年代上涨快、晋升失败、大对象碎片、内存泄漏
方案:排查内存泄漏,优化大对象,调整 G1 参数,减少对象晋升到老年代 - Promotion failed 晋升失败
原因:YGC 后存活对象,老年代没有足够连续空间存放
方案:降低对象晋升速率,减少短生命周期对象进老年代,减少内存碎片
三、内存泄漏
- ThreadLocal 内存泄漏
原因:线程池复用线程,value 无法回收
方案:使用完 ThreadLocal 必须调用 remove () - 静态集合泄漏
原因:static 集合持续 add,没有清理逻辑
方案:增加过期清理、容量上限,避免长生命周期持有大量对象 - 自定义类加载器泄漏
原因:ClassLoader 被引用不释放,加载的类无法卸载
方案:保证类加载器没有强引用,用完及时释放
四、CPU 过高
- CPU 过高(GC 导致)
原因:GC 频繁,GC 线程占用 CPU
方案:定位内存泄漏,优化对象产生速度,减少 GC 频率 - CPU 过高(业务代码)
原因:死循环、正则回溯、密集计算、锁自旋
方案:jstack/Arthas 定位高 CPU 线程,优化循环与正则,减少自旋竞争
五、线程阻塞
- 死锁
原因:锁顺序不一致
方案:统一加锁顺序,使用 tryLock 带超时 - 锁竞争阻塞
原因:大量线程争抢同一把锁
方案:缩小锁粒度,分段锁,减少同步区域 - 连接池耗尽阻塞
原因:连接获取后未归还
方案:规范资源关闭,配置连接池上限与超时
常见得服务调优参考:
| 服务类型 | 负载特征 | 堆大小参考 | G1 停顿目标 | 调优核心重点 |
|---|---|---|---|---|
| 网关 | IO 密集、Netty、直接内存多、延迟敏感 | 1~3G,固定堆 | 50ms | 管控 Direct 内存,ByteBuf 释放,低 STW |
| 登录鉴权 | QPS 中等,token 校验、少量本地缓存 | 1~2G | 100ms | 本地缓存过期淘汰,防止老年代缓慢增长 |
| 导入导出 | 批量任务、大对象、潮汐流量 | 4~6G | 200~300ms | 业务分批,限制并发,减少 Humongous 大对象 |
| 普通业务 CRUD | 对象混杂,DB 查询,业务逻辑多 | 2~4G | 100ms | 监控晋升速率,排查 ThreadLocal / 静态集合泄漏 |
20、什么是JMM
《Java虚拟机规范》[1]中曾试图定义一种“Java内存模型”(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
定义了Java程序中多线程并发访问共享内存时的行为规范。JMM规定了线程如何和主内存、工作内存进行交互,以及如何保证多线程之间的可见性、有序性和原子性。JMM保证了多线程程序的正确性和可靠性。
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。
Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量
才可以被其他线程锁定。 - read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以
便随后的load动作使用。 - load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的
变量副本中。 - use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚
拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 - assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 - store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随
后的write操作使用。 - write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的
变量中。
如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。注意,Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。也就是说read与load之间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。除此外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内
存不接受,或者工作内存发起回写了但主内存不接受的情况出现。 - 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回
主内存。 - 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存
中。 - 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或
assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。 - 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执
行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。 - 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量
前,需要重新执行load或assign操作以初始化变量的值。 - 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个
被其他线程锁定的变量。 - 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
这8种内存访问操作以及上述规则限定,再加上volatile的一些特殊规定,就已经能准确地描述出Java程序中哪些内存访问操作在并发下才是安全的。
21、JMM在java中的一些具体实现:
HotSpot JVM对JMM的具体实现主要依赖于以下关键字和技术:
-
volatile关键字:在HotSpot JVM中,使用volatile关键字修饰的变量可以确保可见性和有序性。当一个线程对volatile变量进行写操作时,会立即刷新到主内存,并通知其他线程刷新缓存。当一个线程对volatile变量进行读操作时,会从主内存中读取最新的值。
-
synchronized关键字:HotSpot JVM使用synchronized关键字来实现锁机制,确保对共享变量的原子性操作。synchronized关键字可以用于方法、代码块或对象实例上。当一个线程进入synchronized块时,它会获取对象的锁,并执行同步代码。其他线程必须等待锁释放后才能进入同步块。
-
内存屏障(Memory Barrier):HotSpot JVM使用内存屏障来保证指令的有序性。内存屏障是一种特殊的指令,用于限制指令重排序和缓存刷新等操作。在HotSpot JVM中,内存屏障主要包括LoadLoad、LoadStore、StoreLoad和StoreStore等屏障,用于保证不同指令之间的顺序关系。
-
原子变量类(Atomic Variable):HotSpot JVM提供了一系列的原子变量类,如AtomicInteger、AtomicLong等。这些类使用了CAS(Compare and Swap)操作,确保对变量的原子性操作。CAS操作是一种无锁的原子操作,它通过比较变量的当前值和期望值来确定是否进行更新。
-
JIT编译器(Just-In-Time Compiler):HotSpot JVM的JIT编译器对代码进行优化,并对内存访问进行重排序。JIT编译器会根据代码的执行情况进行动态编译,以提高程序的执行效率。在进行重排序时,JIT编译器会遵循JMM的规范,确保代码的执行顺序符合预期。
这些关键字和技术是HotSpot JVM实现JMM的重要组成部分。它们共同确保了多线程程序在HotSpot JVM上的正确性和可靠性。在编写多线程程序时,合理使用这些关键字和技术是非常重要的。
- LoadLoad Barriers 示例:
Load1; LoadLoad; Load2 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作 - StoreStore Barriers 示例:
Store1; StoreStore; Store2 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作 - LoadStore Barriers 示例:
Load1; LoadStore; Store2 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作 - StoreLoad Barriers 示例:
Store1; StoreLoad; Load2 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)