深入理解JVM虚拟机,月薪30K的必经之路!【我收藏了】

目录

前言

1 意义

2 运行时数据区组成

2-1 线程私有

程序计数器

虚拟机栈

本地方法栈

2-2 线程共有

方法区(自用)

gc堆(可及)

3 对象的创建

3-1 检查参数是否在常量池中定位到一个类的符号引用;该类是否被加载、解析、初始化过

3-2 若有则分配内存

内存绝对规整

内存不规整

线程安全

4 对象创建流程图

5 对象的内存布局

5-1 对象头

5-2 实例数据

5-3 对齐填充

6 对象的访问定位

6-1 使用句柄

6-2 直接指针

7 oom异常

7-1 虚拟机栈和本地方法栈溢出

7-2 方法区和运行时常量池溢出

7-3 堆溢出

7-4 本机直接内存溢出

8 垃圾对象的判定

8-1 对象的引用

8-2 引用计数法

8-3 根搜索算法

8-4 堆中垃圾回收过程

8-5 方法区中垃圾回收

9 垃圾回收算法

9-1 标记-清除算法

9-2 复制算法

9-3 标记-整理算法

9-4 分代收集算法

9-5 hotspot算法

10 垃圾收集器

10-1 serial收集器

10-2 parnew收集器

10-3 parallel scavenge收集器

10-4 serial olc收集器

10-5 parallel old收集器

10-6 cms收集器

10-7 g1收集器

10-8 内存分配和回收策略

11 虚拟机类加载机制

11-1 类加载的时机

11-2 类加载的过程(5)

11-3 类加载器

11-4 双亲委派模型

12 java内存模型与线程

12-1 硬件效率和一致性

12-2 java内存模型(jave memory model, jmm);

 


前言

Java 虚拟机日益成为互联网大厂面试的重要内容,对于 Java 开发者而言,只有在了解 Java 背后的原理后,才能写出更高质量的代码,才能在错综复杂的 bug 中快速定位出问题并找到解决方案。

本章小编首先会带领大家入门 Java 虚拟机,然后针对面试中常考的知识点和开发中常用的关键技术进行详细地阐述,帮助大家对 Java 做到知其然,并知其所以然。

1 意义

  • 屏蔽各个硬件平台和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果

2 运行时数据区组成

2-1 线程私有

程序计数器

  • 当前线程所执行的字节码的行号指示器:<ol><li>如果正在执行的是java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;</li><li>如果正在执行natvie方法,计数器值为空(undefined)</li></ol>

作用

  • java虚拟机字节码解释器通过改变这个计数器的值来选取下一条要执行的字节码指令
  • 没有规定任何outofmemoryerror的情况

虚拟机栈

  • 用途:为jvm执行java方法服务
  • 编译期间完成分配

<p>结构:栈帧:<b><u>局部变量表</u></b>、操作数栈、动态链接、方法出口</p>

  • 基本类型变量,(boolean,byte,char,short,int,float,long,double)
  • 对象句柄
  • 方法参数
  • 方法的局部变量
  • 两种异常:stackoverflowerror、outofmemoryerror; -xss设置栈大小

本地方法栈

  • 用途:为虚拟机使用到的native方法服务
  • 两种异常:stackoverflowerror、outofmemoryerror

2-2 线程共有

方法区(自用)

  • 用途:用于存储已被jvm加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

运行时常量池

内容:存放编译产生的字面量(<b>常量final</b>)和<b>符号引用</b>

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

特点:运行时常量池相对于class文件常量池的一个重要特征是具备<b>动态性</b>

  • java语言并不要求常量一定只有编译期才能产生,运行期间也能将新的常量放入池中
  • 异常:(无法扩展)outofmemoryerror

方法区的gc

  • 参考垃圾对象的判定

gc堆(可及)

  • 目的:存放对象实例和数组

分代处理

  • 目的:更好的回收内存和更快的分配内存

新生代

  • eden空间
  • from survivor空间
  • to survivor空间等
  • 老年代
  • 空间结构:逻辑连续的空间,物理可不连续
  • 优先分配tlab(thread local allocation buffer),减少加锁,提高效率
  • gc管理的主要区域

异常:如果在堆中没有完成内存分配,并且堆也无法扩展时,会抛出outofmemoryerror

  • -xms初始堆大小 -xmx最大堆大小

3 对象的创建

3-1 检查参数是否在常量池中定位到一个类的符号引用;该类是否被加载、解析、初始化过

  • 若没有做进行类加载

3-2 若有则分配内存

内存绝对规整

  • 用“指针碰撞”来分配内存

内存不规整

  • 用“空闲列表”来分配内存

线程安全

对分配内存空间的工作进行同步处理

  • 采用cas+失败重试的方式保证更新操作的原子性

每个线程分配一块本地线程分配缓冲区

tlab

  • -xx:+/-usetlab
  • 3、始化已分配内存为零值(保证类型不赋初值可以使用)
  • 4、上面工作完成后,执行init方法按照程序员意愿初始化对象

4 对象创建流程图

以上知识点已经整理成文档,需要领取的同学  点我 免费领取,关注小编,可以持续免费嫖资料,

5 对象的内存布局

5-1 对象头

  • 存储运行时数据
  • 存储类型指针

5-2 实例数据

  • 是对象真正存储的有效信息

5-3 对齐填充

  • 起占位符的作用

6 对象的访问定位

6-1 使用句柄

  • 堆中有句柄池,存储到实例数据和类型数据的指针;栈中的引用指向对象的句柄地址

优点

  • <ol><li>reference中地址相对稳定;</li><li>对象被移动(gc时)时只会改变句柄中的实例数据指针</li></ol>

6-2 直接指针

  • 栈中的引用直接存储对象地址,到方法区中类型数据的指针包含在对象实例数据中

     

    优点

  • 访问速度快,节省了一次指针定位的开销

7 oom异常

7-1 虚拟机栈和本地方法栈溢出

线程请求栈深度大于最大深度stackoverflowerror

  • 设置-xss128k,在单线程下,通过不断调用递归方法。

新线程拓展栈时无法扩展出现outofmemoryerror错误

  • 不断创建新线程,并让创建的线程不断运行
  • -xss

7-2 方法区和运行时常量池溢出

java.lang.outofmemoryerror后会跟permgen space

  • 不断创建新的字符串常量,并添加到list中
  • -xx:permsize和-xx:maxpermsize

7-3 堆溢出

java.lang.outofmemoryerror:java heap space

内存泄漏

  • 通过不断创建新对象,并放入list中,保证gcroots到对象之间路径可达
  • 内存溢出
  • -xms -xmx

7-4 本机直接内存溢出

  • 在heap dump文件中没有明显异常
  • -xx

8 垃圾对象的判定

8-1 对象的引用

强引用

  • 存在就不回收

软引用

将要发生内存溢出之前

  • 实现缓存

弱引用

下一次垃圾回收

  • 回调函数中防止内存泄露

虚引用

对对象的生存时间没有影响

  • 能在这个对象被收集器回收时收到一个系统通知

8-2 引用计数法

  • 难以解决对象之间相互循环引用的问题

8-3 根搜索算法

  • 从gc roots向下搜索建立引用链;一个对象如果到gc roots没有任何引用链相连时,证明对象不可用

gc roots

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

8-4 堆中垃圾回收过程

  • 1、如果对象在进行可达性分析后发现没有与gc roots相连接的引用链,那它将会被第一次标记
  • 2、断对象是否有必要执行finalize()方法,(没有覆盖,被调用过,都没有必要执行),放入f-queue队列
  • 3、放入f-queue中,进行第二次标记
  • 4、被拯救的移除队列,被两次标记的被回收

8-5 方法区中垃圾回收

废弃常量

  • 没有任何一个对象引用常量池中的“abc”常量

无用的类(满足条件可以被回收,非必然)

  • 1、该类所有的实例都已经被回收
  • 2、加载该类的加载器被回收
  • 3、该类对应的javalang.class对象没有在任何地方被引用,无法通过反射访问该类的方法

9 垃圾回收算法

9-1 标记-清除算法

  • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
  • <ul><li>效率问题,标记和清除两个过程的效率都不高</li><li>空间问题,产生大量不连续的内存碎片,连续内存不足会再次触发gc</li></ul>

9-2 复制算法

  • 将内存等分,每次用一块,当这块内存用完了,就将活着的对象复制到另一块,然后把前者清空
  • <ol><li>对象存活率较高时就要进行较多的复制操作,效率将会降低 </li><li>空间利用率低</li></ol>

9-3 标记-整理算法

  • 所有存活的对象移向一端,然后直接清理掉端边界以外的内存

9-4 分代收集算法

新生代

  • 复制算法

老年代

  • 标记-整理或标记-清除

9-5 hotspot算法

枚举根结点

  • 当执行系统停顿下来后,并不需要一个不漏的检查完所在执行上下文和全局的引用位置,在hotspot的实现中,使用一组称为oopmap的数据结构来存放对象引用

安全点

  • 在这些特定的位置,线程的状态可以被确定

位置

  • 方法调用指令
  • 循环跳转指令
  • 异常跳转指令

中断方式

抢占式

  • gc发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑在安全点上

主动式

  • 设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起

安全区域

  • 背景:线程sleep状态或者blocked状态的时候,无法响应jvm中断,走到安全的地方,jvm也不能等他们,这样就无法进行gc
  • 安全区域是指在一段代码中,引用关系不会发生变化,这个区域中的任何地方开始gc都是安全的。

10 垃圾收集器

10-1 serial收集器

特点

  • 新生代收集器
  • 采用复制算法
  • 单线程收集
  • 进行垃圾收集时,必须暂停所有工作线程,直到完成

应用场景

  • 是hotspot在client模式下默认的新生代收集器
  • 简单高效(与其他收集器的单线程相比)
  • 对于限定单个cpu的环境来说,serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;

参数设置

  • "-xx:+useserialgc":添加该参数来显式的使用串行垃圾收集器;

10-2 parnew收集器

特点

  • 新生代收集器
  • 采用复制算法
  • 除了多线程外,其余的行为、特点和serial收集器一样

应用场景

  • server模式下,parnew收集器是一个非常重要的收集器
  • 单个cpu环境中,不会比serail收集器有更好的效果,因为存在线程交互开销

参数设置

  • "-xx:+useconcmarksweepgc":指定使用cms后,会默认使用parnew作为新生代收集器;
  • "-xx:+useparnewgc":强制指定使用parnew;
  • "-xx:parallelgcthreads":指定垃圾收集的线程数量,parnew默认开启的收集线程与cpu的数量相同;

10-3 parallel scavenge收集器

特点

  • 新生代收集器
  • 采用复制算法
  • 多线程收集
  • cms等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间; 而parallel scavenge收集器的目标则是达一个可控制的吞吐量(throughput)

应用场景

  • 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
  • 当应用程序运行在具有多个cpu上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互

参数设置

  • "-xx:maxgcpausemillis"控制最大垃圾收集停顿时间
  • "-xx:gctimeratio" 设置垃圾收集时间占总时间的比率
  • "-xx:+useadptivesizepolicy"

10-4 serial olc收集器

特点

  • 针对老年代
  • 采用"标记-整理"算法(还有压缩,mark-sweep-compact)
  • 单线程收集

应用场景

  • 主要用于client模式

在server模式中

  • 在jdk1.5及之前,与parallel scavenge收集器搭配使用(jdk1.6有parallel old收集器可搭配)
  • 作为cms收集器的后备预案,在并发收集发生concurrent mode failure时使用

以上知识点已经整理成文档,需要领取的同学  点我 免费领取,关注小编,可以持续免费嫖资料,

10-5 parallel old收集器

特点

  • 针对老年代
  • 采用"标记-整理"算法
  • 多线程收集

应用场景

  • jdk1.6及之后用来代替老年代的serial old收集器
  • 特别是在server模式,多cpu的情况下
  • 在注重吞吐量以及cpu资源敏感的场景,就有了parallel scavenge加parallel old收集器的"给力"应用组合

参数设置

  • "-xx:+useparalleloldgc":指定使用parallel old收集器

10-6 cms收集器

特点

  • 针对老年代
  • 基于"标记-清除"算法(不进行压缩操作,产生内存碎片)
  • 以获取最短回收停顿时间为目标
  • 并发收集、低停顿
  • 需要更多的内存(看后面的缺点)

应用场景

  • 与用户交互较多的场景
  • 希望系统停顿时间最短,注重服务的响应速度
  • 以给用户带来较好的体验
  • 如常见web、b/s系统的服务器上的应用

参数设置

  • "-xx:+useconcmarksweepgc":指定使用cms收集器

运行过程

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

缺点

  • 对cpu资源非常敏感
  • 无法处理浮动垃圾,可能出现"concurrent mode failure"失败
  • 产生大量内存碎片

10-7 g1收集器

特点

并行与并发

  • gc收集线程并行
  • 用户线程与gc线程并发
  • 分代收集,收集范围包括新生代和老年代
  • 空间整合:结合多种垃圾收集算法,空间整合,不产生碎片
  • 可预测的停顿:低停顿的同时实现高吞吐量

应用场景

  • 面向服务端应用,针对具有大内存、多处理器的机器
  • 最主要的应用是为需要低gc延迟,并具有大堆的应用程序提供解决方案

运行过程(不计remembered set操作)

初始标记

标记gc root能直接关联到的对象

  • 需要停顿用户线程

并发标记

对堆中对象可达性分析

  • 并发执行

最终标记

修正并发标记中因用户线程运行发生改变的标记记录

  • 需要停顿线程

筛选回收

对region的回收价值和成本排序,根据参数指定回收计划

  • 可以并发

参数设置

  • "-xx:+useg1gc":指定使用g1收集器
  • "-xx:initiatingheapoccupancypercent":当整个java堆的占用率达到参数值时,开始并发标记阶段;默认为45
  • "-xx:maxgcpausemillis":为g1设置暂停时间目标,默认值为200毫秒
  • "-xx:g1heapregionsize":设置每个region大小,范围1mb到32mb;目标是在最小java堆时可以拥有约2048个region

10-8 内存分配和回收策略

对象优先在eden分配

  • 大多数情况下,对象在新生代eden区中分配,当eden区没有足够空间进行分配时,虚拟机将发起一次minor gc

大对象直接进入老年代

  • 所谓大对象,就是需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串以及数组
  • 长期存活的对象将进入老年代

动态对象年龄判定

  • 如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代,无须等到maxtenuringthreshold中要求的年龄

空间分配担保

  • minor gc之前,jvm检查老年代最大连续空间是否大于新生代所有对象的空间,成立则确保minor gc安全
  • 不成立,参看参数handlepromotionfailure是否允许担保失败,允许则检查老年代最大连续空间是否大于历次晋升的对象的平均大小,大于则尝试minor gc
  • 否则,进行full gc
  • 内存管理机制
  • 垃圾收集器
  • 内存分配策略

11 虚拟机类加载机制

11-1 类加载的时机

声明周期

  • 加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析3个部分统称为连接

以下情况对类进行初始化

  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
  • 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.methodhandle实例最后的解析结果bef_getstatic、bef_putstatic、bef_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

11-2 类加载的过程(5)

加载

完成3件事

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口

类的来源

  • 从zip包中读取,这很常见,最终成为日后jar、ear、war格式的基础
  • 从网络中获取,这种场景最典型的应用就是applet
  • 运行时计算生成,这种场景使用得最多的就是动态代理基础
  • 由其它文件生成,典型场景就是jsp应用,即由jsp文件生成赌赢的class类
  • 从数据库中读取,这种场景相对的少些,例如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发

验证

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

验证项

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备

  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都讲在方法区汇总进行分配

解析

  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

解析动作分类

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

初始化

  • 类初始化是类加载过程中的最后一步,这时才真正开始执行类中定义的java程序代码

11-3 类加载器

  • 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
  • 比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个虚拟机加载,只要它们的类加载器不同,那这两个类就必定不相等

11-4 双亲委派模型

分类

启动类加载器

  • 这个类加载器使用c++语言实现,是虚拟机自身的一部分

其他类加载器

  • 这些类加载器由java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.classloader
  • java内存模型与线程
  • 线程安全

12 java内存模型与线程

12-1 硬件效率和一致性

硬件使用了很多手段, 目的是提高硬件执行效率

  • 为了解决处理器和内存速度的矛盾, 加多级缓存
  • 为了使处理器内部运算单元尽量充分使用, 处理器对输入代码进行乱序执行(out-of-order execution)

实际jvm虚拟机中也是用了类似的技术手段来提升性能

  • 线程的工作内存 - 硬件多级缓存
  • 编译器指令重排序(instruction reorder) - 乱序执行

使用优化问题所带来的问题

多级缓存引入了缓存一致性问题

  • 使用缓存一致性协议来解决, 如: msi/ mesi/ mosi/ synapse/ firefly/ dragon protocol 等

12-2 java内存模型(jave memory model, jmm);

  • 主要目标是定义程序中各个变量的访问规则, 这里的变量指的是实例字段/ 静态字段/ 数组对象的元素 等, 可以被线程共享的变量
  • @jvm没有直接像c/c++一样直接使用物理硬件和操作系统的内存模型, 增加了跨平台能力;@内存模型的定义必须足够严谨, 保证并发内存操作不会产生奇异;@但是定义也必须足够宽松, 使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器/ 高速缓存/ 指令集中某些特有指令)/;

主内存和工作内存

主内存

  • jvm堆中的一部分

工作内存

  • 每个线程都有自己的工作内存, 工作内存中保存被该内存使用的变量的主内存副本
  • 线程的读写操作必须对工作内存操作, 无法对主内存直接进行读写操作
  • 线程无法直接读写其他线程的工作内存;线程间的变量值的传递需要通过主内存来完成
  • jvm虚拟机栈的一部分
  • 对应于硬件, 很可能对应了高速缓存甚至寄存器

内存间的交互操作

  • 主要讲了java内存模型的原子性
  • read/load需要顺序执行;store/write需要顺序执行;但是在指令间可以插入其他指令, 如: read a; read b; load a; use a; load b;

8中原子操作还需要遵从以下规则

  • 不允许read和load / store和write单独出现;即不允许变量从主存读取了工作内存不接受 或者 从工作内存回写了主存不接受的情况
  • 不允许一个线程丢弃它最近的assign操作;即变量在工作内存中改变了之后必须把该变化同步到主存
  • 不允许一个线程无原因地(没有发生过assign操作)把数据从工作内存同步到主存;???防止对主存的无意义刷新更新其他线程已经修改的值(大雾);即对一个变量使用store之前必须经过assign操作
  • 变量只能在主存中"诞生", 不允许在工作内存中使用一个未经初始化(load/assign)的变量;即对一个变量使用use/ store之前必须经过 load/assign操作
  • 一个变量在同一时刻只允许一个线程对其进行lock操作, 但lock可以被同一线程重复执行多次(可重入), 执行多次lock后必须执行相同次数的unlock操作, 变量才能被解锁
  • 如果对一个变量执行lock操作, 将会清空工作内存中此变量的值, 在执行引擎使用该变量前, 需要重新执行load或者assign操作初始化变量的数值
  • 如果一个变量事先没有被lock操作锁定, 那么就不允许对它执行unlock操作;不允许一个线程去unlock一个被其他线程锁住的变量
  • 对一个变量执行unlock操作之前, 必须将该变量同步回主存中(执行store write操作)
  • 这些规则(加上对volatile的一些特殊规定)确定了那些内存访问操作在并发下是安全的

volatile的特殊规定

保证volatile对所有线程的可见性;即 当某线程改变变量值后, 新值对其他线程可立即得知;

  • 可见性不等同于一致性;
  • volatile在符合以下所有情况时可以保证并发安全:1. 运算结果并不依赖变量的当前值;2. 确保只有一个线程可以修改变量;3.变量不需要与其他的状态变量同时参与不变约束;

volatile变量可以禁止指令冲排序优化

  • 利用了内存屏障, 指令重排序无法越过内存屏障

对long/double的特殊规定

  • long/double的非原子性协议:允许虚拟机将非volatile修饰的64位数据的读写操作划为两次32位的操作来进行
  • 在目前商用虚拟机中64位数据的读写也是原子性的

原子性/可见性/有序性

  • 原子性:由java内存模型直接保证的原子性操作包括lock/unlock/read/load/use/assign/store/write
  • 可见性:当一个线程修改了共享变量的值, 其他线程能够立即得知这个修改;java内存模型通过在变量修改后将新值同步回主存, 在变量读取前从主存刷新变量值这种依赖主存为传递媒介的方式来实现可见性的; 普通变量和volatile变量都是如此;不过volatile变量的特殊规定保证了新值能够立即同步到主存, 以及每次使用前立即从主存刷新; 因此可以说volatile保证了多线程操作时变量的可见性, 但普通变量无法保证这一点;除了volatile变量外, sync同步代码块和final也可以保证可见性;
  • 有序性:如果在本线程内观察, 所有操作都是有序的; 如果在一个线程中观察另一个线程, 所有操作都是无序的;前半句指线程内表现为串行的语义(whthin-thread as-if-serial semantics);后半句指"指令重排序"现象和"工作内存与主内存同步延迟"现象;volatile和sync同步代码块可以保证有序性;

先发性原则

  • 先行发生是java内存模型中定义的两项操作间的偏序关系;如果操作a先行发生于操作b, 实际指 在发生b之前, a产生的影响会被b观察到;

满足以下任意一规则, 则不可指令重拍

  • 程序次序规则
  • 管理锁定规则
  • volatile变量规则
  • 线程启动规则
  • 线程终止规则
  • 线程中断规则
  • 对象终结规则
  • 传递性
  • 时间先后顺序和先行发生原则之间基本没有太大关系, 所以衡量并发安全问题时只按先行发生原则为准即可;

来自小编给程序员的专属福利

以上是小编分享的一些技术学习经验,当然小编也准备了一些列的Java核心知识点资料以及jvm性能调优的面试资料,需要的领取的同学 点我 免费领取 ,关注我,大家有什么解决不了的技术问题一起和小编讨论呀!

 喜欢小编的分享可以点赞关注哦,小编持续为你分享最新文章 和 福利领取哦

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值