JVM

绪 --JVM 探究

  • 请你谈谈对JVM的理解?java 8虚拟机和之前的变化更新?
  • 什么是OOM?什么事栈溢出StackOverFlowError?怎么分析?
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?
  • 谈谈JVM中,类加载器你的认识?
  1. JVM的位置
  2. JVM的体系结构
  3. 类加载器
  4. 双亲委派机制
  5. 沙箱安全机制
  6. Native
  7. PC寄存器
  8. 方法区
  9. 三种JVM
  10. 新生区、老年区
  11. 永久区
  12. 堆内存调优
  13. GC(常用算法)
  14. JMM
  15. 总结
    不懂的点自己,通过以下方式学习
    1.百度
    2.思维导图 processon->推荐->搜索JVM

单点登录--| SSO

1. JVM的位置

JVM在操作系统之上,可以看作是JVM app,在这个app里运行java程序ABCD

2. JVM体系结构

精简版

复杂版

3. 类加载器

作用:加载Class文件
1.虚拟机自带的加载器
2.启动类(根bootstrap)加载器 ----jre/lib/rt.jar
3.扩展类加载器(ext)---jre/lib/ext/*.jar
4.应用程序(系统app)加载器

4.双亲委派机制

类加载双亲委派机制--安全
1.APP-->EXT-->BOOT首先向上委托寻找
2.BOOT->EXT-->APP 然后从上向下逐级寻找加载类,一旦找到就终止,都找不到会抛出class not Found Exception
安全体现在,程序员没办法手写覆盖jdk自带包里的类,例重写String方法,运行时仍旧加载的是root加载的rt.jar下的String类。
除非用自己的包替换原来的rt.jar(通常不这样做,只有大公司性能调优才可能更改自带包)
java中null有两种情况:Java调用不到是底层c语言写的,或者是不存在

5.沙箱安全机制(了解即可)

5.1 什么是沙箱?

Java安全模式的核心就是Java沙箱(sanbox).沙箱是一个限制程序运行的环境。
沙箱机制就是将Java代码限定再虚拟机(JVM)特定的运行范围中,并严格限制代码对本地系统资源访问。通过这样的措施来保证代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问
系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有java程序运行都可以指定沙箱,定制安全策略。

5.2 安全模型

在java中将执行程序分成本地代码和远程代码两种,本地代码默认是为可信任的,而远程代码则被看作是不受信的。对于受信的本地代码,可以访问一切本地资源。而对于非受信的远程代码在早期的Java视线中,安全依赖于沙箱机制。
如图JDK1.0安全模型,远程代码无法访问本地资源:

JDK1.1-- 允许用户指定代码对本地资源的访问权限

JDK1.2-- 增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制

JDK1.6(延续至今 )--引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)

5.3 组成沙箱的基本条件

  • 字节码校验器:确保遵循语言规范。这样可以帮助java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类
  • 类装载器(class loader)
    • 防止恶意代码去干涉善意代码
    • 守护被新人的类库边界
    • 将代码归入保护域,确定了代码可以进行哪些操作
      虚拟机为不同的类加载器载入的类提供不同的命名空间。命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
      由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
  • 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
  • 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
  • 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
    • 安全提供者
    • 消息摘要
    • 数字签名
    • 加密
    • 鉴别
      通过Java命令行启动的Java应用程序,默认不启用沙箱。要想启用沙箱,启动命令需要做如下形式的变更:
      java -Djava.security.manager
      更多参考

6. Native

native关键字修饰的方法,说明java的作用范围达不到,会去调用底层C语言的库
调用该方法时,会进入本地方法栈(如start0),调用本地方法接口(JNI),最终执行本地方法库中的方法.
JNI作用:扩展Java的使用,融合不同编程语言为java所用。
本地方法栈(native method stack): 在内存区域中专门开辟的一块标记区域,用于登记native方法。在最终执行的时候,通过JNI加载本地方法库中的方法

7. PC 寄存器

程序计数器:Program Counter Register
程序计数器就是一个指针,指向方法区中的方法字节码,用来存储指向一条即将要执行的指令代码的地址。由执行引擎读取下一条指令。

1.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
2.在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
3.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。
4.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
5.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
6.它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且没有垃圾回收

使用PC寄存器存储字节码指令地址有什么用呢(面试常问)?
(1)多线程宏观上是并行(多个事件在同一时刻同时发生)的,但实际上是串行交替执行的
(2)因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
(3)JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
为什么要设置每个线程私有?
保证各个线程相互独立,运行时互不干扰
PC寄存器参考博客

8. 方法区

方法区是被所有线程共享,所有定义的方法信息都保存在该区域,如字段和方法字节码,以及一些特殊方法,如构造函数,接口代码
静态变量Static,常量final,类信息Class类(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法无关。
注: 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期间生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

9. 栈(stack)

程序= 数据结构+算法 不是框架+业务逻辑

  • 栈特点:
    • 栈是后进先出的数据结构
    • 栈内存,线程级的,每个线程一个。主管程序的运行,生命周期和线程同步
    • 线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题。
    • 栈:8大基本类型+对象引用+实例方法
  • 栈运行原理:栈帧
    栈满了抛出错误StackOverFlowError(如两个方法互相调用,造成死局)

    (注:图片引用自博客
  • 栈+堆+方法区 交互例子,转载自CSDN博主「A-莫天」
public class Student {
    String name ;//定义成员变量
    int age ; //定义成员变量
    void Speak() {
        System.out.println("学生的姓名是:"+name);
        System.out.println("学生的年龄是:"+age);
        System.out.println("############");
    }
    public static void main(String[] args) {
        Student stu = new Student() ; //实例化对象stu
        stu.name = "张三" ;       //赋值
        stu.age = 18;             //赋值
        stu.stuid = "1008611" ;   //赋值
        stu.Speak();              //调用方法speak
        }
}

10. 三种JVM

  • sun公司虚拟机 HotSpot(java -version可以查看,我们用的都是这个)
  • BEA JRockit
  • IBM J9VM

11. 堆(Heap)

11.1 堆概述

(注:此部分主要参考博客
一个JVM只有一个堆内存,主要存储实例对象(new的东西),而引用在栈中
堆内存大小可以调节
在IDEA里 运行程序-> Edit configuration可以编辑vm配置(该面板中也可以给main方法传递参数)

  • 堆内存分为三个区域
    • 新生区 Young Generation
    • 养老区 old generation
    • 永久区 perm
  • 分区的原因
    不同对象的生命周期不同,但大部分都是临时对象,"朝生夕死"。
    分区后可以缩小垃圾回收搜集范围,多数情况只需要对新生区进行搜集,不需要对整个堆内存进行扫描

11.2 新生区

是类的诞生、成长、甚至消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区分为伊甸区(Eden space)和幸存者区
* 伊甸区 所有类对象在此被new出来
* 幸存区 分为0区(Surivior 0 space)和1区(Survivor 1 space).当伊甸区的空间用完时,程序又需要创建对象。JVM垃圾回收器将对伊甸园区进行垃圾回收(Minor GC 轻量级回收 YGC),将伊甸园区中的不在被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,在将该区进行垃圾回收,然后移动到1区。如果1区也满了,在移动到养老区。
注:From区和To区并不是固定的,复制之后交互,谁空谁是To

11.3 老年区

一个对象被放置到养老区的情况:

  • 对象的年龄达到阈值外 -XX:MaxTenuringThreshold参数来设置阈值,这个阈值用4位来存储,最大值就是15
  • 对象创建后,无法放置到伊甸园区(比如伊甸园区的大小为10m,新的对象大小为11m,伊甸园区不够放,触发YGC。YGC后伊甸园区被清空,但还是无法容下11m的“超大对象”,所以直接放置到养老区。当然如果养老区放置不下则会触发FGC/Major GC,FGC后还放不下则OOM);
  • YGC后,对象无法放置到幸存者To区也会直接晋升到养老区;
  • 如果幸存区中相同年龄的所有对象大小大于幸存区空间的一半,年龄大于或等于这些对象年龄的对象可以直接进入养老区,无需等到年龄阈值。
    注 GC回收主要是在伊甸园区和养老区,jvm 堆内存溢出报错 OOM
    伊甸园区被填满触发轻GC
    养老区被填满触发重GC

11.4 永久区

这个区域用来存放JDK自带的Class对象,interface元数据,java运行时一些环境类信息。不存在垃圾回收,关闭jvm自动释放该区域内存。

  • 在JDK 8以后变成元空间
    java 7

    java 8

    java 7和java8之间的区别是永久区和元空间
    永久区是在JVM的堆内存中存储,java 8以后的元空间并不在虚拟机中而是使用本机的物理内存.
  • 方法区和永久区、元空间的关系
    方法区包括类信息、常量、静态变量等,是JVM规范。 方法区是jvm规范里面的概念。
    • <=1.6 方法区的实现就是永久代。常量池是方法区
    • 1.7 方法区=永久代+堆(常量池和静态变量)实现
    • 1.8 方法区由元空间(类信息)+堆实现(常量池、静态变量)
  • 永久区OOM
    • 启动类加载了大量的第三方jar包
    • Tomcat部署了太多的应用,大量动态生成的反射类,不断被加载,直接到内存满,就会出现OOM.
      例 -Xms8m -Xmx8m -XX:+PrintGCDetails
import java.util.Random;

public class JvmTest {

	public static void main(String[] args) {

		String str ="studydaydayup";
		while(true){
			str += str+ new Random().nextInt(88888)+new Random().nextInt(999999);
		}
	}
}

输出结果

[GC [PSYoungGen: 1861K->320K(2368K)] 1861K->892K(7872K), 0.0011170 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] 
[GC [PSYoungGen: 1954K->288K(2368K)] 2527K->2006K(7872K), 0.0007110 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 1831K->0K(2368K)] [ParOldGen: 4774K->1755K(5504K)] 6606K->1755K(7872K) [PSPermGen: 2477K->2475K(21248K)], 0.0089610 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC [PSYoungGen: 1528K->0K(2368K)] [ParOldGen: 4811K->4811K(5504K)] 6339K->4811K(7872K) [PSPermGen: 2475K->2475K(21248K)], 0.0031400 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC [PSYoungGen: 0K->0K(2368K)] 4811K->4811K(7872K), 0.0004680 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 0K->0K(2368K)] [ParOldGen: 4811K->4796K(5504K)] 4811K->4796K(7872K) [PSPermGen: 2475K->2475K(21248K)], 0.0099630 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:2694)
	at java.lang.String.<init>(String.java:203)
	at java.lang.StringBuilder.toString(StringBuilder.java:405)
	at io.JvmTest.main(JvmTest.java:11)
Heap
 PSYoungGen      total 2368K, used 51K [0x00000000ffd60000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd60000,0x00000000ffd6ce20,0x00000000fff60000)
  from space 320K, 0% used [0x00000000fff60000,0x00000000fff60000,0x00000000fffb0000)
  to   space 320K, 0% used [0x00000000fffb0000,0x00000000fffb0000,0x0000000100000000)
 ParOldGen       total 5504K, used 4796K [0x00000000ff800000, 0x00000000ffd60000, 0x00000000ffd60000)
  object space 5504K, 87% used [0x00000000ff800000,0x00000000ffcaf338,0x00000000ffd60000)
 PSPermGen       total 21248K, used 2508K [0x00000000fa600000, 0x00000000fbac0000, 0x00000000ff800000)
  object space 21248K, 11% used [0x00000000fa600000,0x00000000fa8731f8,0x00000000fbac0000)

11.5 堆空间对象的分配过程

年轻代GC采用复制算法

  1. 刚开始对象在伊甸园区被new出来,幸存者区和养老区都是空的

  2. 随着对象的不断创建,伊甸园区被填满

  3. 触发Minor GC(Young GC),删除未引用的对象,剩下来还存在引用的对象将移动到幸存0区,然后清空伊甸园区:

  4. 随着对象创建,伊甸园区又满了,再次触发轻GC,这次和上次不同,会将留下的对象移动到幸存者1区,并将上一轮GC留下来的存储在幸存者0区的对象年龄递增后移动到幸存者1区,所有幸存对象都移动到幸存者1区后,幸存者0区和伊甸园区空间清除:

  5. 随着对象的创建伊甸园区再次满了,触发第三次 YGC,这一次新滚空间将发生互换,GC留下来的幸存者将移动到幸存者0区,幸存者1区的幸存对象年龄增加后移动到0区,然后未引用的伊甸园区和幸存者1区被清除

  6. 随着YGC的不断发生,幸存对象在两个幸存区不断地交换存储,年龄不断增加,当幸存对象的年龄达到阈值(由JVM参数MaxTenuring Threadshold决定,此例中是8),他们将被移动到养老区

  7. 随着上述过程的不断出现,当养老区快满时,将触发FGC 将养老区的内存清理,若养老区执行了GC之后发现仍然无法进行对象的保存,就会产生OOM异常。

12 堆内存调优

12.1 出现堆内存OOM怎么办

  1. 尝试扩大堆内存看结果
    -Xms1024m -Xmx1024m -XX:+PrintGCDetails
    2.如果仍旧OOM可能存在死循环,分析内存,看下哪个地方出现了问题
  • 专业工具 MAT(eclipse), Jprofiler(IDEA) 内存快照分析工具,能看到代码第几行出错(推荐)
  • Debug:一行行代码分析
  1. MAT,Jprofiler
  • 分析Dump内存文件,快速定位内存
  • 获得堆中的数据
  • 获得大的对象

12.2 Jprofiler安装(参考)

  • IDEA里安装插件File->Settings->Plugins->搜索Jprofiler(没有的话去仓库搜索)安装->重启IDEA

    完成后IDEA里打开有图标
  • 再安装客户端,官网下载安装目录注意不要有空格
  • IDEA运行环境配置
  • IDEA点击JProfiler图标时,会自动弹出JProfiler窗口,在里面就可以监控自己的代码性能

12.3 生成dump文件

  • 设置VM options: -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError(出现OOM异常,就把文件dump下来)

    • Xms设置初始化内存大小,Xmx设置最大分配内存,一般测试时都要调小,容易分析也不会占用太大内存。
    • -XX:+,中加号表示命令
      On后面是dump条件,同理也可以dump其他异常或者错误情况
  • 运行后dump文件如存在src同级目录里,分析完后要及时清理,通常文件比较大

  java.lang.OutOfMemoryError: Java heap space
  Dumping heap to java_pid7851.hprof ...
  Heap dump file created [5451477 bytes in 0.061 secs]
  Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  	at java.util.Arrays.copyOfRange(Arrays.java:2694)
  	at java.lang.String.<init>(String.java:203)
  	at java.lang.StringBuilder.toString(StringBuilder.java:405)
  	at io.JvmTest.main(JvmTest.java:11)

  • 双击打开dump文件在Jprofiler里显示分析
    • 常用big Object选项查看哪个占用内存异常大
    • 常用左侧thread选项,会显示多少行造成内存泄漏
  • 另外JAVA中Runtime类,提供一些方法来分析内存
    Runtime.getRuntime().maxMemory()
    Runtime.getRuntime().maxMemory()

13 垃圾回收GC

参考链接 1
参考链接2
JVM在进行GC时主要针对堆和方法区,其中99%在堆的以下区域: 伊甸区+幸存区+老年区

13.1 怎么判断对象可回收

两种经典算法:引用计数法和可达性分析算法

13.1.1 引用计数算法

通过判断对象的引用数量来决定对象是否可以被回收。给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能在被使用的。
优点:效率高
缺点:存在相互循环引用问题。
例:循环引用 object a 和object b互为对方的属性,尽管没有其他的引用了,但是互为引用计数器值始终为1,不能被GC回收。

java虚拟机里面不使用该算法来管理内存,而使用可达性分析

13.1.2 可达性分析

从"GC Roots"对象作为起点向下搜索,所经过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连即这个对象不可达时,则证明此对象是不可用的,可以被回收了。

  • 什么是GC Roots
    以当前存活的对象集为root,遍历出他们(引用)关联的所有对象(Heap中的对象),没有遍历到的对象即为非存活对象,这部分对象可以gc掉。这里的初始存活对象集就是GC Roots。
    哪些对象可以作为GC root?因为gc大多数情况都是在堆中发生的,那么方法区,本地方法区,栈(不受GC管理)引用的对象就可以当作GC Root对象。
    常见的4个GC Roots 对象
    • 虚拟机栈中引用的对象
    • 方法区中静态属性实体引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI引用的对象
      还有其他一些对象也可能是GC Roots的一部分,比如被classloader加载的class对象,monitor的对象,被JVM持有的对象等等,这些都需要视当前情况而定

13.2 垃圾回收算法

13.2.1 复制算法

将内存容量划分为大小相等的两块,每次只使用一块,触发GC时,将还活着的复制到另一个块上面,然后清空当前区域

  • 优点: 没有内存碎片
  • 缺点: 浪费空间(一个幸存区是空的)
    一般eden:from:to的大小为8:1:1,具体过程参考11.5
    复制算法最佳使用场景:对象存活度较低的时候

13.2.2 标记清除算法

分为标记和清除两个阶段
第一次扫描,给活着的对象做标记
第二次扫描,对没有标记的对象做清除

优点:不会额外浪费空间
缺点:两次扫描严重浪费时间,会产生内存碎片

13.2.3 标记压缩算法

标记压缩算法在标记清除算法的基础上,进一步优化,防止内存碎片的产生
分为标记和压缩两个阶段,标记阶段仍旧标记出所有存活的对象。
但压缩阶段,再次扫描,向一端移动存活对象,清空边界外所有的对象,多了一个移动整理的成本

再优化,标记清除多次后,再进行压缩

13.2.4 分代收集算法

总结
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记清除算法=标记压缩算法>复制算法
没有最好的算法,只有最合适的算法,都是在时间和空间取舍-->GC:分代收集算法

年轻代---存活率低,采用复制算法
老年代---存活率高,区域大,使用标记清除+标记压缩算法实现
jvm调优,调参数只要内存碎片不是太多,就继续标记清除,达到一定阈值再压缩

14 JMM

对于一个陌生的技术,从3个维度去学习它
它是什么?它有什么用?它该如何学习?
以下参考链接
参考链接2

14.1. 什么是JMM?

JMM--Java Memory Model的缩写,是java内存模型。

  • 现代计算机硬件内存模型

    存取速度慢->快:内存(本地硬盘)<高速缓存<寄存器
    运行原理:CPU寄存器处理速度远快于主内存(不是一个量级的),在二者之间加入高速缓存作为缓冲,将运算用到的数据复制到缓存里,让运算快速进行。运算结束后,在从缓存同步到内存中。
    缓存一致性问题:每一个CPU都有自己的高速缓存,但是主内存区域是共享的。当多个CPU的运算任务都涉及同一块主内存区域,可能导致各自缓存的数据不一致。所以要遵循协议缓存Cache一致性协议(比如MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol)来解决CPU本地缓存和主内存数据不一致问题。

同理对应计算机内存模型,java也有类似的模型

  • java 内存模型

    JMM在线程和主内存(共享变量存储位置)之间,抽象出了工作内存(local memory).主要对应寄存器和高速缓存区。
    工作原理:每个线程将数据拷贝到本地内存进行操作,操作完成后在某个时刻批量刷新回主内存。

14.2. 它是干嘛的,有什么用?

作用:缓存一致性协议,用于定义数据读写规则(遵守,找到这个规则).

  • java 内存模型和 jvm内存模式的区别
    JVM内存模式指的是JVM的内存分区;而Java内存模式是一种虚拟机规范,JMM规范了Java虚拟机及计算机内存是如何协作的,一个线程如何何时
    可以看到其他线程修改过后的共享变量值,在必须时如何同步的访问共享变量。

  • java内存模型定义了8个操作,完成主内存和工作内存交互

    read:把一个变量的值从主内存传输到工作内存中
    load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
    use:把工作内存中一个变量的值传递给执行引擎
    assign:把一个从执行引擎接收到的值赋给工作内存的变量
    store:把工作内存的一个变量的值传送到主内存中
    write:在 store 之后执行,把 store 得到的值放入主内存的变量中
    lock:作用于主内存的变量
    unlock

  • 内存模型的三大特性:

    • 原子性
    • 可见性
    • 有序性
      • 指令重排序,最终执行的指令顺序是被优化后的,和源码顺序不一致。这样做的好处是先利用寄存器已有的数据计算,不用频繁访问内存,效率高。
        指令重排序,会考虑数据依赖性,不会破坏有依赖的数据之间的顺序。在同一个线程内看还是有序的,执行结果一致,但是在另一个线程看此线程就是无序混乱的。

14.3 它该如何学习?

各个官方,博客,视频主要涉及学习线程并发的东西,synchronized,lock,volatile(写操作直接写回主内存,保证可见性,有序性,不保证原子性),final(不了解)
最后搜索"JMM的面试题"

posted @ 2021-05-13 16:50  晒网达人  阅读(147)  评论(0编辑  收藏  举报