深入理解 JVM

发布于 2024-12-18 22:22:20 字数 30140 浏览 0 评论 0

1. 运行时数据区域

  • Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

  • 线程私有的:虚拟机栈,本地方法栈,程序计数器
  • 线程共享的 方法区,堆

程序计数器

  • 程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码行号指示器,在虚拟机的概念模型里,字节码解释器工作时 就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要这个计数器来完成。(如果正在执行 的是本地方法则计数器为空)。

Java 虚拟机栈

  • 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

Java 堆

  • Java 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。
  • 这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用 分代回收算法 ,所有堆内存也分为 新生代老年代 ,可以方便垃圾的准确回收。

方法区

  • 方法区主要用于存放已经被虚拟机加载的类信息,如 常量,静态变量 ,即时编译器编译后的代码等。和 Java 堆一样不需要连续的内存,并且可以动态扩展。
  • 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

运行时常量池

  • 运行时常量池是方法区的一部分。class 文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,会在类加载后放入这个区域。

直接内存

  • 直接内存并不是虚拟机运行时数据区域的一部分。
  • 在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

2. Minor GC 和 Full GC

  • Minor GC:指发生在新生代的垃圾收集动作,因为 Java 对象大多都具
    备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • Major GC 或 Full GC:指发生在老年代的 GC,出现了 Major GC,经常
    会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
    就有直接进行 Major GC 的策略选择过程) 。 MajorGC 的速度一般会比 Minor GC 慢 10
    倍以上。

Minor GC 触发机制

当年轻代满时就会触发 Minor GC,这里的年轻代满指的是 Eden 代满,Survivor 满不会引发 GC

Full GC 触发机制:

  • 当年老代满时会引发 Full GC,Full GC 将会同时回收年轻代、年老代,
  • 当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载

3. Java 中的四种引用

强引用,软引用,弱引用,虚引用

强引用

就是指在程序代码中普遍存在的,类似 Object obj=new Object() 这类的引用,只要强引用还存在,垃圾回收期永远不会回收掉被引用的对象

软引用

用来描述一些还有用,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出前,将会把这些对象列进回收范围之内并进行第二次回收,如果这此次回收还是没有足够的内存,才会抛出内存溢出。

弱引用

用来描述非必须的对象,但是它的强度比软引用更弱一下,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,只会回收被弱引用关联的对象

虚引用

被称为幽灵引用或幻引用,是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其它生存时间构成影响,也无法通过虚引用来取得一个实列。为一个对象设置虚引用的目的就是在对象被回收时收到一个系统通知。

4. 垃圾收集算法

Serial 收集器

  • 一个单线程的收集器,只会使用一个 CPU 或一条收集线程去完成垃圾收集工作。在进行垃圾收集时必须暂停其它所有的工作线程,直接到结束。(Stop The Word) 这项工作是虚拟机在后台自动发起和完成的。
  • JDK1.3 之前是新生代收集的唯一选择。
  • 它依然是虚拟机运行在 Client 模式下的默认新手代收集器,简单而高效。

ParNew 收集器

Serial 收集器的多线程版本,使用多条线程收集。其余的和 Serial 一样,是许多运行在 Server 模式下的虚拟机首选新生代收集器。且目前除了 Serial 收集器,只有它可以与 CMS 收集器配合工作

3.Parallel Scavenge 收集器

  • 它是一款新生代收集器。使用复制算法收集,又是并行的多线程收集器
  • 特点是达到一个可控制的吞吐量,也被称为“吞吐量优先”收集器。

Serial Old 收集器

  • 它是 Serial 收集器的老年代版本,是一个单线程收集器,使用标记-整理算法收集。
  • 主要意义是给 Client 模式下虚拟机使用。如果是 Server 模式,则有两种用途,一是在 JDK1.5 之前与 Parallel Scavenge 收集器搭配使用。二是作为 CMS 收集器的后背预案

Parallel Old 收集器

它是 Parallel Scavenge 收集器的老年代版本,使用多线程和标记-整理算法。JDK1.6 才开始提供。

CMS 收集器

  • 是一种以获取最短回收停顿时间的为目标的收集器。基于标记-清楚算法实现。
  • 运作过程分为四个阶段。初始标记,并发标记,重新标记,并发清除。
  • 初始标记和并发标记仍然需要"Stop The Word".初始标记只是记录下 GC Roots 能直接关联到对象,速度快。并发标记就是进行 GC Roots Tracing 过程。重新标记修正并发标记期间因程序继续运作导致标记产生变动的一部分对象的标记记录。整个过程耗时最长是并发标记和并发清除过程。
  • 优点是并发收集,低停顿。缺点是:对 CPU 资源非常敏感,无法处理浮动垃圾。收集结束时会产生大量空间碎片

G1 收集器

  • 当前收集器技术最前沿成果之一。将整个 Java 堆分为多个大小相等的独立区域。虽然保留新生代和老年代,但它们不再是物理隔离,都是一部分不需要连续的集合。
  • 特点是并行与并发充分利用 CPU 缩短停顿时间。分代收集,空间整合不会产生内存空间碎片,可预测的停顿。有计划的避免回收整个 Java 堆。
  • 运行大致分为:初始标记,并发标记,最终标记,筛选回收。

标记-清除算法

  • 算法分为标记和清除两个阶段。首先先标记所有要被回收的对象,标记完成后再统一清除被标记的对象。

主要缺点有两个,

  • 一是效率问题,标记和清除的过程效率都不高。二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多,可能会导致,当程序在以后的运行过程中需要分配较大的对象时无法找到足够的连续内存,而不得不提前出发另一次垃圾收集动作

复制算法

  • 为了解决效率问题,一种复制收集的算法出现了。它将可用内存按容量划分为大小相等的两块,每次只用其中的一块。当这一块内存用完,就将还存活着的 对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中一块进行内存回收,内存分配时也就不用内存碎片等复杂情况,只要 移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半未免太高了一点。

标记-整理算法

  • 复制手机算法在对象存活率较高的时要执行多的复制操作,效率将会变低。更关键的是,如果不想浪费 50%的空间,就需要额外的空间进行分配担保,以 应对被使用的内存中对象都 100%存货的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,有人提出了另一种 标记-整理的算法,标记过程仍然与 标记-清楚算法一样。但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

  • 根据对象的存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生 代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活 率高,没有额外空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来进行回收

5. 内存分配与回收策略

  • 对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的 Eden 区上,如果启动本地线程分配缓冲,将按线程的优先级在 TLAB 上分 配。少数情况也可能分配在老年代中,分配的规则并不是百分之白固定,其细节取决于当前使用的是哪一种垃圾回收期组合,还有虚拟机中于内存相关的参数设置。

对象优先在 Eden 区分配

对象通常在新生代的 Eden 区进行分配,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC,与 Minor GC 对应的是 Major GC、Full GC。

  • Minor GC:指发生在新生代的垃圾收集动作,非常频繁,速度较快。
  • Major GC:指发生在老年代的 GC,出现 Major GC,经常会伴随一次 Minor GC,同时 Minor GC 也会引起 Major GC,一般在 GC 日志中统称为 GC,不频繁。
  • Full GC:指发生在老年代和新生代的 GC,速度很慢,需要 Stop The World。

大对象直接进入老年代

  • 需要大量连续内存空间的 Java 对象称为大对象,大对象的出现会导致提前触发垃圾收集以获取更大的连续的空间来进行大对象的分配。虚拟机提供了-XX:PretenureSizeThreadshold 参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。

长期存活的对象进入老年代

  • 每个对象有一个对象年龄计数器,与前面的对象的存储布局中的 GC 分代年龄对应。对象出生在 Eden 区、经过一次 Minor GC 后仍然存活,并能够被 Survivor 容纳,设置年龄为 1,对象在 Survivor 区每次经过一次 Minor GC,年龄就加 1,当年龄达到一定程度(默认 15),就晋升到老年代,虚拟机提供了-XX:MaxTenuringThreshold 来进行设置。

动态对象年龄判断

  • 对象的年龄到达了 MaxTenuringThreshold 可以进入老年代,同时,如果在 survivor 区中相同年龄所有对象大小的总和大于 survivor 区的一半,年龄大于等于该年龄的对象就可以直接进入老年代。无需等到 MaxTenuringThreshold 中要求的年龄。

具体代码如下:

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=15
        -XX:+PrintTenuringDistribution
     * */
    
    public static void testTenuringThreshold2() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
    
    public static void main(String[] args) {
        testPretenureSizeThreshold2();
    }
}

空间分配担保

  • 发生 Minor GC 时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,若成立,则说明 Minor GC 是安全的,否则,虚拟机需要查看 HandlePromotionFailure 的值,看是否运行担保失败,若允许,则虚拟机继续检查老年代最大可用的 连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次 Minor GC;若小于或者 HandlePromotionFailure 设置不运行冒险,那么此时将改成一次 Full GC,以上是 JDK Update 24 之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。
  • 冒险是指经过一次 Minor GC 后有大量对象存活,而新生代的 survivor 区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把 survivor 区无法容纳的对象直接进入老年代。

回收方法区

  • 很多人任务方法区是没有垃圾回收的,Java 虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而在方法去进行垃圾收集的性价比一般比 较低,在堆中,由其是在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~96%的空间,而永久代的垃圾收集效率远低于此。
  • 永久代的垃圾主要回收两部分内容:废弃常量和无用的类。
  • 回收废弃常量于回收 Java 堆 中的对象非常相似。以常量池中字面量的回收为列,假如一个字符串“ abc "已经进入常量池中,但是当前系统没有任何一个 String 对象叫做” abc "的,换句话就是没有任何 Sting 对象引用常量池中的"abc",也没有其它地方引用了这个字面变量,如果这时候发生内存回收,而且必要的话,这个“ abc "常量就会被系统请出常量池,常量池中的其它类,接口,方法,字段的符号引用也与此类似。

输入图片说明

Java 中对象访问是如何进行的

  • 对象访问在 Java 中无处不在,即时是最简单的访问也会涉及到 Java 栈,Java 堆,方法区这三个最重要的内存区域之间的关系。
Object obj=new Object();
  • 假设这段代码出现在方法体中, 那吗“ Object obj ”这部分的语义将会反应到 Java 栈 的本地变量中,作为一个 reference 类型数据出现。而“ new Object() ”这部分的语义将会反应到 Java 堆 中,形成一块存储了 Object 类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定的。
  • 另外,在 Java 堆中还必须包含能查找到此对象类型数据(如对象类型,父亲,实现的接口,方法等)的地址消息,这些类型数据则存储在方法区中。

怎样判断对象是否存活

  • 是否使用引用计数法?很多判断对象存活的算法是这样的,给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1,当引用失效时,计数器减 1;
  • 任何时刻计数器都为 0 的对象就是不可能再被使用的。客观的来说,引用计数法的实现简单,判定效率也很高,在大部分情况下是一个不错的算法,也有一 些著名的案例,列如微软的 COM 技术,但是,在 Java 语言中没有选用引用技术发来管理内存,其中最主要的原因是因为它很难解决对象之间的互循环引用问 题。

摘抄自<<深入理解 Java 虚拟机>>一书中的原话

  • 根搜索算法:Java 是使用根搜索算法判断对象是否存活的。
  • 这个算法的思路就是通过一系列的名为“GC roots"的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象的 GC roots 没有任何引用链相连时,则证明此对象是不可用的。如下图所示,对象 object5,object6,object7 虽然相互关联,但是他们的 GC roots 是不可达到的,所以它们将会被判定是可回收的对象。

输入图片说明

作为 GC roots 的几种对象

  • 虚拟机栈(栈中的本地变量表) 中的引用对象。
  • 方法区中的类静态属性引用对象。
  • 方法区中的常量引用的对象。
  • 本地方法中 JNI(即一般说的 native 方法)的引用的对象。

6. 虚拟机类加载机制

类加载的时机

  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备 (Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和卸载 (Unloading)7 个阶段。其中准备、验证、解析 3 个部分统称为连接(Linking)
    输入图片说明

  • 加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。

何时开始类加载的第一个阶段

  • java 虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行初始化(而加载,验证,准备自然需要再次之前开始)
  1. 遇到 new,getstatic,pustaticinvokestatic 这 4 条字节 码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见 Java 代码场景是:使用 new 关键字实例化对象,读取或设置一个类的 静态字段(被 final 修饰,已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类时,如果发现父类还没有初始化,则需要先触发父类初始化。
  4. 当虚拟机启动时,用户指定一个执行的主类,虚拟机会先初始化这个主类。
  5. 当使用 jdk1.7 动态语言支持时,如果一个实例最后解析结果 REF_getStatic,REF_putStatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

什么是被动引用

一个类进行主动引用时会执行初始化。所有引用类的方式多不会触发初始化称为被动引用。

  • 通过子类调用父类的静态字段,不会导致子类初始化,只会触发父类的初始化。
  • 通过数组定义的引用类,不会触发此类的初始化。
  • 常量在编译阶段会存入调用类的常量池中,因此不会触发定义常量的类初始化。

类的加载过程

1.加载

在加载阶段(可以参考 java.lang.ClassLoader 的 loadClass() 方法),虚拟机需要完成以下 3 件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个 Class 文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口;

加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

2.验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段大致会完成 4 个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合 Class 文件格式的规范;例如:是否以魔术 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比 javac 编译阶段的语义分析),以保证其描述的信息符合 Java 语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object 之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
  4. 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3.准备

  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零 值,假设一个类变量的定义为:
public static int value=123;
  • 那变量 value 在准备阶段过后的初始值为 0 而不是 123.因为这时候尚未开始执行任何 java 方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器() 方法之中,所以把 value 赋值为 123 的动作将在初始化阶段才会执行。
  • 至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是 ConstantValue 时,会在准备阶段初始化为指定的值,所以标注为 final 之后,value 的值在准备阶段初始化为 123 而非 0.

4.解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

5.初始化

  • 如果一个类被主动引用,就会触发类的初始化。
  • 在 java 中,直接引用的情况有,通过 new 关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。通过反射方式执行以上三种行为。初始 化子类的时候,会触发父类的初始化。作为程序入口直接运行时(也就是直接调用 main 方法)。除了以上四种情况,其他使用类的方式叫做被动引用,而被动引 用不会触发类的初始化

6.使用

  • 类的使用包括主动引用和被动引用
  • 被动引用:引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。定义类数组,不会引起类的初始化。引用类的常量,不会引起类的初始化。

7.卸载

  • 满足下面的情况,类就会被卸载:该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。加载该类的 ClassLoader 已经被回收。该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 如果以上三个条件全部满足,jvm 就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java 类的整个生命周期就结束了。

总结

  • 对象基本上都是在 jvm 的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被 jvm 垃圾收集器回收。
  • 对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。

类的生命周期

  • jvm(java 虚拟机)中的几个比较重要的内存区域,这几个区域在 java 类的生命周期中扮演着比较重要的角色:
  1. 方法区:在 java 的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
  2. 常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
  3. 堆区:用于存放类的对象实例。
  4. 栈区:也叫 java 虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方 法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
  • 当我们编写一个 java 的源文件后,经过编译会生成一个后缀名为 class 的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在 java 虚拟机中运行,java 类的生命周期就是指一个 class 文件从加载到卸载的全过程

  • 一个 java 类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况

输入图片说明

类加载器

  • 通过一个类的全限定名来获取描述此类的二进制字节流,这个动作放到 java 虚拟机外部去实现。以便让应用程序自己决定如何去获取所需要的类。实现各动作的代码模块称为“类加载器”。
  • 比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个;诶是来源同一个 class 文件,但类加载器不同,他们也不相等。

启动类加载器

这个类加载器负责放在<JAVA_HOME>\lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。

扩展类加载器

这个类加载器由 sun.misc.Launcher$AppClassLoader 实现。它负责<JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。用户可以直接使用。

应用程序类加载器

这个类由 sun.misc.Launcher$AppClassLoader 实现。是 ClassLoader 中 getSystemClassLoader() 方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定 义类加载器,默认使用这个

自定义加载器

用户自己定义的类加载器。

双亲委派模型

如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException),子加载器才会尝试自己去加载。

优点

  • Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存在在 rt.jar 中,无论哪一个 类加载器要加载这个类,最终都是委派给处于模型最顶端的 Bootstrap ClassLoader 进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。
  • 相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将混乱。因此,如果开发者尝试编写一个与 rt.jar 类库中重名的 Java 类,可 以正常编译,但是永远无法被加载运行。

7. happens-before 原则

概述

  • 我们无法就所有场景来规定某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这规则就是 happens-before,从 JDK 5 开始,JMM 就使用 happens-before 的概念来阐述多线程之间的内存可见性。
  • 在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。
    happens-before 原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。下面我们就一个简单的例子稍微了解下 happens-before ;
i = 1;       //线程 A 执行
j = i ;      //线程 B 执行

j 是否等于 1 呢?假定线程 A 的操作(i = 1)happens-before 线程 B 的操作(j = i),那么可以确定线程 B 执行后 j = 1 一定成立,如果他们不存在 happens-before 原则,那么 j = 1 不一定成立。这就是 happens-before 原则的威力。

原则定义

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。

规则如下

程序次序规则

一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

锁定规则

一个 unLock 操作先行发生于后面对同一个锁额 lock 操作;

volatile 变量规则

对一个变量的写操作先行发生于后面对这个变量的读操作;

传递规则

如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C;

线程启动规则

Thread 对象的 start() 方法先行发生于此线程的每个一个动作;

程中断规则

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

线程终结规则

线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行;

对象终结规则

一个对象的初始化完成先行发生于他的 finalize() 方法的开始;

8. 对象

Java 中创建对象的 5 种方式

使用 new 关键字 → 调用了构造函数

Employee emp1 = new Employee();

使用 Class 类的 newInstance 方法→ 调用了构造函数

<!--使用 Class 类的 newInstance 方法创建对象。这个 newInstance 方法调用无参的构造函数创建对象。-->

Employee emp2 = (Employee) Class.forName("org.programming.mitra.exercises.Employee").newInstance();

使用 Constructor 类的 newInstance 方法 → 调用了构造函数

<!--和 Class 类的 newInstance 方法很像, java.lang.reflect.Constructor 类里也有一个 newInstance 方法可以创建对象-->

Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();

使用 clone 方法→ 没有调用构造函数

<!--无论何时我们调用一个对象的 clone 方法,jvm 就会创建一个新的对象,将前面对象的内容全部拷贝进去。用 clone 方法创建对象并不会调用任何构造函数。-->

<!--要使用 clone 方法,我们需要先实现 Cloneable 接口并实现其定义的 clone 方法-->
Employee emp4 = (Employee) emp3.clone();

使用反序列化→ 没有调用构造函数

ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();

对象的创建

Java 对象生命周期

对象的整个生命周期大致可以分为 7 个阶段:

创建阶段(Creation)

在创建阶段系统通过下面的几个步骤来完成对象的创建过程
1,为对象分配存储空间
2,开始构造对象
3,从超类到子类对 static 成员进行初始化
4,超类成员变量按顺序初始化,递归调用超类的构造方法
5,子类成员变量按顺序初始化,子类构造方法调用

一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段

应用阶段(In Use)

对象至少被一个强引用持有着

不可视阶段(Invisible)

当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
简单说就是程序的执行已经超出了该对象的作用域了。

不可到达阶段(Unreachable)

对象处于不可达阶段是指该对象不再被任何强引用所持有

与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被 JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些 GC root 会导致对象的内存泄露情况,无法被回收。

可收集阶段(Collected)

当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。
如果该对象已经重写了 finalize() 方法,则会去执行该方法的终端操作。

终结阶段(Finalized)

当对象执行完 finalize() 方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。

对象空间重新分配阶段

垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。

对象内存分配

类加载检查通过后,虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后可以完全确定,对象内存分配任务就是把一块确定大小的内存从堆中划分出来。

指针碰撞法

  • 如果堆中内存是绝对规整的。用过的内存放一边,空闲的放一边,中间放着一个指针作为分界点的指示器,那所分配内存就是把指针向空闲一边移动一段与对象大小相等的距离,即为“指针碰撞”

空闲列表法

  • 如果堆中内存不规整,已使用内存和未使用内存相互交错,虚拟机就必须一个列表,记录哪些内存块可用,在分配时从列表中找到一块足够大空间划分给对象,并更新列表上记录,即为“空闲列表”

总结

  • 选择何种分配方式,由堆是否规整决定,而堆是否规整由采用的垃圾收集器是否有压缩整理功能决定。
  • 使用 Serial,ParNew 等带 Compactg 过程的收集器时,系统采用指针碰撞法
  • 使用 CMS 这种基于 Mark-Sweep 算法的收集器时,系统采用空闲列表法

对象的访问定位

  • Java 程序需要通过栈上的 references 数据来操作堆上的具体对象。因为 referencesz 只是指向对象的一个引用,并没有定义这个引用通过何种方式去方位堆中对象的具体位置。所以对象访问方式取决于虚拟机实现而定的。
  • 目前主流的访问方式有使用句柄和直接指针两种。

句柄定位

使用句柄访问时,Java 堆中会划分出一块内存来作为句柄池,references 中存储的就是对象的句柄地址。句柄中包含对象实列数据与类型数据各组的具体地址信息 references->句柄池->java 堆

输入图片说明

直接指针定位

如果是直接指针访问,Java 堆的布局就必须考虑如何放置访问类型数据相关。

输入图片说明

各自优点

  • 句柄访问最大好处就是 references 中存储的是稳定的句柄地址,在对象移动(垃圾收集时移动对象是普遍行为) 时只会改变句柄中的实列数据指针,references 本身不需要修改。
  • 直接指针访问的最大好处是速度快,节省了一次定位的实时间开销。

9. 常量池总结

全局字符串池

string pool 也有叫做 string literal pool

  • 全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到 string pool 中(记住:string pool 中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
  • 在 HotSpot VM 里实现的 string pool 功能的是一个 StringTable 类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的) 的引用(而不是驻留字符串实 例本身),也就是说在堆中的某些字符串实例被这个 StringTable 引用之后就等同被赋予了”驻留字符串”的身份。这个 StringTable 在每个 HotSpot VM 的实例只有一份,被所有的类共享。

class 文件常量池

class constant pool

  • 我们都知道,class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal) 和符号引用(Symbolic References)。
  • 字面量就是我们所说的常量概念,如文本字符串、被声明为 final 的常量值等。
  • 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般 是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量。类和接口的全限定名,字段的名称和描述符,方法的名称和描 述符。

常量池的每一项常量都是一个表,一共有如下表所示的 11 种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值 1-12),代表当前这个常量属于哪种常量类型。

输入图片说明

运行时常量池(runtime constant pool)

当 java 文件被编译成 class 文件之后,也就是会生成我上面所说的 class 常量池,那么运行时常量池又是什么时候产生的呢?

  • jvm 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm 就会将 class 常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class 常量池中存的是字面量和符号引用,也就是说 他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串 池,也就是我们上面所说的 StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

举个实例来说明一下:

public class HelloWorld {
    public static void main(String []args) {
		String str1 = "abc"; 
		String str2 = new String("def"); 
		String str3 = "abc"; 
		String str4 = str2.intern(); 
		String str5 = "def"; 
		System.out.println(str1 == str3);//true 
		System.out.println(str2 == str4);//false 
		System.out.println(str4 == str5);//true
    }
}
  • 回到上面的那个程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个”abc”实例,全局 StringTable 中存放着”abc”的一个引用值
  • 然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且 StringTable 中存储一个”def”的引用值,还有一个是 new 出来的一个”def”的实例对象 与上面那个是不同的实例
  • 当在解析 str3 的时候查找 StringTable,里面有”abc”的全局驻留字符串引用,所以 str3 的引用地址与之前的那个已存在的相同
  • str4 是在运行的时候调用 intern() 函数,返回 StringTable 中”def”的引用值,如果没有就将 str2 的引用值添加进去,在 这里,StringTable 中已经有了”def”的引用值了,所以返回上面在 new str2 的时候添加到 StringTable 中的 “def”引用值
  • 上面程序的首先经过编译之后,在该类的 class 常量池中存放一些符号引用,然后类加载之后,将 class 常量池中存放的符号引用转存到运行时常 量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中 str1 所指向的”abc”实例对象),然后将这个对象的引用存到全 局 String Pool 中,也就是 StringTable 中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询 StringTable,保 证 StringTable 里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

总结

  • 1.全局常量池在每个 VM 中只有一份,存放的是字符串常量的引用值。
  • 2.class 常量池是在编译的时候每个 class 都有的,在编译阶段,存放的是常量的符号引用。
  • 3.运行时常量池是在类加载完成之后,将每个 class 常量池中的符号引用值转存到运行时常量池中,也就是说,每个 class 都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

class 文件常量池和运行时常量池

最近一直被方法区里面存着什么东西困扰着?

 1.方法区里存 class 文件信息和 class 文件常量池是个什么关系。

 2.class 文件常量池和运行时常量池是什么关系。        

方法区存着类的信息,常量和静态变量,即类被编译后的数据。这个说法其实是没问题的,只是太笼统了。更加详细一点的说法是方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用。

符号引用包括:1.类的全限定名,2.字段名和属性,3.方法名和属性。

输入图片说明
输入图片说明

可以看到在方法区里的 class 文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。

输入图片说明

输入图片说明

class 文件常量池和运行时常量池的关系以及区别

  • class 文件常量池存储的是当 class 文件被 java 虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量。
  • 运行时常量池是当 class 文件被加载完成后,java 虚拟机会将 class 文件常量池里的内容转移到运行时常量池里,在 class 文件常量池的 符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的 时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

总结:

  • 方法区里存储着 class 文件的信息和运行时常量池,class 文件的信息包括类信息和 class 文件常量池。

  • 运行时常量池里的内容除了是 class 文件常量池里的内容外,还将 class 文件常量池里的符号引用转变为直接引用,而且运行时常量池里的内容是能 动态添加的。例如调用 String 的 intern 方法就能将 string 的值添加到 String 常量池中,这里 String 常量池是包含在运行时常量池里 的,但在 jdk1.8 后,将 String 常量池放到了堆中。

10. 类文件结构

1.class 类文件结构

  • class 文件结构是一组以 8 位字节为基础单位的二进制流。存储的内容几乎全部是程序运行的必要数据,无空隙。
  • 如果需要占用 8 位字节以上空间的数据,则按照高位在前的方式分割成若干个 8 位字节进行存储。
  • class 文件结构采用一种类似 C 语言体系的伪结构体系,这种伪结构只有无符号数和表两种数据类型。

魔数与 Class 文件的版本

  • class 文件的头 4 个字节称为魔数,唯一作用是确定这个文件是否为一个能被虚拟机接受的文件。
  • 魔数值可以自由选择,只要未被广泛使用同事不会引起混淆。
  • 紧接着魔数的 4 个字节是 class 文件版本号,第 5 和第 6 个字节是次版本你好,7 和 8 个字节是 class 文件版本号(java 版本号从 45 开始。jdk7 是 51.0)

常量池

  • 主次版本号之后的是常量池,常量池可以理解为 class 文件中的资源仓库。
  • class 文件结构中只有常量的容量技术是从 1 开始
  • 常量池主要存放两大类常量:字面量(如文本字符串,finald 常量) 和符号引用(类和接口的全限定名,字段的名称和描述符,方法的名称和描述符)。
  • 虚拟机运行时,需从常量池获取对应的符号引用,再在类创建时或运行将诶系会,翻译到哪具体的内存地址中。

访问标志

常量池之后的两个字节代表访问标志,用于识别 class 是类还是接口,是否为 public 类型或 abstract 类型等等。

类索引,父类缩影与接口索引集合

  • 这三项按顺序排列在访问标志之后,class 文件中由这三项来确定整个类的继承关系。
  • 类索引用于确定类的全限定名,父类索引用于确定类的父类权限定名。接口索引集合描述类实现了哪些接口

字段表集合

用于描述接口或类中声明的变量。字段包裹类级别的变量和实列变量。不包括方法内部声明的局部变量。

方法表集合

方法表结构依次包括访问标志,名称索引,描述索引,属性集合。

11. 虚拟机字节码执行引擎

执行引擎是 Java 虚拟机最核心的组成部分之一。

运行时栈帧结构

  • 栈帧用于虚拟机进行方法调用和方法执行的数据结构。
  • 栈帧存储了方法的局部变量表,操作数据栈,动态链接和返回地址等信息。每一个方法从调用开始至执行完成过程,都是在虚拟机中入栈到出栈的过程。
  • 栈帧需要分配多少内存,不受程序运行时期变量数据影响,取决虚拟机的具体实现。

1.局部变量表

  • 一组变量值存储空间,存放方法参数和方法内部的局部变量,类编译为 class 文件时就在方法的 code 属性 max_locals 中确定了方法局部变量表的最大容量。
  • 一变量槽”Slot“为最小单位,虚拟机没指明 solt 的占用内存大小,一般每个 solt 都可以存放一个 boolean,bye,char,short,int,float,reference 或 returnAddress 类的数据(32 位或更小物理内存存放)。
  • 它是建立在线程的堆栈上。是线程私有的数据,所以是线程安全的。
  • 虚拟机通过索引的方式使用局部变量表。执行方法时,通过局部变量表完成参数值到参数变量列表的过程。如果执行实列方法(非 static),变量表中第 0 位索引的 slot 默认用户传递方法的引用。

2.操作数栈

  • 它是一个后入先出的栈。同局部变量表一样,最大深度在编译时写入到 code 属性的 max_stacks 中。
  • 操作数栈的每一个元素可以是任意的 Java 数据类型。32 位的数据类所占的栈容量为 1,64 位栈容量 2(long,double)
  • 一个方方法刚开始执行时,操作数栈时空的。在方法执行过程中,通过各种字节码指令往操作数栈写入和提取内容,也就是出栈/入栈操作。

3.动态链接

  • 每个栈帧都包含一个指向运行时常量池中该栈所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接

4.方法返回地址

  • 一个方法执行后,只有通过正常完成出口和异常完成出口两种方式退出。
  • 正常完成出口:当执行引擎遇到一个方法返回的字节码指令
  • 异常完成出口:方法执行过程中遇到异常且方法中未处理此异常,就会导致方法退出。
  • 方法正常退出时,调用者的程序计数器的值可以作为返回地址。
  • 方法退出的过程等于就是把当前栈帧出栈。

方法调用

  • 方法调用不等于方法执行,方法调用阶段唯一任务就是确定被调用方法的版本。
  • 一切方法调用在 Class 文件里存储都只是符号引用,而不是方法在实际运行时内存布中的入口地址(直接引用)。

1.解析

  • 所有方法调用重点目标方法在 Class 文件里都是一个常量池的符号引用,解析阶段会将一部分符号引用转化为直接引用。
  • "编译器可知,运行期间不可变"这类方法的调用称为解析.(静态方法和私有方法)
  • 只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本.(比如静态方法,私有方法,实列构造器,父类方法) 它们在类加载时候会把符号引用解析为该方法的直接引用。

2.静态分派

在重载时时通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译可知的。
静态分派的典型应用就是方法重载

//左边是静态类型 右边是实际类型
Human man=new Man();
Huamn woman=new Woman();

3.动态分派

方法的重写就是动态分派的体现。

12. 高效并发

主内存与工作内存

  • JAVA 内模型规定所有的变量都存储在主内存中,每个线程都有自己的工作内存,线程的工作内存中保存的是当前线程使用到的变量值的副本(主内村拷贝过来的)。
  • 线程对变量的所有操作都必须在工作内存中进行,不能直接与主内存进行读写交.线程间相互的传值需要通过主内存完成。

内存间的交互

JAVA 内存模型定义了以下 8 种操作来完成内存交互工作:

  • lock (锁定):作用于主内存的变量。把一个变量标识为一条线程独占的状态。
  • unlock (解锁):作用于主内存的变量.把一个处于锁定状态的变量释放出来。
  • read (读取):作用于主内存的变量。把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • load (载入):作用于工作内存的变量,它把 read 操作从主内存中得到的值放入工作内存的变量副本中。
  • use (使用):作用与工作内存的变量.它把工作内存中一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作。
  • assign (赋值):作用于工作内存的变量,它把一个从执行引擎收到的赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store (存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的 wirte 操作使用。
  • wirte (写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量值放入主内存中。

上述操作虚拟机实现时保证每一种操作都是原子性的。且比如满足如下规则

  • 不 _允许一个变量从主内存读取但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现
  • 变量在工作内存中改变之后必须把该变化同步回主内存
  • 一个新的变量必须在主内存中诞生。不允许工作内存直接使用未初始化的变量。
  • 一个变量同一个时刻只能一条线程进行 lock 操作,但是 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果一个变量执行 lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作。
  • 如果一个变量事先没有被 lock 操作锁定,那将不允许执行 unlock 操作,也不允许去 unlock 一个被其它线程锁定住的变量
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中

对于 volatile 型变量的特殊规则

  • volatile 是虚拟机提供的最轻量级同步机制。它具备两种特性:保证被修饰的变量对所有线程可见(即可见性)和禁止指令重排序。
  • volatile 只能保证可见性,不能保证操作运算的原子性。
  • 运算结果并不依赖变量的当前值时和不需要与其他的状态变量共同参与不变约束时适合使用 volatile

对于 long 和 double 型变量的特殊规则

  • 对于 64 位的数据类型 long 和 double,在内存模型中有一条相对宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据类型分为两次 32 位的操作来新型。
  • 允许虚拟机实现选择可以不保证 64 位数据类型的 load , storm , read , write 这个四个操作的原子性(ong 和 double 的非原子性协定)
  • JAVA 内存模型虽然允许虚拟机不把 longdouble 变量的读写实现成原子操作,但允许虚拟机选择把这些操作实现为具有原子性的操作。目前各种平台下的虚拟机几乎都选择吧 64 位数据类型读写操作作为原子操作对待。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

想挽留

暂无简介

0 文章
0 评论
20372 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文