【学习笔记】jvm

jvm

视频教程网址黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓_哔哩哔哩_bilibili

文章目录


一,jvm的概念

​ jvm:Java virtual machine即Java虚拟机,Java程序的运行环境(Java二进制字节码的运行环境),正是因为是虚拟机运行的,所以可以跨平台运行

Java源代码---Javac--->Java class字节码----jvm---->载入内存的运行阶段

二,jvm的内存结构

image-20211209101438818在这里插入图片描述

1,程序计数器

1.1作用:记录下一条jvm指令的地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VD7DfpPc-1650292825916)(D:\文档\学习资料\笔记\jvm.assets\image-20211209101759047.png)]

1.2特点:
  • 线程私有:随着线程创建而创建,销毁而销毁
  • 不会出现内存溢出
  • 是一块比较小的内存区域

2,虚拟机栈

2.1概念:
  • 线程运行所需要的内存空间

  • 有多个栈帧组成(栈帧:每个方法运行时所需要的内存,包括属性,方法等)

  • 每个线程只能有一个活动栈帧

  • 栈溢出:

    • 栈帧太多
    • 栈帧太大
    • 使用-Xss 空间大小 即可改变初始空间大小(初始化是几百k)
  • 注意:

    1. 栈不需要垃圾回收机制,因为栈中存放的时运行时方法所需要保存的数据,当方法运行完自然会把原先的数据弹出
    2. 栈的空间大小并不是越大越好,由于物理内存是一定的,所以栈的空间设置大了那所能产生的线程反而会变少,但是栈的空间较大是可以增加递归调用的次数的
    3. 栈空间方法内线程私有,每个线程对应一个栈空间,不会相互干扰(除了全局变量或其他能够让多个线程访问的变量)
2.2线程诊断(linux命令)

​ top 查看进程占用情况

​ ps H -eo pid,tid,%cpu | grep 进程id 显示特点进程的线程总数量(H),进程id,线程id,cpu占比

​ jstack 进程id 查看Java虚拟机中所有指定的进程信息,再将进程id转换为十六进制对比nid找到对应线程(方 法),即可进一步得知出问题的代码行数,在最后还会有关于进程是否发生死锁的信息,同样可以定位到具体 的代码行数

3,本地方法栈

  • 概念:jvm连接执行引擎时所需要的方法存放的位置

4,堆

4.1定义

通过new生成的对象都会放在堆中

4.2特点
  • 它是线程共享的,堆中的对象需要考虑线程安全的问题

  • 有垃圾回收机制

  • 内存溢出

    • 使用-Xmx 空间大小 即可改变初始堆空间的大小(默认是好几个G)
      在这里插入图片描述
4.3堆内存诊断
  • 工具(Terminal中输入命令):

    1. jps工具:jps 查看当前系统中有哪些Java进程

    2. jmap工具:

      jmap -heap 进程id 查看特定进程某一时刻堆内存占用情况

      jmap -dump:format=b,live,file=文件名.bin 进程id 将某时刻堆的占用情况存为(当前目录下)文件格式为二进制,存活的对象

    3. jconsole工具:图形界面的多功能检测工具,可以连续检测

    4. jvirsualvm工具:双击(连接)对应进程—>检测---->堆dump—>检查—>查找,即可查看各个实例内存的占用大小
      在这里插入图片描述

在这里插入图片描述

5,方法区

5.1概念

​ 是所有线程共享的区域,它存贮了类的属性,方法,代码,逻辑上是堆的一部分(但并不强制位置)

5.2组成

在这里插入图片描述

5.3内存溢出
  • 1.8 以前会导致永久代内存溢出(使用的是Java内存)

    默认没有内存大小上限,使用 -XX:MaxPermSize=8m设置方法区最大值

  • 1.8 之后会导致元空间内存溢出(使用的是系统内存)

    默认没有内存大小上限,使用-XX:MaxMetaspaceSize=8m设置方法区最大值

5.4运行时常量池

//使用javap -c 类名.class 即可将对应类进行进行反编译

  • 常量池(constant pool),就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
  • 运行时常量池,常量池是class文件中的,当它被加载时,它的常量池信息就会放进运行时常量池,并把里面的符号地址变为真实地址
5.5StringTable(串池)
  • 是个hashTable,贮存k-v值
  • 当执行到需要加载一个字符串时会先到StringTable中查找(边运行边加载,不会一下子将所有字符串都加载进来),如果没有就会将该字符串加入StringTable(可避免重复创建字符串对象)

//反编译中aload代表取出,astore则是存入

在这里插入图片描述

s4是通过String Builder重新生成的,所以和s3不是同一个对象,它也不是存放在串池中的,而是存放在堆区中的

在这里插入图片描述

s5是直接去常量池中找“ab”并不是先找“a”再找“b”最后拼在一起(java编译期间的优化),所以它和s3是一个东西,上一个点的s4是通过s1和s2拼接而来,所以是有可能发生改变的,所以必须用StringBuilder来进行动态拼接

  • 可以使用 字符串.intern可以将字符串主动放入串池,并返回池中该字符串的地址,如果池中已经有有了则不会讲字符串放入,并返回池中那个已经有了的字符串地址(jdk1.8以上,若是1.8一下则在存入串池时复制一个新的字符串对象放入串池)

  • 串池位置如组成图所示

5.6StringTable垃圾回收
  • 打印信息

    -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
    

    设置堆大小,打印StringTable信息,打印垃圾回收信息

  • 哈希表就是数组加链表,数组个数成为桶(buckets)

    • 调整 -XX:StringTableSize=桶个数,一定范围内桶的个数越多速度越快

    • 考虑将字符串对象是否入池,用intern()来避免重复创建过多字符串(在下面的例子中如果没有主动加上intern的话是不会进入串池,直接在堆区创建对象)

在这里插入图片描述

6,直接内存

6.1定义:

Direct Memory,常见于 NIO 操作时,用于数据缓冲区

分配回收成本较高,但读写性能高

不受 JVM 内存回收管理,属于系统内存,有可能导致内存溢出

6.2读取原理

不适用直接内存的话,Java在条用系统读写操作时会经过两个缓冲区

byte[] buf = new byte[_1Mb];
int len = file.read(buf);

在这里插入图片描述

使用直接内存可以在系统和堆内存中创建一个共享内存,减少了一层缓冲空间

ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
int len = file.read(bb);

在这里插入图片描述

6.3分配和回收原理
  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法

  • ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffffer 对象,一旦ByteBuffffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

  • 使用-XX:+DisableExplicitGC禁用显式垃圾回收(由程序员写代码主动进行的垃圾回收),但是会影响直接内存的主动回收,从而降低性能


三,垃圾回收

1,如何判断对象可以回收

1.1引用计数法

​ 当一个对象没有引用的时候则可以进行垃圾回收

​ 如下图当两个对象相互引用时则不会被回收,从而形成死锁

在这里插入图片描述

1.2 可达性分析算法
  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以

​ 回收

  • 哪些对象可以作为 GC Root ?

    使用memory analyzer(MAT)来打开使用jmap保存下来的.bin文件即可查看此快照的GC Root,打开主线程即可查看有哪些对象

1.3 四种引用
1.3.1强引用
  • 定义:只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
1.3.2软引用(SoftReference)
  • 定义:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象,可以配合引用队列来释放软引用自身

  • 使用软引用

在这里插入图片描述

​ 运行结果

  • 配合队列回收软引用(上面例子中的四个null就没移除了)

1.3.3弱引用(WeakReference)
  • 定义:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身

  • 使用

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e0qezrfe-1650292635769)(D:\文档\学习资料\笔记\jvm.assets\image-20211211210152824.png)]

1.3.4虚引用(PhantomReference)
  • 定义:必须配合引用队列使用,主要配合 ByteBuffffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
1.3.5终结器引用(FinalReference)
  • 定义:同样需要配合引用队列使用,无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程(优先级很低)通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象

2,垃圾回收算法

2.1标记清除

​ 速度较快,但容易产生空间碎片

2.2标记整理

​ 速度较慢,但不会产生空间碎片(标记+整理空间)

2.3复制

​ 不会有内存碎片,需要占用双倍空间(标记整理+复制)

在这里插入图片描述

​ (最后还要交换FROM和TO这两个大空间即交换FROM和TO的指针,以备循环)

2.4分代垃圾回收

不会采用其中一种单独的算法,而是多种算法灵活应用
在这里插入图片描述

  • 对象首先分配在伊甸园区域

  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to

  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit),但如果新生代空间紧张尽管对象还没有到晋升阈值也可以直接放入老年代,或者新进来的对象太大,新生代放不下同样是可以直接放到老年代的

  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW(stop the world)的时间更长

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )(初始化和最大值同时进行)
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio(初始化比例) 和 -XX:+UseAdaptiveSizePolicy(开关)
幸存区比例-XX:SurvivorRatio=ratio(如果数值是8则说明新生代有10份空间,8份是伊甸园,将剩下的2份空间进行二等分,一份给from一份给to)
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC-XX:+ScavengeBeforeFullGC(是个开关)

注意:当子线程中发生内存溢出时不会影响主线程的正常运行


四,垃圾回收器

4.1分类

  1. 串行单线程

    • 堆内存较小,适合个人电脑
  2. 吞吐量优先

    • 多线程

    • 堆内存较大,需要多核 cpu

    • 让单位时间内,STW 的时间最短,垃圾回收时间占比最低,这样就称吞吐量高

  3. 响应时间优先

    • 多线程

    • 堆内存较大,需要多核 cpu

    • 尽可能让单次 STW 的时间最短

4.2串行

  • 使用

    开启命令:-XX:+UseSerialGC = Serial + SerialOld

  • 由两部分组成(两个组成部分分别进行回收):一个是新生代的回收器Serial 使用复制算法,另一个是老年代的回收器SerialOld使用标记整理算法

  • 回收过程
    在这里插入图片描述

4.3吞吐量优先

  • 使用

    • 开启命令:-XX:+UseParallelGC ~ -XX:+UseParallelOldGC (jdk1.8以上默认开启,两个开启一个就会自动开启另一个)

    • 设置并行线程数:-XX:ParallelGCThreads=n

      //并行与并发的概念
      你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
      你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
      你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
      并发的关键是你有处理多个任务的能力,不一定要同时。
      并行的关键是你有同时处理多个任务的能力。
      所以我认为它们最关键的点就是:是否是『同时』。
      
    • 开启动自适应大小策略(包括新生代伊甸园比例,堆大小,晋升阈值):-XX:+UseAdaptiveSizePolicy

    • 设置垃圾回收占用时间:-XX:GCTimeRatio=ratio

      GC时间占用率公式为1/(1+ratio),默认为99则GC时间占用率为0.01,vm将调整堆大小来实现一百分钟只能有一分钟用于垃圾回收这个标准,一般是将堆空间增大,占用率才会变小

    • 设置每次最大暂停时间:-XX:MaxGCPauseMillis=ms

      默认值为200ms,若想要降低每次最大暂停时间,一般是需要减少堆空间的,所以与上一条形成对立关系

  • 同样是由新生代(复制)和老年代组成(标记整理)

  • 回收过程(同时进行的线程数默认与你的cpu核数相同)
    在这里插入图片描述

4.4响应时间优先(CMS)

  • 使用

    • 开启命令:-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld (老年代和新生代回收器)

      老年代回收器在某些时刻可以和用户线程并发执行(concurrent),不过有时会并发失败,并发失败时则退化到 SerialOld 串行回收器

    • 设置并行、并发线程数:-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads

      并行线程数默认值为cpu核数,并发线程数建议为并行的四分之一

    • 设置启动垃圾回收内存占比时机:-XX:CMSInitiatingOccupancyFraction=percent

      //浮动垃圾:在并发清理过程中产生的垃圾
      

      如果像其他垃圾回收机制一样等到内存占比接近100%时在启动的话,由于浮动垃圾的存在就有可能发生内存溢出,所以需要设置启动垃圾回收内存占比时机

    • 开启在重新标记之前对新生代进行一次垃圾回收:-XX:+CMSScavengeBeforeRemark

      以减少重新标记时查找的数量(是一个开关,+代表开启,-代表禁用)

  • 同样有新生代(复制算法)和老年代(标记清除算法)回收器组成

    由于老年代并没有整理空间碎片的功能,所以会由于空间碎片导致的空间不足导致并发失败,此时则会采用SerialOld 串行回收器执行标记整理算法

  • 回收过程(在并发标记后需要再进行一次标记,因为在并发标记过程中其他线程可能改变对象的引用之类)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFixUw2C-1650292635774)(D:\文档\学习资料\笔记\jvm.assets\image-20211212190708440.png)]

4.5G1

  • 定义:Garbage First

    2004 论文发布

    2009 JDK 6u14 体验

    2012 JDK 7u4 官方支持

    2017 JDK 9 默认(废弃CMS)

  • 适用场景

    同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms

    适应超大堆内存,会将堆划分为多个大小相等的 Region(分区,里面同样有伊甸园等结构)

    整体上是 标记+整理 算法,两个区域之间是 复制 算法

  • 相关 JVM 参数

    启用(jdk1.8不是默认的):-XX:+UseG1GC

    设置分区的大小(必须是2的整数倍):-XX:G1HeapRegionSize=size

    设置暂停目标:-XX:MaxGCPauseMillis=time

1) G1 垃圾回收阶段

2) Young Collection(新生代的垃圾回收)
  • 会有stw

    E代表伊甸园

    S表示幸存区,将多个伊甸园复制到一个幸存区

O表示老年代,到达晋升阈值就会放到老年代,年龄不够就和其他伊甸园一样进行垃圾回收并复制到新的幸存区

3) Young Collection + CM
  • 在 Young GC 时会进行 GC Root 的初始标记(标记根对象)

  • 老年代占用堆空间比例达到阈值时,进行并发标记(标记根对象下的子对象,不会 STW),由下面的 JVM 参数决定-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

4) Mixed Collection

会对 E、S、O 进行全面垃圾回收,在老年代进行回收时因为有最大暂停时间的目标,所以会选择回收价值最大的几个老年代进行回收

  • 最终标记(Remark,相当于CMS的重新标记)会 STW

  • 拷贝存活(Evacuation)会 STW

设置最大存活时间:-XX:MaxGCPauseMillis=ms

5) Full GC
  • SerialGC

    新生代内存不足发生的垃圾收集 - minor gc

    老年代内存不足发生的垃圾收集 - full gc

  • ParallelGC

    新生代内存不足发生的垃圾收集 - minor gc

    老年代内存不足发生的垃圾收集 - full gc

  • CMS

    新生代内存不足发生的垃圾收集 - minor gc

    老年代内存不足

  • G1

    新生代内存不足发生的垃圾收集 - minor gc

    老年代内存不足

其中CMS和G1的老年代内存不足要分两种情况:
1,并发回收速度可以跟得上垃圾的产生速度,则不算是full GC
2,并发护手速度跟不上垃圾的产生速度,此时会发生并发失败,则将老年代的回收器退化为串回收的老年代回收器,这种则可以称为full GC,与SerialOld不同的是,G1中退化的老年代回收器已经优化为多线程了
6) Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题

    脏卡:引用了新生代对象的老年区对象
    
    在新生代的可达性分析中需要找到其根对象(跟对象一般在老年区中),如果要遍历整个老年区效率太低,于是将老年去进行分区,成为卡表(每个卡大概时512k),只需要关注脏卡即可
    

  • 卡表与 Remembered Set(记录外部对自身的引用,将其标记为脏卡)

  • 在引用变更时通过 post-write barrier + dirty card queue

    //后写屏障:用于更新脏卡,将更新操作放在脏卡队列中
    //脏卡队列:更新脏卡是个异步操作,会有线程来执行队列里的操作
    
  • concurrent refinement threads 更新 Remembered Set

7) Remark(最终标记,重新标记)
  • pre-write barrier(前写屏障) + satb_mark_queue(混合标记队列)

    //黑色代表已经标记完成,灰色灰色代表正在进行标记,白色代表尚未标记
    //在进行并发标记时如果有其他线程改变对象引用,前写屏障会将引用被改变的对象加入混合标记队列,再由其他线程来检查队列中的对象是否可以被垃圾回收(父级对象是强引用之类),从而进行一个最终标记(重新标记)
    

8)调优: JDK 8u20 字符串去重
  • 优点:节省大量内存

  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

  • 开启命令:-XX:+UseStringDeduplication

  • 过程:

String s1 = new String("hello"); // char[]{'h','e','l','l','o'} 
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
将所有新分配的字符串放入一个队列
当新生代回收时,G1并发检查是否有字符串重复
如果它们值一样,让它们引用同一个 char[]
  • 注意

    与 String.intern() 不一样,String.intern() 关注的是字符串对象,而字符串去重关注的是 char[],在 JVM 内部,使用了不同的字符串表

9)功能增强: JDK 8u40 并发标记类卸载
  • 说明:所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类,在大型公司有许多自定义的类加载器,这个功能十分有必要

  • 开启命令:-XX:+ClassUnloadingWithConcurrentMark (默认启用)

10) 功能增强:JDK 8u60 回收巨型对象
//巨型对象:一个对象大于 region(分区) 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝

  • 回收时被优先考虑

  • G1 会跟踪巨型对象在老年代中所有 incoming (引用巨型对象的引用)引用,这样老年代 incoming 引用为0 的巨型对象(即没有老年代引用他了)就可以在新生代垃圾回收时处理掉

11)功能增强: JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC,stw会比较长,尽快启动混合回收就能有效避免退化为FullGC

  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 来调整启动老年代垃圾回收的内存占比阈值

    但是这个阈值调太小会频繁进行老年代回收,调太大会触发FullGC,都会降低运行效率
    
  • JDK 9 可以动态调整

    -XX:InitiatingHeapOccupancyPercent 用来设置初始值

    进行数据采样并动态调整(相当于是让机器来代替人脑去进行调试,试出最优阈值)

    总会添加一个安全的空档空间(多一份保障来容纳浮动垃圾)

12) 更多:JDK 9 更高效的回收

​ 250+增强

​ 180+bug修复

​ https://docs.oracle.com/en/java/javase/12/gctuning


五,垃圾回收调优

预备知识:

  • ​ 掌握 GC 相关的 VM 参数,会基本的空间调整

  • 掌握相关工具

  • 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

5.1 调优领域
  • 内存

  • 锁竞争

  • cpu 占用

  • io

5.2 确定目标
  • 【低延迟】还是【高吞吐量】,选择合适的回收器

  • CMS,G1,ZGC(低延迟)

  • ParallelGC(高吞吐)

  • Zing(据说是零延迟,可以处理超大堆空间)

5.3 最快的 GC
  • 答案是不发生 GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题数据是不是太多?

    resultSet = statement.executeQuery(“select * from 大表 limit n”)

    不可以直接将数据库还没经过筛选的数据全部放进Java内存中,应当在数据库查询时就尽可能加上筛选条件

  • 数据表示是否太臃肿?

    • 对象图:查询出需要的数据即可,不需要将对象整个数据放进来
    • 对象大小:能用基本的数据类型就用最基本的,如果用封装类型会增加内存
  • 是否存在内存泄漏?

    • 不可以往对象中一直加数据,从而造成泄露/溢出

    • 若是长时间存在的对象:考虑用软、弱引用

    • 缓存数据:不建议直接用Java实现,可以考虑用第三方缓存(如redis)实现,第三方自己会考虑数据存活时间的

5.4 新生代调优
  • 新生代的特点

    • 所有的 new 操作的内存分配非常廉价

      • TLAB thread-local allocation buffffer(线程分配局部缓冲区)

        //TLAB:每个线程都会在伊甸园中分配一块区域,以解决不同线程在申请/分配空间时的并发安全问题
        
    • 死亡对象的回收代价是零

    • 大部分对象用过即死

    • Minor GC 的时间远远低于 Full GC(相差大概一到两个数量级)

  • 新生代空间越大越好吗?

    设置新生代最大、初始化空间命令:-Xmn

    设置新生代堆的初始大小和最大大小(字节)。GC在这一地区的出现频率高于其他地区。如果新生代的空间太小,则会执行许多次要的垃圾回收。如果空间太大,很多新生代都可以晋升到老年代,则仅靠FullGC来回收垃圾,这可能需要很长时间才能完成。建议您将年轻一代的大小保持在25%以上,小于总堆大小的50%
    
  • 新生代需要能容纳所有【并发量 * (一次请求-响应中产生的数据)】的数据

  • 幸存区需要能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置得当,让长时间存活对象尽快晋升

    -XX:MaxTenuringThreshold=threshold (最大晋升阈值)

    -XX:+PrintTenuringDistribution (显示晋升详细信息)

    Desired survivor size 48286924 bytes, new threshold 10 (max 10)

- age 1: 28992024 bytes, 28992024 total 
\- age 2: 1366864 bytes, 30358888 total 
\- age 3: 1425912 bytes, 31784800 total 
...
5.5 老年代调优
//以 CMS 为例
  • CMS 的老年代内存越大越好

  • 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代

  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

    -XX:CMSInitiatingOccupancyFraction=percent(启动FullGC内存占比阈值一般是75%-80%)

5.6 案例
  • 案例1 Full GC 和 Minor GC频繁

    1. 增大新生代空间
    2. 设置较大的晋升阈值
  • 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (低延迟:CMS)

    • 开启在重新标记之前对新生代进行一次垃圾回收:-XX:+CMSScavengeBeforeRemark

      减少重新标记所耗费的时间

  • 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

    ​ jdk1.8之前,可能是永久代空间不足,提高或不限制永久代空间即可

    默认没有内存大小上限,使用 -XX:MaxPermSize=8m设置方法区最大值
    

六,类加载与字节码技术

  • 学习目标:

    1. 类文件结构

    2. 字节码指令

    3. 编译期处理

    4. 类加载阶段

    5. 类加载器

    6. 运行期优化

1. 类文件结构

一个简单的 HelloWorld.java

package cn.itcast.jvm.t5; // HelloWorld 示例 
public class HelloWorld { 
    public static void main(String[] args) {
    	System.out.println("hello world"); 
    }
}

执行 javac -parameters -d . HellowWorld.java

//加上-parameters就会保留args的参数信息

编译为 HelloWorld.class 后是这个样子的:

[root@localhost ~]# od -t xC HelloWorld.class 
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13 
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
0001120 00 00 02 00 14

根据 JVM 规范,类文件结构如下:

ClassFile { 
//u4:0-4个字节;魔数magic number
u4 magic; 
//u2:4-6个字节,以此类推;小版本号
u2 minor_version; 
//大版本号
u2 major_version; 
//常量池信息
u2 constant_pool_count; 	//常量池长度
cp_info constant_pool[constant_pool_count-1]; 
//访问修饰(类是公共的还是私有的等等)
u2 access_flags; 
//类名,包名
u2 this_class; 
//父类信息
u2 super_class; 
//接口信息
u2 interfaces_count; 
u2 interfaces[interfaces_count]; 
//成员变量信息
u2 fields_count; 
field_info fields[fields_count]; 
//类中的方法信息
u2 methods_count; 
method_info methods[methods_count]; 
//类中的附加属性信息
u2 attributes_count; 

attribute_info attributes[attributes_count]; 

}
1.1 魔数

0~3 字节,表示它是否是【class】类型的文件

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

1.2 版本

4~7 字节,表示类的版本 00 34(52) 表示是 Java 8(51则是Java 7,53则是Java 9以此类推)

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

1.3 常量池

8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#1项 0a 表示一个 Method 信息(查本知识点后面的表格),00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项

来获得这个成员变量的【所属类】和【成员变量名】

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26

项来获得这个方法的【所属类】和【方法名】

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【 】(构造方法)

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【()V】其实就是表示无参、无返回值

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

第#9项 01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是【Code】

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

第#10项 01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65

是【LineNumberTable】

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63

第#11项 01 表示一个 utf8 串,00 12(18) 表示长度,4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61

62 6c 65是【LocalVariableTable】

0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63

0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01

第#12项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是【this】

0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01

0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63

第#13项 01 表示一个 utf8 串,00 1d(29) 表示长度,是【Lcn/itcast/jvm/t5/HelloWorld;】

0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63

0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f

0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16

第#14项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是【main】

0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16

第#15项 01 表示一个 utf8 串,00 16(22) 表示长度,是【([Ljava/lang/String;)V】其实就是参数为

字符串数组,无返回值

0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16

0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72

0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13

第#16项 01 表示一个 utf8 串,00 04 表示长度,是【args】

0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13

第#17项 01 表示一个 utf8 串,00 13(19) 表示长度,是【[Ljava/lang/String;】

0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13

0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69

0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61

第#18项 01 表示一个 utf8 串,00 10(16) 表示长度,是【MethodParameters】

0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61

0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46

第#19项 01 表示一个 utf8 串,00 0a(10) 表示长度,是【SourceFile】

0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46

0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64

第#20项 01 表示一个 utf8 串,00 0f(15) 表示长度,是【HelloWorld.java】

0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

第#21项 0c 表示一个 【名+类型】,00 07 00 08 引用了常量池中 #7 #8 两项

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

第#22项 07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

第#23项 0c 表示一个 【名+类型】,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64

第#24项 01 表示一个 utf8 串,00 0f(15) 表示长度,是【hello world】

0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64

第#25项 07 表示一个 Class 信息,00 20(32) 引用了常量池中 #32 项

0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

第#26项 0c 表示一个 【名+类型】,00 21(33) 00 22(34)引用了常量池中 #33 #34 两项

0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

第#27项 01 表示一个 utf8 串,00 1b(27) 表示长度,是【cn/itcast/jvm/t5/HelloWorld】

0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c

0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61

第#28项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/Object】

0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61

0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61

第#29项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/System】

0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61

0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f

第#30项 01 表示一个 utf8 串,00 03 表示长度,是【out】

0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f

0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72

第#31项 01 表示一个 utf8 串,00 15(21) 表示长度,是【Ljava/io/PrintStream;】

0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72

0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76

第#32项 01 表示一个 utf8 串,00 13(19) 表示长度,是【java/io/PrintStream】

0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76

0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d

第#33项 01 表示一个 utf8 串,00 07 表示长度,是【println】

0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a

第#34项 01 表示一个 utf8 串,00 15(21) 表示长度,是【(Ljava/lang/String;)V】

0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a

0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

Constant TypeValue
CONSTANT_Class7
CONSTANT_Fieldref9
CONSTANT_Methodref10
CONSTANT_InterfaceMethodref11
CONSTANT_String8
CONSTANT_Integer3
CONSTANT_Float4
CONSTANT_Long5
CONSTANT_Double6
CONSTANT_NameAndType12
CONSTANT_Utf81
CONSTANT_MethodHandle15
CONSTANT_MethodType16
CONSTANT_InvokeDynamic18
1.4 访问标识与继承信息

21 表示该 class 是一个类,公共的(20+01)

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

05 表示根据常量池中 #5 找到本类全限定名

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

06 表示根据常量池中 #6 找到父类全限定名

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

表示接口的数量,本类为 0

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

Flag NameValueInterpretation
ACC_PUBLIC0x0001Declared public ; may be accessed from outside its
ACC_FINAL0x0010Declared final ; no subclasses allowed.
ACC_SUPER0x0020Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE0x0200Is an interface, not a class.
ACC_ABSTRACT0x0400Declared abstract ; must not be instantiated.
ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.
ACC_ANNOTATION0x2000Declared as an annotation type.
ACC_ENUM0x4000Declared as an enum type.
1.5 Field(成员变量) 信息

表示成员变量数量,本类为 0

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

FieldTypeTypeInterpretation
Bbytesigned byte
CcharUnicode character code point in the Basic Multilingual Plane,encoded with UTF-16
Ddoubledouble-precision flfloating-point value
Ffloatsingle-precision flfloating-point value
Iintinteger
Jlonglong integer
L ClassName;referencean instance of class ClassName
Sshortsigned short
Zbooleantrue or false
[referenceone array dimension
1.6 Method 信息

表示方法数量,本类为 2

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成

  • 红色代表访问修饰符(本类中是 public)

  • 蓝色代表引用了常量池 #07 项作为方法名称

  • 绿色代表引用了常量池 #08 项作为方法参数描述

  • 黄色代表方法属性数量,本方法是 1

  • 红色代表方法属性

    • 00 09 表示引用了常量池 #09 项,发现是【Code】属性
    • 00 00 00 2f 表示此属性的长度是 47
    • 00 01 表示【操作数栈】最大深度
    • 00 01 表示【局部变量表】最大槽(slot)数00 00 00 05 表示字节码长度,本例是 5
    • 2a b7 00 01 b1 是字节码指令
    • 00 00 00 02 表示方法细节属性数量,本例是 2
    • 00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
      • 00 00 00 06 表示此属性的总长度,本例是 6
      • 00 01 表示【LineNumberTable】长度
      • 00 00 表示【字节码】行号 00 04 表示【java 源码】行号
    • 00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
      • 00 00 00 0c 表示此属性的总长度,本例是 12
      • 00 01 表示【LocalVariableTable】长度
      • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
      • 00 05 表示局部变量覆盖的范围长度
      • 00 0c 表示局部变量名称,本例引用了常量池 #12 项,是【this】
      • 00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是【Lcn/itcast/jvm/t5/HelloWorld;】
      • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01

0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00

0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00

0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00

  • 红色代表访问修饰符(本类中是 public static)
  • 蓝色代表引用了常量池 #14 项作为方法名称
  • 绿色代表引用了常量池 #15 项作为方法参数描述
  • 黄色代表方法属性数量,本方法是 2
  • 红色代表方法属性(属性1)
    • 00 09 表示引用了常量池 #09 项,发现是【Code】属性
    • 00 00 00 37 表示此属性的长度是 55
    • 00 02 表示【操作数栈】最大深度
    • 00 01 表示【局部变量表】最大槽(slot)数
    • 00 00 00 05 表示字节码长度,本例是 9
    • b2 00 02 12 03 b6 00 04 b1 是字节码指令
    • 00 00 00 02 表示方法细节属性数量,本例是 2
    • 00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
      • 00 00 00 0a 表示此属性的总长度,本例是 10
      • 00 02 表示【LineNumberTable】长度
      • 00 00 表示【字节码】行号 00 06 表示【java 源码】行号
      • 00 08 表示【字节码】行号 00 07 表示【java 源码】行号
    • 00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
      • 00 00 00 0c 表示此属性的总长度,本例是 12
      • 00 01 表示【LocalVariableTable】长度00 00 表示局部变量生命周期开始,相对于字节码的偏移量
      • 00 09 表示局部变量覆盖的范围长度
      • 00 10 表示局部变量名称,本例引用了常量池 #16 项,是【args】
      • 00 11 表示局部变量的类型,本例引用了常量池 #17 项,是【[Ljava/lang/String;】
      • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0

0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00

0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00

0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a

0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b

0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00

红色代表方法属性(属性2)

  • 00 12 表示引用了常量池 #18 项,发现是【MethodParameters】属性
    • 00 00 00 05 表示此属性的总长度,本例是 5
    • 01 参数数量
    • 00 10 表示引用了常量池 #16 项,是【args】
    • 00 00 访问修饰符

0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00

0001120 00 00 02 00 14

1.7 附加属性
  • 00 01 表示附加属性数量
  • 00 13 表示引用了常量池 #19 项,即【SourceFile】
  • 00 00 00 02 表示此属性的长度
  • 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】

0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00

0001120 00 00 02 00 14

参考文献(官方文档)

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

2. 字节码指令
2.1 入门

接着上一节,研究一下两组字节码指令,一个是

public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令

2a b7 00 01 b1
  1. 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数

  2. 2.b7 => invokespecial 预备调用构造方法,哪个方法呢?

  3. 00 01 引用常量池中 #1 项,即【 Method java/lang/Object.“”😦)V 】 (调用父类Object的构造方法)

  4. b1 表示返回

另一个是 public static void main(java.lang.String[]); 主方法的字节码指令

b2 00 02 12 03 b6 00 04 b1
  1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?

  2. 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】

  3. 12 => ldc 加载参数,哪个参数呢?

  4. 03 引用常量池中 #3 项,即 【String hello world】

  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?

  6. 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】

  7. b1 表示返回

请参考

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

2.2 javap 工具

自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件

[root@localhost ~]# javap -v HelloWorld.class //加上-v就可以输出详细信息(字节码常量池信息)

Classfile /root/HelloWorld.class 

Last modified Jul 7, 2019; size 597 bytes 

MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc 

Compiled from "HelloWorld.java" 

public class cn.itcast.jvm.t5.HelloWorld 

minor version: 0 

major version: 52 

flags: ACC_PUBLIC, ACC_SUPER 

Constant pool: 

\#1 = Methodref #6.#21 // java/lang/Object."<init>":()V 

\#2 = Fieldref #22.#23 // 

java/lang/System.out:Ljava/io/PrintStream; 

\#3 = String #24 // hello world 

\#4 = Methodref #25.#26 // java/io/PrintStream.println: 

(Ljava/lang/String;)V 

\#5 = Class #27 // cn/itcast/jvm/t5/HelloWorld 

\#6 = Class #28 // java/lang/Object 

\#7 = Utf8 <init> 

\#8 = Utf8 ()V 

\#9 = Utf8 Code 

\#10 = Utf8 LineNumberTable 

\#11 = Utf8 LocalVariableTable 

\#12 = Utf8 this 

\#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;#14 = Utf8 main 

\#15 = Utf8 ([Ljava/lang/String;)V 

\#16 = Utf8 args 

\#17 = Utf8 [Ljava/lang/String; 

\#18 = Utf8 MethodParameters 

\#19 = Utf8 SourceFile 

\#20 = Utf8 HelloWorld.java 

\#21 = NameAndType #7:#8 // "<init>":()V 

\#22 = Class #29 // java/lang/System 

\#23 = NameAndType #30:#31 // out:Ljava/io/PrintStream; 

\#24 = Utf8 hello world 

\#25 = Class #32 // java/io/PrintStream 

\#26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V 

\#27 = Utf8 cn/itcast/jvm/t5/HelloWorld 

\#28 = Utf8 java/lang/Object 

\#29 = Utf8 java/lang/System 

\#30 = Utf8 out 

\#31 = Utf8 Ljava/io/PrintStream; 

\#32 = Utf8 java/io/PrintStream 

\#33 = Utf8 println 

\#34 = Utf8 (Ljava/lang/String;)V 

{ 

public cn.itcast.jvm.t5.HelloWorld(); //构造方法

    descriptor: ()V //参数类型

    flags: ACC_PUBLIC //访问修饰符

    Code: //方法属性

        stack=1, locals=1, args_size=1 //栈深度,局部变量个数,参数个数
			//字节码
            0: aload_0 			//0代表字节码行号

            1: invokespecial #1 // Method java/lang/Object." <init>":()V

            4: return 

		LineNumberTable: //字节码与Java行数对应表

			line 4: 0 			//line 4代表Java行号,0代表字节码行号

		LocalVariableTable: //局部变量表
												//0-5作用范围
			Start Length Slot Name Signature //slot槽位号,Name变量名,Signature局部变量名

			  0      5     0  this  Lcn/itcast/jvm/t5/HelloWorld; 

public static void main(java.lang.String[]); 

    descriptor: ([Ljava/lang/String;)V 

    flags: ACC_PUBLIC, ACC_STATIC 

    Code: 

        stack=2, locals=1, args_size=1 

        0: getstatic #2 // Field 

        java/lang/System.out:Ljava/io/PrintStream; 

        3: ldc #3 // String hello world 

        5: invokevirtual #4 // Method 

        java/io/PrintStream.println:(Ljava/lang/String;)V 

        8: return 

    LineNumberTable: 

        line 6: 0 

        line 7: 8 

    LocalVariableTable: 

        Start Length Slot Name Signature 

          0      9    0   args [Ljava/lang/String; 

    MethodParameters: //方法参数信息

    	Name 	Flags //参数名,权限修饰:默认

    	args 

}
2.3 图解方法执行流程
1)原始 java 代码
package cn.itcast.jvm.t3.bytecode; 
/**
* 演示 字节码指令 和 操作数栈、常量池的关系 
*/
public class Demo3_1 { 
    public static void main(String[] args) { 
        int a = 10; //一般的变量是存贮在字节码中
        int b = Short.MAX_VALUE + 1; //超出范围就放在常量池中
        int c = a + b; 
        System.out.println(c); 
    } 
}
2)编译后的字节码文件
[root@localhost ~]# javap -v Demo3_1.class 

Classfile /root/Demo3_1.class 

Last modified Jul 7, 2019; size 665 bytes 

MD5 checksum a2c29a22421e218d4924d31e6990cfc5 

Compiled from "Demo3_1.java" 

public class cn.itcast.jvm.t3.bytecode.Demo3_1 

minor version: 0 

major version: 52 

flags: ACC_PUBLIC, ACC_SUPER 

Constant pool: 

\#1 = Methodref #7.#26 // java/lang/Object."<init>":()V 

\#2 = Class #27 // java/lang/Short 

\#3 = Integer 32768 

\#4 = Fieldref #28.#29 // 

java/lang/System.out:Ljava/io/PrintStream; 

\#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V 

\#6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1 

\#7 = Class #33 // java/lang/Object 

\#8 = Utf8 <init> 

\#9 = Utf8 ()V 

\#10 = Utf8 Code 

\#11 = Utf8 LineNumberTable 

\#12 = Utf8 LocalVariableTable 

\#13 = Utf8 this 

\#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1; 

\#15 = Utf8 main 

\#16 = Utf8 ([Ljava/lang/String;)V 

\#17 = Utf8 args 

\#18 = Utf8 [Ljava/lang/String; 

\#19 = Utf8 a#20 = Utf8 I 

\#21 = Utf8 b 

\#22 = Utf8 c 

\#23 = Utf8 MethodParameters 

\#24 = Utf8 SourceFile 

\#25 = Utf8 Demo3_1.java 

\#26 = NameAndType #8:#9 // "<init>":()V 

\#27 = Utf8 java/lang/Short 

\#28 = Class #34 // java/lang/System 

\#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream; 

\#30 = Class #37 // java/io/PrintStream 

\#31 = NameAndType #38:#39 // println:(I)V 

\#32 = Utf8 cn/itcast/jvm/t3/bytecode/Demo3_1 

\#33 = Utf8 java/lang/Object 

\#34 = Utf8 java/lang/System 

\#35 = Utf8 out 

\#36 = Utf8 Ljava/io/PrintStream; 

\#37 = Utf8 java/io/PrintStream 

\#38 = Utf8 println 

\#39 = Utf8 (I)V 

{ 

public cn.itcast.jvm.t3.bytecode.Demo3_1(); 

descriptor: ()V 

flags: ACC_PUBLIC 

Code: 

stack=1, locals=1, args_size=1 

0: aload_0 

1: invokespecial #1 // Method java/lang/Object." 

<init>":()V

4: return 

LineNumberTable: 

line 6: 0 

LocalVariableTable: 

Start Length Slot Name Signature 

0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1; 

public static void main(java.lang.String[]); 

descriptor: ([Ljava/lang/String;)V 

flags: ACC_PUBLIC, ACC_STATIC 

Code: 

stack=2, locals=4, args_size=1 

0: bipush 10 

2: istore_1 

3: ldc #3 // int 32768 

5: istore_2 

6: iload_1 

7: iload_2 

8: iadd 

9: istore_3 

10: getstatic #4 // Field 

java/lang/System.out:Ljava/io/PrintStream; 

13: iload_3 

14: invokevirtual #5 // Method 

java/io/PrintStream.println:(I)V 

17: return 

LineNumberTable: 

line 8: 0 

line 9: 3

line 10: 6 

line 11: 10 

line 12: 17 

LocalVariableTable: 

Start Length Slot Name Signature 

0 18 0 args [Ljava/lang/String; 

3 15 1 a I 

6 12 2 b I 

10 8 3 c I 

MethodParameters: 

Name Flags 

args 

}
3)常量池载入运行时常量池

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JuiqdNa6-1650292635776)(D:\文档\学习资料\笔记\jvm.assets\image-20211214172234543.png)]

4)方法字节码载入方法区

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7EvrsRC5-1650292635776)(D:\文档\学习资料\笔记\jvm.assets\image-20211214172314379.png)]

5main 线程开始运行,分配栈帧内存

左边的叫局部变量表,右边的叫操作数栈,字节码中(stack=2,locals=4)对应两个区域的空间大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CJJ9UPop-1650292635777)(D:\文档\学习资料\笔记\jvm.assets\image-20211214172406748.png)]

6)执行引擎开始执行字节码

bipush 10

  • bipush将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有

  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)

  • ldc 将一个 int 压入操作数栈

  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)

  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m1QioJjT-1650292635778)(D:\文档\学习资料\笔记\jvm.assets\image-20211215160226085.png)]

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1(槽位1)

    bipush 10,istore_1就相当于Java代码中的a=10

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LV2qpxYL-1650292635779)(D:\文档\学习资料\笔记\jvm.assets\image-20211215160557147.png)]

ldc #3

  • 从常量池加载 #3 数据到操作数栈

  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算

    好的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8sQ0umEy-1650292635780)(D:\文档\学习资料\笔记\jvm.assets\image-20211215161421690.png)]

istore_2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nnx7STFz-1650292635782)(D:\文档\学习资料\笔记\jvm.assets\image-20211215161700465.png)]
在这里插入图片描述

iload_1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1WfLPuqD-1650292635783)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162218933.png)]

iload_2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MfCmmJJw-1650292635783)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162242971.png)]

iadd

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S9SdYzSU-1650292635784)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162306705.png)]

istore_3

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7uRnwQGt-1650292635785)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162422892.png)]

getstatic #4

在常量池中找出#4的成员变量的引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NPtfKVgY-1650292635786)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162624355.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RopRMKT3-1650292635787)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162740434.png)]

iload_3

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfAFSEbi-1650292635788)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162827536.png)]

invokevirtual #5

  • 找到常量池 #5 项

  • 定位到方法区 java/io/PrintStream.println:(I)V 方法

  • 生成新的栈帧(分配 locals、stack等)

  • 传递参数,执行新栈帧中的字节码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NdMeHgxq-1650292635789)(D:\文档\学习资料\笔记\jvm.assets\image-20211215162950065.png)]

  • 执行完毕,弹出栈帧

  • 清除 main 操作数栈内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z7cNaUqy-1650292635789)(D:\文档\学习资料\笔记\jvm.assets\image-20211215163009865.png)]

return

  • 完成 main 方法调用,弹出 main 栈帧

  • 程序结束

2.4 练习 - 分析 i++

目的:从字节码角度分析 a++ 相关题目

源码:

package cn.itcast.jvm.t3.bytecode; 
/**
* 从字节码角度分析 a++ 相关题目 
*/ 
public class Demo3_2 { 
    public static void main(String[] args) { 
        int a = 10; 
        int b = a++ + ++a + a--; 
        System.out.println(a); 
        System.out.println(b); 
    } 
}

字节码:

public static void main(java.lang.String[]); 

descriptor: ([Ljava/lang/String;)V 

flags: (0x0009) ACC_PUBLIC, ACC_STATIC 

Code: 

stack=2, locals=3, args_size=1 

0: bipush 10 

2: istore_1 

3: iload_1 

4: iinc 1, 1 

7: iinc 1, 1 

10: iload_1 

11: iadd 

12: iload_1 

13: iinc 1, -1 

16: iadd 

17: istore_218: getstatic #2 // Field 

java/lang/System.out:Ljava/io/PrintStream; 

21: iload_1 

22: invokevirtual #3 // Method 

java/io/PrintStream.println:(I)V 

25: getstatic #2 // Field 

java/lang/System.out:Ljava/io/PrintStream; 

28: iload_2 

29: invokevirtual #3 // Method 

java/io/PrintStream.println:(I)V 

32: return 

LineNumberTable: 

line 8: 0 

line 9: 3 

line 10: 18 

line 11: 25 

line 12: 32 

LocalVariableTable: 

Start Length Slot Name Signature 

0 33 0 args [Ljava/lang/String; 

3 30 1 a I 

18 15 2 b I

分析:

  • 注意 iinc 指令是直接在局部变量 slot 上进行运算

  • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UYpay4zq-1650292635790)(D:\文档\学习资料\笔记\jvm.assets\image-20211215163742376.png)]

iinc 第几个槽位,自增几

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dXEmlJrO-1650292635791)(D:\文档\学习资料\笔记\jvm.assets\image-20211215163827820.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HaR4QVIw-1650292635792)(D:\文档\学习资料\笔记\jvm.assets\image-20211215164031270.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jPrbEvvX-1650292635793)(D:\文档\学习资料\笔记\jvm.assets\image-20211215164059572.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jGHFxCn-1650292635794)(D:\文档\学习资料\笔记\jvm.assets\image-20211215164158373.png)]

2.5 条件判断指令

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nANk9zV1-1650292635795)(D:\文档\学习资料\笔记\jvm.assets\image-20211215164535193.png)]

几点说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节

  • goto 用来进行跳转到指定行号的字节码

  • lcmp 比较long类型值 fcmpl 比较float类型值(当遇到NaN时,返回-1)

  • fcmpg 比较float类型值(当遇到NaN时,返回1)

源码:

public class Demo3_3 { 

	public static void main(String[] args) { 
        int a = 0; 
        if(a == 0) { 
             a = 10; 
        } else { 
            a = 20; 
        } 
    } 
}

字节码:

0: iconst_0 //获得常量0,比较小的数(-1到5)是用iconst来表示的

1: istore_1 

2: iload_1 

3: ifne 		12 //不等于零跳12行

6: bipush 		10 

8: istore_1 

9: goto 		15 

12: bipush 		20 

14: istore_1 

15: return 
2.6 循环控制指令

其实循环控制还是前面介绍的那些指令,例如 while 循环:

public class Demo3_4 { 
    public static void main(String[] args) { 
        int a = 0; 
        while (a < 10) { 
        	a++; 
        } 
    } 
}

字节码是:

0: iconst_0 

1: istore_1 

2: iload_1 

3: bipush 10 

5: if_icmpge 14 

8: iinc 1, 1 

11: goto 2 

14: return 

再比如 do while 循环:

public class Demo3_5 { 
    public static void main(String[] args) { 
        int a = 0; 
        do {
        	a++; 
        } while (a < 10); 
    } 
}

字节码是:

  0: iconst_0

 1: istore_1

 2: iinc 1, 1

 5: iload_1

 6: bipush 10

 8: if_icmplt 2

11: return

最后再看看 for 循环:

public class Demo3_6 { 
    public static void main(String[] args) { 
        for (int i = 0; i < 10; i++) { 
            
        } 
    } 
}

字节码是:

0: iconst_0 

1: istore_1 

2: iload_1 

3: bipush 10 

5: if_icmpge 14 

8: iinc 1, 1 

11: goto 2 

14: return 

注意

比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归😊

2.7 练习 - 判断结果

请从字节码角度分析,下列代码运行的结果:

java和C语言的编译方式还不一样,Java是将变量复制到操作数栈里进行操作,但是C语言始终都是一个内存所以结果会不一样,所以这个题在C语言里答案为10

详细见:C语言和java的自增运算区别_m0_37215251的博客-CSDN博客

public class Demo3_6_1 { 
    public static void main(String[] args) { 
        int i = 0; 
        int x = 0; 
        while (i < 10) { 
            x = x++; 
            i++; 
        }
        System.out.println(x); // 结果是 0 
    } 
}
2.8 构造方法

1) ()V

public class Demo3_8_1 { 
    static int i = 10; 
    static { 
        i = 20; 
    }
    static { 
    	i = 30; 
    } 
}

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方

法 ()V :

0: bipush 10 
2: putstatic #2 // Field i:I 

5: bipush 20 
7: putstatic #2 // Field i:I 

10: bipush 30 
12: putstatic #2 // Field i:I 

15: return 

()V 方法会在类加载的初始化阶段被调用

public class Demo3_8_2 { 
    private String a = "s1"; 
    { 
    	b = 20; 
    }
    private int b = 10; 
    { 
   		 a = "s2"; 
    }
    public Demo3_8_2(String a, int b) { 
        this.a = a; 
        this.b = b; 
    }
    public static void main(String[] args) { 
        Demo3_8_2 d = new Demo3_8_2("s3", 30); 
        System.out.println(d.a); 
        System.out.println(d.b);                                                             } 
}

编译器会按从上至下的顺序,收集所有 {} 代码块(初始化代码块)和成员变量赋值的代码,形成新的构造方法,但原始构

造方法内的代码总是在最后

//执行顺序:静态代码块->非静态代码块->类的构造方法

public cn.itcast.jvm.t3.bytecode.Demo3_8_2(java.lang.String, int); 
descriptor: (Ljava/lang/String;I)V 
flags: ACC_PUBLIC 
Code: 
stack=2, locals=3, args_size=3 
0: aload_0 		//把this加载到操作数栈
1: invokespecial #1 // super.<init>()V 
4: aload_0 
5: ldc #2 // <- "s1" 
7: putfield #3 // -> this.a 
10: aload_0 
11: bipush 20 // <- 20 
13: putfield #4 // -> this.b 
16: aload_0 
17: bipush 10 // <- 10 
19: putfield #4 // -> this.b 
22: aload_0 
23: ldc #5 // <- "s2" 
25: putfield #3 // -> this.a 
28: aload_0 // ------------------------------ 
29: aload_1 // <- slot 1(a) "s3" | 
30: putfield #3 // -> this.a | 
33: aload_0 | 
34: iload_2 // <- slot 2(b) 30 | 
35: putfield #4 // -> this.b -------------------- 
38: return 
LineNumberTable: ... 
LocalVariableTable: 
Start Length Slot Name Signature 
0 39 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_8_2; 
0 39 1 a Ljava/lang/String; 
0 39 2 b I 
MethodParameters: ...
2.9 方法调用

看一下几种不同的方法调用对应的字节码指令

public class Demo3_9 { 
    public Demo3_9() { } 

    private void test1() { } 

    private final void test2() { } 

    public void test3() { } 

    public static void test4() { } 

    public static void main(String[] args) { 
        Demo3_9 d = new Demo3_9(); 
        d.test1(); 
        d.test2(); 
        d.test3(); 
        d.test4(); 
        Demo3_9.test4(); 
    } 

}

字节码:

0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9 申请内存,再将对象引用放入操作数栈
3: dup 				//再复制一份引用
4: invokespecial #3 // Method "<init>":()V 构造方法	使用那个复制的引用执行构造方法,执行后删除引用
7: astore_1 		//将对象地址出栈,返回局部变量表,存入d

8: aload_1 			//进栈
9: invokespecial #4 //调用 Method test1:()V 私有

12: aload_1 
13: invokespecial #5 // Method test2:()V 最终私有

16: aload_1 
17: invokevirtual #6 // Method test3:()V 公共类:有可能被方法重写,不确定是什么类型,动态绑定

20: aload_1 
21: pop 	//由于静态方法不需要引用所以将引用弹出

22: invokestatic #7 // Method test4:()V 静态类
25: invokestatic #7 // Method test4:()V 
28: return
  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈

  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配

    合 invokespecial 调用该对象的构造方法 “”😦)V (会消耗掉栈顶一个引用),另一个要

    配合 astore_1 赋值给局部变量

  • 最终方法(fifinal),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静

    态绑定

  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态

  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】

  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用

    invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了😂

  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

2.10 多态的原理
package cn.itcast.jvm.t3.bytecode; 
import java.io.IOException; 
/**
\* 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩 
\* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers 
*/ 
public class Demo3_10 { 
    public static void test(Animal animal) { 
        animal.eat(); 
        System.out.println(animal.toString()); 
    }
    public static void main(String[] args) throws IOException { 
            test(new Cat()); 
            test(new Dog()); 
            System.in.read(); 
        } 
    }

    abstract class Animal { 
        public abstract void eat(); 
        @Override 
        public String toString() { 
        	return "我是" + this.getClass().getSimpleName(); 
        } 
    }

    class Dog extends Animal { 
        @Override 
        public void eat() { 
        	System.out.println("啃骨头"); 
        } 
    }

    class Cat extends Animal { 
        
        @Override 
        public void eat() { 
        	System.out.println("吃鱼"); 
    } 
}

1)运行代码

停在 System.in.read() 方法上,这时运行 jps命令获取进程 id

2)运行 HSDB 工具

进入 JDK 安装目录,执行

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面 File–>attach… 进程 id

3)查找某个对象

打开 Tools -> Find Object By Query

输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行

(与sql语句很像,最后那个d是把它重命名为d,查询的内容为对象的地址)

4)查看对象内存结构

点击超链接(上一步查到的对象地址)可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord(包含哈希码,加锁的锁标记),后 8 字节就是对象的 Class 指针但目前看不到它的实际地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dJ5uvjjK-1650292635796)(D:\文档\学习资料\笔记\jvm.assets\image-20211215195454814.png)]

5)查看对象Class 的内存地址

可以通过 Windows -> Console 进入命令行模式,执行

mem 0x00000001299b4978 2

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

结果中第二行 0x000000001b7d4028 即为 Class 的内存地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RCljhCv-1650292635797)(D:\文档\学习资料\笔记\jvm.assets\image-20211215195532380.png)]

6)查看类的 vtable(虚方法表:刚才查询到的类存贮多态信息的地方)

  • 方法1:Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BiR9WLd5-1650292635798)(D:\文档\学习资料\笔记\jvm.assets\image-20211215195601074.png)]

  • 方法2:或者 Tools -> Class Browser 输入 Dog 查找,可以得到相同的结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9GUjIaQe-1650292635798)(D:\文档\学习资料\笔记\jvm.assets\image-20211215195617588.png)]

无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态

相关的,fifinal,static 不会列入)

那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计

算得到:

0x000000001b7d4028 

1b8 + 

\--------------------- 

0x000000001b7d41e0

通过 Windows -> Console 进入命令行模式,执行

mem 0x000000001b7d41e0 6 

0x000000001b7d41e0: 0x000000001b3d1b10 

0x000000001b7d41e8: 0x000000001b3d15e8 

0x000000001b7d41f0: 0x000000001b7d35e8 

0x000000001b7d41f8: 0x000000001b3d1540 

0x000000001b7d4200: 0x000000001b3d1678 

0x000000001b7d4208: 0x000000001b7d3fa8 

就得到了 6 个虚方法的入口地址

7)验证方法地址

通过 Tools -> Class Browser 查看每个类的方法定义(搜索Dog,即可看到方法和父类Animal,同理点进Animal,以及Animal的父类Object查看方法),比较可知

Dog - public void eat() @0x000000001b7d3fa8 
Animal - public java.lang.String toString() @0x000000001b7d35e8; 
Object - protected void finalize() @0x000000001b3d1b10; 
Object - public boolean equals(java.lang.Object) @0x000000001b3d15e8; 
Object - public native int hashCode() @0x000000001b3d1540; 
Object - protected native java.lang.Object clone() @0x000000001b3d1678;

对号入座,发现

  • eat() 方法是 Dog 类自己的

  • toString() 方法是继承 String 类的

  • fifinalize() ,equals(),hashCode(),clone() 都是继承 Object 类的

8)小结

当执行 invokevirtual 指令时,

  1. 先通过栈帧中的对象引用找到对象

  2. 分析对象头,找到对象的实际 Class

  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了

  4. 查表得到方法的具体地址

  5. 执行方法的字节码

2.11 异常处理

try-catch

public class Demo3_11_1 { 
    public static void main(String[] args) { 
        int i = 0; 
        try {
       		 i = 10; 
        } catch (Exception e) { 
       		 i = 20; 
        } 
    } 
}

注意

为了抓住重点,下面的字节码省略了不重要的部分

public static void main(java.lang.String[]); 
descriptor: ([Ljava/lang/String;)V 
flags: ACC_PUBLIC, ACC_STATIC 
Code: 
stack=1, locals=3, args_size=1 
0: iconst_0 
1: istore_1 //i = 0
              
2: bipush 10 
4: istore_1 //i = 10
              
5: goto 12 
   //catch块
8: astore_2 //查看局部变量表可知,这是异常对象e
9: bipush 20 
11: istore_1 //i=20
12: return 
              
Exception table: //异常表
from to target type //从2-4行(含头不含尾)如果发生异常就和type匹配,正确就进入第8行
  2   5   8     Class java/lang/Exception 
LineNumberTable: ... 
              
LocalVariableTable: //局部变量表
Start Length Slot Name Signature 
  9      3    2     e   Ljava/lang/Exception; 
  0     13    0   args  [Ljava/lang/String; 
  2     11    1     i    I 
StackMapTable: ... 
MethodParameters: ... 
}
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围

    内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号

  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

多个 single-catch 块的情况

public class Demo3_11_2 { 
    public static void main(String[] args) { 
        int i = 0; 
        try {
            i = 10; 
        } catch (ArithmeticException e) { 
            i = 30; 
        } catch (NullPointerException e) { 
            i = 40; 
        } catch (Exception e) { 
            i = 50; 
        } 
    } 
}
public static void main(java.lang.String[]); 
descriptor: ([Ljava/lang/String;)V 
flags: ACC_PUBLIC, ACC_STATIC 
Code: 
stack=1, locals=3, args_size=1 
0: iconst_0 
1: istore_1 
2: bipush 10 
4: istore_1 
5: goto 26 
8: astore_2 
9: bipush 30 
11: istore_1 
12: goto 26 
15: astore_2 
16: bipush 40 
18: istore_1 
19: goto 26 
22: astore_2 
23: bipush 50 
25: istore_1 
26: return 
Exception table: 
from to target type 
  2   5   8    Class java/lang/ArithmeticException 
  2   5   15   Class java/lang/NullPointerException 
  2   5   22   Class java/lang/Exception 
LineNumberTable: ... 
LocalVariableTable: 
Start Length Slot Name Signature 
  9     3      2    e   Ljava/lang/ArithmeticException; 
 16     3      2    e   Ljava/lang/NullPointerException; 
 23     3      2    e   Ljava/lang/Exception; 
  0     27     0   args [Ljava/lang/String; 
  2     25     1    i    I 
StackMapTable: ... 
MethodParameters: ... 
  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

multi-catch 的情况

public class Demo3_11_3 {
    public static void main(String[] args) { 
        try {
            Method test = Demo3_11_3.class.getMethod("test"); 
            test.invoke(null); 
        } catch (NoSuchMethodException | IllegalAccessException | 
                 InvocationTargetException e) { 
            e.printStackTrace(); 
        } 
    }

    public static void test() { 
        System.out.println("ok"); 
    } 
}
public static void main(java.lang.String[]); 
descriptor: ([Ljava/lang/String;)V 
flags: ACC_PUBLIC, ACC_STATIC 
Code: 
stack=3, locals=2, args_size=1 
0: ldc #2 
2: ldc #3 
4: iconst_0 
5: anewarray #4 
8: invokevirtual #5 
11: astore_1 
12: aload_1 
13: aconst_null 
14: iconst_0 
15: anewarray #6 
18: invokevirtual #7 
21: pop 
22: goto 30 
25: astore_1 
26: aload_1 
27: invokevirtual #11 // e.printStackTrace:()V 
30: return 
Exception table: 
from to target type 
0    22  25    Class java/lang/NoSuchMethodException 
0    22  25    Class java/lang/IllegalAccessException 
0    22  25    Class java/lang/reflect/InvocationTargetException 
LineNumberTable: ... 
LocalVariableTable: 
Start Length Slot Name Signature 
12    10      1   test Ljava/lang/reflect/Method; 
26    4       1   e    Ljava/lang/ReflectiveOperationException; 
0     31      0   args [Ljava/lang/String; 
StackMapTable: ... 
MethodParameters: ...

fifinally

public class Demo3_11_4 { 

    public static void main(String[] args) { 

        int i = 0; 

        try {

        	i = 10; 

        } catch (Exception e) { 

        	i = 20; 

        } finally { 

        	i = 30; 

        } 

    } 

}
public static void main(java.lang.String[]); 
descriptor: ([Ljava/lang/String;)V 
flags: ACC_PUBLIC, ACC_STATIC 
Code: 
stack=1, locals=4, args_size=1 
0: iconst_0 
1: istore_1 // 0 -> i 
2: bipush 10 // try -------------------------------------- 
4: istore_1 // 10 -> i 									| 
5: bipush 30 // finally 								| 
7: istore_1 // 30 -> i 									| 
8: goto 27 // return ----------------------------------- 
11: astore_2 // catch Exceptin -> e ---------------------- 
12: bipush 20 // 										| 
14: istore_1 // 20 -> i 								| 
15: bipush 30 // finally 								| 
17: istore_1 // 30 -> i									| 
18: goto 27 // return ----------------------------------- 
21: astore_3 // catch any -> slot 3 ---------------------- 
22: bipush 30 // finally 								| 
24: istore_1 // 30 -> i 								| 
25: aload_3 // <- slot 3 	抛出异常							| 
26: athrow // throw ------------------------------------ 
27: return 
Exception table: 
from to target type 
2    5   11    Class java/lang/Exception 
2    5   21    any // 剩余的异常类型,比如 Error 
11  15   21    any // 剩余的异常类型,比如 Error 
LineNumberTable: ... 
LocalVariableTable: 
Start Length Slot Name Signature 
12     3      2    e   Ljava/lang/Exception; 
0     28      0   args [Ljava/lang/String; 
2     26      1    i    I 
StackMapTable: ... 
MethodParameters: ...

可以看到 fifinally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流

2.12 练习 - fifinally 面试题

fifinally 出现了 return

先问问自己,下面的题目输出什么?//20

public class Demo3_12_2 { 

    public static void main(String[] args) { 

        int result = test(); 

        System.out.println(result); 

    }

    public static int test() { 

        try {

            return 10; 

        } finally { 

            return 20; 

        } 

    } 

} 
public static int test(); 

descriptor: ()I 

flags: ACC_PUBLIC, ACC_STATIC 

Code: 

stack=1, locals=2, args_size=0 

0: bipush 10 // <- 10 放入栈顶 

2: istore_0 // 10 -> slot 0 (从栈顶移除了) 留下疑问??

3: bipush 20 // <- 20 放入栈顶 

5: ireturn // 返回栈顶 int(20) 

6: astore_1 // catch any -> slot 1 

7: bipush 20 // <- 20 放入栈顶 

9: ireturn // 返回栈顶 int(20) 

Exception table: 

from to target type 

0 3 6 any 

LineNumberTable: ... 

StackMapTable: ... 
  • 由于 fifinally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 fifinally 的为准至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子

  • 跟上例中的 fifinally 相比,发现没有 athrow 了,这告诉我们:如果在 fifinally 中出现了 return,会吞掉异常😱😱😱,可以试一下下面的代码

public class Demo3_12_1 { 

    public static void main(String[] args) { 

        int result = test(); 

        System.out.println(result); 

    }

    public static int test() { 

        try {

       		int i = 1/0; 

        	return 10; 

        } finally { 

        	return 20; 

        } 

    } 

}

fifinally 对返回值影响

同样问问自己,下面的题目输出什么?

public class Demo3_12_2 { 

    public static void main(String[] args) { 

        int result = test(); 

        System.out.println(result); 

    }

    public static int test() { 

        int i = 10; 

        try {

        	return i; 

        } finally { 

        	i = 20; 

        } 

    } 

} 
public static int test(); 

descriptor: ()I 

flags: ACC_PUBLIC, ACC_STATIC 

Code: 

stack=1, locals=3, args_size=0 

0: bipush 10 // <- 10 放入栈顶 

2: istore_0 // 10 -> i 

3: iload_0 // <- i(10) 

4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 

5: bipush 20 // <- 20 放入栈顶 

7: istore_0 // 20 -> i 

8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值 

9: ireturn // 返回栈顶的 int(10) 

10: astore_2 //catch any and athrow

11: bipush 20 

13: istore_0 

14: aload_2 

15: athrow 

Exception table: 

from to target type 

3    5    10    any 

LineNumberTable: ... 

LocalVariableTable: 

Start Length Slot Name Signature 

3      13     0    i    I

StackMapTable: ... 
2.13 synchronized
public class Demo3_13 { 
    public static void main(String[] args) { 
        Object lock = new Object(); 
        synchronized (lock) { //synchronized:在操作执行前加锁,执行后解锁,确保并发安全
        	System.out.println("ok"); 
        } 
    } 
} 
public static void main(java.lang.String[]); 
descriptor: ([Ljava/lang/String;)V 
flags: ACC_PUBLIC, ACC_STATIC 
Code: 
stack=2, locals=4, args_size=1 
0: new #2 // new Object 
3: dup 
4: invokespecial #1 // invokespecial <init>:()V 
7: astore_1 // lock引用 -> lock 
              
8: aload_1 // <- lock (synchronized开始) 
9: dup 			//复制两份是为了一份给加锁用一份给解锁用
10: astore_2 // lock引用 -> slot 2 
11: monitorenter // monitorenter(lock引用) 加锁
              
12: getstatic #3 // <- System.out 
15: ldc #4 // <- "ok" 
17: invokevirtual #5 // invokevirtual println: (Ljava/lang/String;)V 
              
20: aload_2 // <- slot 2(lock引用) 
21: monitorexit // monitorexit(lock引用) 解锁
22: goto 30 
25: astore_3 // any -> slot 3 
26: aload_2 // <- slot 2(lock引用) 
27: monitorexit // monitorexit(lock引用) 
28: aload_3 
29: athrow 
30: return 
Exception table: 
from to target type 
12 22 25 any 
25 28 25 any 
LineNumberTable: ... 
LocalVariableTable: 
Start Length Slot Name Signature 
0 31 0 args [Ljava/lang/String; 
8 23 1 lock Ljava/lang/Object; 
StackMapTable: ... 
MethodParameters: ...
3. 编译期处理

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成

和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃

嘛)

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,

编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并

不是编译器还会转换出中间的 java 源码,切记。

3.1 默认构造器
public class Candy1 { 
}

编译成class后的代码:

public class Candy1 { 

// 这个无参构造是编译器帮助我们加上的 
public Candy1() { 
    super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V 
    } 

}
3.2 自动拆装箱

这个特性是 JDK 5 开始加入的, 代码片段1 :

public class Candy2 { 
    public static void main(String[] args) {
    	Integer x = 1; 
    	int y = x; 
    } 
}

这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :

public class Candy2 { 
    public static void main(String[] args) { 
        Integer x = Integer.valueOf(1); 
        int y = x.intValue(); 
    } 
}

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是

包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编

译阶段被转换为 代码片段2

3.3 泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息

在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

public class Candy3 { 
    public static void main(String[] args) { 
        List<Integer> list = new ArrayList<>(); 
        list.add(10); // 实际调用的是 List.add(Object e) 
        Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); 
    } 
}

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

// 需要将 Object 转为 
Integer Integer x = (Integer)list.get(0);

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:

// 需要将 Object 转为 Integer, 并执行拆箱操作 
int x = ((Integer)list.get(0)).intValue();

还好这些麻烦事都不用自己做。

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

public cn.itcast.jvm.t3.candy.Candy3(); 
descriptor: ()V 
flags: ACC_PUBLIC 
Code: 
stack=1, locals=1, args_size=1 
0: aload_0 
1: invokespecial #1 // Method java/lang/Object." <init>":()V
4: return 
LineNumberTable: 
line 6: 0 
LocalVariableTable: 
Start Length Slot Name Signature 
0     5      0    this Lcn/itcast/jvm/t3/candy/Candy3; 
public static void main(java.lang.String[]); 
descriptor: ([Ljava/lang/String;)V 
flags: ACC_PUBLIC, ACC_STATIC 
Code: 
stack=2, locals=3, args_size=1 
0: new #2 // class java/util/ArrayList 
3: dup 
4: invokespecial #3 // Method java/util/ArrayList." <init>":()V
7: astore_1 
              
8: aload_1 
9: bipush 10 
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 将10装箱
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 
              				//5执行添加函数,2是list位置,z是返回一个布尔值
19: pop 
20: aload_1 //返回赋值给list
21: iconst_0 	//get的下标
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 
              				//执行get函数
27: checkcast #7 // class java/lang/Integer 将Object强转为Integer
30: astore_2 		//存入x
31: return 
LineNumberTable: 
line 8: 0 
line 9: 8 
line 10: 20 
line 11: 31 
LocalVariableTable: 
Start Length Slot Name Signature
 0       32   0   args  [Ljava/lang/String; 
 8       24   1   list  Ljava/util/List; 
LocalVariableTypeTable: //局部变量类型表
 Start Length Slot Name Signature //保留了list泛型是Integer的信息,但无法通过反射得到
   8     24    1   list Ljava/util/List<Ljava/lang/Integer;>;

使用反射,仍然能够获得这些信息://在返回值中带上泛型

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
Method test = Candy3.class.getMethod("test", List.class, Map.class); //获得方法
Type[] types = test.getGenericParameterTypes(); //得到参数类型
for (Type type : types) { //遍历
    if (type instanceof ParameterizedType) { //是否为泛型
        ParameterizedType parameterizedType = (ParameterizedType) type; 					 	System.out.println("原始类型 - " + parameterizedType.getRawType()); 
        Type[] arguments = parameterizedType.getActualTypeArguments(); 
        
        for (int i = 0; i < arguments.length; i++) { 
            System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]); 
        } 
    } 
}

输出

原始类型 - interface java.util.List 
泛型参数[0] - class java.lang.String 
原始类型 - interface java.util.Map 
泛型参数[0] - class java.lang.Integer 
泛型参数[1] - class java.lang.Object
3.4 可变参数

可变参数也是 JDK 5 开始加入的新特性:

例如:

public class Candy4 { 
    public static void foo(String... args) { 
        String[] array = args; // 直接赋值 
        System.out.println(array); 
    }
    public static void main(String[] args) { 
        foo("hello", "world"); 
    }
}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。

同样 java 编译器会在编译期间将上述代码变换为:

public class Candy4 { 
    public static void foo(String[] args) { 
        String[] array = args; // 直接赋值 
        System.out.println(array); 
    }
    public static void main(String[] args) { 
        foo(new String[]{"hello", "world"}); 
    } 
}

注意

如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递

null 进去

3.5 foreach 循环

仍是 JDK 5 开始引入的语法糖,数组的循环:

public class Candy5_1 { 
    public static void main(String[] args) { 
        int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 
        for (int e : array) { System.out.println(e); 
                            }
    }
}

会被编译器转换为:

public class Candy5_1 { 
    public Candy5_1() { 
    }
    public static void main(String[] args) { 
        int[] array = new int[]{1, 2, 3, 4, 5}; 
        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e); 
        } 
    } 
}

而集合的循环:

public class Candy5_2 { 
    public static void main(String[] args) { 
        List<Integer> list = Arrays.asList(1,2,3,4,5); 
        for (Integer i : list) { 
            System.out.println(i);
        } 
    }
}

实际被编译器转换为对迭代器的调用:

public class Candy5_2 { 
    public Candy5_2() { 
    }
    public static void main(String[] args) { 
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); 
        Iterator iter = list.iterator(); //迭代器
        while(iter.hasNext()) {
            Integer e = (Integer)iter.next(); //泛型被擦除,需要强转回来
            System.out.println(e); 
        }
    }
}

注意

foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中

Iterable 用来获取集合的迭代器( Iterator )

3.6 switch 字符串

从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) { 
            case "hello": { 
                System.out.println("h"); 
                break; 
            }case "world": { 
                System.out.println("w"); 
                break; 
            }
        }
    }
}

注意

switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清

会被编译器转换为:

public class Candy6_1 { 

    public Candy6_1() { 

    }

    public static void choose(String str) { 
        byte x = -1; 
        switch(str.hashCode()) { 
        	case 99162322: // hello 的 hashCode 
        		if (str.equals("hello")) {
        			x = 0; 
    			}
   				break; 
            case 113318802: // world 的 hashCode 
                if (str.equals("world")) { 
                    x = 1; 
                } 
    }

    switch(x) { 
        case 0:
            System.out.println("h");break; 

        case 1: 
            System.out.println("w"); 
        } 
    } 
}

可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应

byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可

能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C. 这两个字符串的hashCode值都是

2123 ,如果有如下代码:

public class Candy6_2 { 
    public static void choose(String str) {
        switch (str) { 
            case "BM": { 
                System.out.println("h"); 
                break; }
            case "C.": { 
                System.out.println("w"); 
                break; 
            }
        }
    }
}

会被编译器转换为:

public class Candy6_2 { 
    public Candy6_2() { }
    public static void choose(String str) { 
        byte x = -1; 
        switch(str.hashCode()) { 
            case 2123: // hashCode 值可能相同,需要进一步用 equals 比较 
                if (str.equals("C.")) { 
                    x = 1; 
                } else if (str.equals("BM")) { 
                    x = 0; 
                } 
            default:
                switch(x) { 
                    case 0: System.out.println("h"); 
                        break; 
                    case 1: System.out.println("w"); 
                }
        }
    }
}
3.7 switch 枚举

switch 枚举的例子,原始代码:

enum Sex { MALE, FEMALE }
public class Candy7 {
    public static void foo(Sex sex) { 
        switch (sex) { 
            case MALE: 
                System.out.println("男"); break; 
            case FEMALE: 
                System.out.println("女"); break; 
        }
    }
}

转换后代码:

public class Candy7 { 
/**
\* 定义一个合成类(仅 jvm 使用,对我们不可见) 
\* 用来映射枚举的 ordinal 与数组元素的关系 
\* 枚举的 ordinal 表示枚举对象的序号,从 0 开始 
\* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 
*/ 
static class $MAP { 
    // 数组大小即为枚举元素个数,里面存储case用来对比的数字 
    static int[] map = new int[2]; 
        static { 
            map[Sex.MALE.ordinal()] = 1; //下表为0,ordinal枚举编号
            map[Sex.FEMALE.ordinal()] = 2; //下表为1
        } 
    }
    public static void foo(Sex sex) { 
        int x = $MAP.map[sex.ordinal()]; //找到sex对应的枚举编号
        switch (x) { 
            case 1: 
                System.out.println("男"); break; 
            case 2:
                System.out.println("女");break; 
        } 
    } 
}
3.8 枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

enum Sex { MALE, FEMALE }

转换后代码:

public final class Sex extends Enum<Sex> { 
    public static final Sex MALE; 
    public static final Sex FEMALE; 
    private static final Sex[] $VALUES; static { 
        MALE = new Sex("MALE", 0); 
        FEMALE = new Sex("FEMALE", 1); 
        $VALUES = new Sex[]{MALE, FEMALE}; }
    
    /*** Sole constructor. Programmers cannot invoke this constructor. 
    * It is for use by code emitted by the compiler in response to 
    * enum type declarations. 
    ** @param name - The name of this enum constant, which is the identifier 
    * used to declare it.
    * @param ordinal - The ordinal of this enumeration constant (its position 
    * in the enum declaration, where the initial constant is assigned */
    private Sex(String name, int ordinal) { 
        super(name, ordinal); 
    }
    public static Sex[] values() { 
        return $VALUES.clone(); 
    }
    public static Sex valueOf(String name) { 
        return Enum.valueOf(Sex.class, name); 
    }
}
3.9 try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources`:

try(资源变量 = 创建资源对象){ 
} catch( ) { 
}

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、

Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-with-

resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

public class Candy9 { 
    public static void main(String[] args) {
        try(InputStream is = new FileInputStream("d:\\1.txt")) { 
            System.out.println(is); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        }
    }
}

会被转换为:

public class Candy9 { 
    public Candy9() { 
    }
    public static void main(String[] args) { 
        try {
            InputStream is = new FileInputStream("d:\\1.txt"); 
            Throwable t = null; 
        try {
        	System.out.println(is); 
        } catch (Throwable e1) { 
        // t 是我们代码出现的异常 
            t = e1; 
            throw e1; 
        } finally { 
            // 判断了资源不为空 
            if (is != null) { 
                // 如果我们代码有异常 
                if (t != null) { 
                    try {
                    	is.close(); 
                    } catch (Throwable e2) { 
                        // 如果 close 出现异常,作为被压制异常添加 
                        t.addSuppressed(e2); 
                    } 
                    } else { 
                        // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e 
                        is.close(); 
                    } 
                } 
            }
        } catch (IOException e) { 
        	e.printStackTrace(); 
        } 
    } 
}

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信

息的丢失(想想 try-with-resources 生成的 fifianlly 中如果抛出了异常):

public class Test6 { 
    public static void main(String[] args) { 
        try (MyResource resource = new MyResource()) { 
            int i = 1/0; 
        } catch (Exception e) {
            e.printStackTrace(); 
        }
    }
}

class MyResource implements AutoCloseable { 
    public void close() throws Exception { 
        throw new Exception("close 异常");
    }
}

输出:

java.lang.ArithmeticException: / by zero at test.Test6.main(Test6.java:7) Suppressed: java.lang.Exception: close 异常 at test.MyResource.close(Test6.java:18) at test.Test6.main(Test6.java:6)

如以上代码所示,两个异常信息都不会丢。

3.10 方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致

  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)

class A { 
    public Number m() { 
        return 1;
    }
}

class B extends A { 
    @Override // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类 
    public Integer m() {
        return 2; 
    }
}

对于子类,java 编译器会做如下处理:

class B extends A { 
    public Integer m() { 
        return 2; 
    }// 此方法才是真正重写了父类 public Number m() 方法 
    public synthetic bridge Number m() { //合成方法,仅jvm可用
        // 调用 public Integer m() 
        return m(); 
    }
}

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以

用下面反射代码来验证:

for (Method m : B.class.getDeclaredMethods()) {
    System.out.println(m); 
}

会输出:

public java.lang.Integer test.candy.B.m() 
public java.lang.Number test.candy.B.m()

3.11 匿名内部类

源代码:

public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() { 
            @Override 
            public void run() { 
                System.out.println("ok");
            }
        };
    }
}

转换后代码:

// 额外生成的类 
final class Candy11$1 implements Runnable { 
    Candy11$1() { }
    
    public void run() { 
        System.out.println("ok");
    } 
}
public class Candy11 { 
    public static void main(String[] args) { 
        Runnable runnable = new Candy11$1(); 
    } 
}

引用局部变量的匿名内部类,源代码:

public class Candy11 { 
    public static void test(final int x) { 
        Runnable runnable = new Runnable() { 
            @Override 
            public void run() { 
                System.out.println("ok:" + x); 
            }
        };
    } 
}

转换后代码:

// 额外生成的类 
final class Candy11$1 implements Runnable { 
    int val$x; Candy11$1(int x) { 
        this.val$x = x; 
    }
    public void run() { 
        System.out.println("ok:" + this.val$x); 
    } 
}
public class Candy11 { 
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(x); 
    } 
}

注意

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 fifinal 的:因为在创建

Candy11$1 对象时,将 x 的值赋值给了 Candy11 1 对 象 的 v a l 1 对象的 val 1valx 属性,所以 x 不应该再发生变

化了,如果变化,那么 val$x 属性没有机会再跟着一起变化

4. 类加载阶段
4.1 加载
  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 fifield 有:

    • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
    • _super 即父类
    • _fifields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类

  • 加载和链接可能是交替运行的

注意

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror

    是存储在堆中

  • 可以通过前面介绍的 HSDB 工具查看

4.2 链接

1,验证

验证类是否符合 JVM规范,安全性检查

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld 
Error: A JNI error has occurred, please check your installation and try again 
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 
3405691578 in class file cn/itcast/jvm/t5/HelloWorld 
at java.lang.ClassLoader.defineClass1(Native Method) 
at java.lang.ClassLoader.defineClass(ClassLoader.java:763) 
at 
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) 
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) 
at java.net.URLClassLoader.access$100(URLClassLoader.java:73) 
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362) 
at java.security.AccessController.doPrivileged(Native Method) 
at java.net.URLClassLoader.findClass(URLClassLoader.java:361) 
at java.lang.ClassLoader.loadClass(ClassLoader.java:424) 
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) 
at java.lang.ClassLoader.loadClass(ClassLoader.java:357) 
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

2,准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾

  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

  • 如果 static 变量是 fifinal 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶

    段完成

  • 如果 static 变量是 fifinal 的,但属于引用类型,那么赋值也会在初始化阶段完成(引用类型需要初始化后才能使用)

3,解析

将常量池中的符号引用解析为直接引用

package cn.itcast.jvm.t3.load; 
/*** 解析的含义 */ 
public class Load2 { 
    public static void main(String[] args) throws ClassNotFoundException, IOException { 		ClassLoader classloader = Load2.class.getClassLoader(); 
                                                                                       			//loadClass 方法不会导致类的解析和初始化
                                                                                       			Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C"); 
     	//new C();//new就可以将D类解析出来
                                                                                       			System.in.read(); 
    } 
}

class C { 
    D d = new D(); 
}

class D { }
4.3 初始化

()V 方法

初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时

  • 子类初始化,如果父类还没初始化,会引发

  • 子类访问父类的静态变量,只会触发父类的初始化

  • 执行Class.forName

  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static fifinal 静态常量(基本类型和字符串)不会触发初始化(在连接的准备阶段初始化完成的)

  • 类对象.class 不会触发初始化(在类加载时完成)

  • 创建该类的数组不会触发初始化

  • 类加载器的 loadClass 方法

  • Class.forName 的参数 2 为 false 时

实验

class A { 
    static int a = 0; 
    static { //静态代码块回合类的初始化一起执行,以此来检验该类是否已经初始化
        System.out.println("a init"); 
    }
}

class B extends A { 
    final static double b = 5.0; 
    static boolean c = false; 
    static { 
        System.out.println("b init");
    }
}

验证(实验时请先全部注释,每次只执行其中一个)

public class Load3 { 
    static { 
    	System.out.println("main init"); 
    }
    public static void main(String[] args) throws ClassNotFoundException { 
        // 1. 静态常量(基本类型和字符串)不会触发初始化 
        System.out.println(B.b); 
        // 2. 类对象.class 不会触发初始化 
        System.out.println(B.class); 
        // 3. 创建该类的数组不会触发初始化 
        System.out.println(new B[0]); 
        // 4. 不会初始化类 B,但会加载 B、A 
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.B"); 
        // 5. 不会初始化类 B,但会加载 B、A 
        ClassLoader c2 = Thread.currentThread().getContextClassLoader(); 
        //false代表不进行初始化
        Class.forName("cn.itcast.jvm.t3.B", false, c2); 
        // 1. 首次访问这个类的静态变量或静态方法时 
        System.out.println(A.a); 
        // 2. 子类初始化,如果父类还没初始化,会引发 
        System.out.println(B.c); 
        // 3. 子类访问父类静态变量,只触发父类初始化 
        System.out.println(B.a); 
        // 4. 会初始化类 B,并先初始化类 A 
        Class.forName("cn.itcast.jvm.t3.B"); 
    } 
}
4.4 练习

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化

public class Load4 { 
    public static void main(String[] args) { 
        System.out.println(E.a); 
        System.out.println(E.b); 
        System.out.println(E.c); 
    }
}

class E { 
    public static final int a = 10; 
    public static final String b = "hello"; 
    //由于要进行装箱操作,需要调用方法,所以只能推迟到初始化后赋值
    public static final Integer c = 20;	
}

典型应用 - 完成懒惰初始化单例模式

//这个内部类才是懒惰的,外部类时正常的,但是又通过内部的懒惰类来调用父类,从而实现调用懒惰类时才真正启动外部类,但是好像没什么用,不用的时候不会初始化,用到的时候自然会初始化…

public final class Singleton { 
    private Singleton() { } 
    // 内部类中保存单例 
    private static class LazyHolder { 
        static final Singleton INSTANCE = new Singleton(); 
    }
    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员 
    public static Singleton getInstance() { 
        return LazyHolder.INSTANCE; 
    }
}

以上的实现特点是:

  • 懒惰实例化

  • 初始化时的线程安全是有保障的

5. 类加载器

以 JDK 8 为例:

名称(由上到下)加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问(启动类加载器)
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension(双亲委派:加载时会检查上级是否已经加载过了,还要再委托上级检查上上级是否加载过了,如果上级都没有加载过,则加载类查找器去对应路径查找,让对应的加载器执行加载)
自定义类加载器自定义上级为 Application
5.1 启动类加载器

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load;
public class F { 
    static { 
        System.out.println("bootstrap F init"); 
    }
}

执行

package cn.itcast.jvm.t3.load; 
public class Load5_1 { 
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        //输出加载类的名字
        System.out.println(aClass.getClassLoader()); 
    }
}

输出

//由于启动类加载器是由c++编写的,java无法直接访问,所以会打印出null,其他两个加载器是可以正常打印出来 的,所以间接证明了结果是对的

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. 
cn.itcast.jvm.t3.load.Load5 
bootstrap F init 
null
  • -Xbootclasspath 表示设置 bootclasspath

  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后

  • 可以用这个办法替换核心类

    • java -Xbootclasspath:
    • java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>
5.2 扩展类加载器
package cn.itcast.jvm.t3.load; 

public class G { 
    static { 
        System.out.println("classpath G init"); 
    }
}

执行

public class Load5_2 { 
    public static void main(String[] args) throws ClassNotFoundException { 
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G"); 							System.out.println(aClass.getClassLoader());
    }
}

输出

classpath G init 
sun.misc.Launcher$AppClassLoader@18b4aac2

写一个同名的类

package cn.itcast.jvm.t3.load; 
public class G { 
    static { 
        System.out.println("ext G init"); 
    }
}

打个 jar 包

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class 
已添加清单 
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext

重新执行 Load5_2

输出

ext G init 
sun.misc.Launcher$ExtClassLoader@29453f44
5.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

注意

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

protected Class<?> loadClass(String name, boolean resolve) 

throws ClassNotFoundException { 
    synchronized (getClassLoadingLock(name)) { 
        // 1. 检查该类是否已经加载 
        Class<?> c = findLoadedClass(name); 
        if (c == null) { 
            long t0 = System.nanoTime(); 
            try {
                if (parent != null) { 
                    // 2. 有上级的话,委派上级 loadClass 
                    c = parent.loadClass(name, false); 
                } else { 
                    //parent==null说明这个加载器是启动加载器
                    // 3. 如果没有上级了(ExtClassLoader),则委派 
                    BootstrapClassLoader
                    c = findBootstrapClassOrNull(name); 
                } 
            } catch (ClassNotFoundException e) { 
            }
            if (c == null) { 
                long t1 = System.nanoTime(); 
                //都找不到就自己加载
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载 
                c = findClass(name); 
                // 5. 记录耗时 
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 
                sun.misc.PerfCounter.getFindClasses().increment(); 
            } 
        }
        if (resolve) { 
            resolveClass(c); 
        }
        return c; 
    } 
}

例如:

public class Load5_3 { 
    public static void main(String[] args) throws ClassNotFoundException { 
        Class<?> aClass = Load5_3.class.getClassLoader() .loadClass("cn.itcast.jvm.t3.load.H"); 
        System.out.println(aClass.getClassLoader()); 
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

  2. sun.misc.Launcher A p p C l a s s L o a d e r / / 2 处 , 委 派 上 级 s u n . m i s c . L a u n c h e r AppClassLoader // 2 处,委派上级sun.misc.Launcher AppClassLoader//2sun.misc.LauncherExtClassLoader.loadClass()

  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader查找

  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  6. sun.misc.Launcher E x t C l a s s L o a d e r / / 4 处 , 调 用 自 己 的 f i f i n d C l a s s 方 法 , 是 在 J A V A H O M E / j r e / l i b / e x t 下 找 H 这 个 类 , 显 然 没 有 , 回 到 s u n . m i s c . L a u n c h e r ExtClassLoader // 4 处,调用自己的 fifindClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher ExtClassLoader//4fifindClassJAVAHOME/jre/lib/extHsun.misc.LauncherAppClassLoader 的 // 2 处

  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 fifindClass 方法,在classpath 下查找,找到了

5.4 线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

public class DriverManager { 
    // 注册驱动的集合 
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
    // 初始化驱动 
    static { 
        loadInitialDrivers(); 
        println("JDBC DriverManager initialized"); 
    }

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但

JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在

DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() { 
    String drivers; 
    
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String> () {
            public String run() { 
                return System.getProperty("jdbc.drivers"); 
            } 
        }); 
    } catch (Exception ex) { 
        drivers = null; 
    }// 1)使用 ServiceLoader 机制加载驱动,即 SPI	
    AccessController.doPrivileged(new PrivilegedAction<Void>() { 
        public Void run() { 
            //ServiceLoader返回的是线程上下文类加载器,破坏了双亲委派规则,本来要用启动类加载器的
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 				Iterator<Driver> driversIterator = loadedDrivers.iterator(); 
            try{
                while(driversIterator.hasNext()) { 
                    driversIterator.next(); 
                } 
            } catch(Throwable t) { 
                // Do nothing 
            }return null; 
        } 
    }); 
    println("DriverManager.initialize: jdbc.drivers = " + drivers); 
    // 2)使用 jdbc.drivers 定义的驱动名加载驱动 
    if (drivers == null || drivers.equals("")) { 
        return; 
    }
    String[] driversList = drivers.split(":"); 
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {println("DriverManager.Initialize: loading " + aDriver); 
             // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
             //本来是要用启动类加载器加载的,但是这里是用app类加载器,打破了双亲委派规则
             Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); 
            } catch (Exception ex) { 
            println("DriverManager.Initialize: load failed: " + ex); 
        }
    }
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此

可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kGARG23h-1650292635801)(D:\文档\学习资料\笔记\jvm.assets\image-20211216221455994.png)]

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class); 
Iterator<接口类型> iter = allImpls.iterator(); 
while(iter.hasNext()) { 
    iter.next(); 
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC

  • Servlet 初始化器

  • Spring 容器

  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) { 
    // 获取线程上下文类加载器 
    ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
    return ServiceLoader.load(service, cl); 
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由

Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类

LazyIterator 中:(没讲这个)

private S nextService() {
    if (!hasNextService()) throw new NoSuchElementException();
    String cn = nextName; nextName = null;
    Class<?> c = null; 
    try {
        c = Class.forName(cn, false, loader); 
    } catch (ClassNotFoundException x) { 
        fail(service,"Provider " + cn + " not found"); 
    }if (!service.isAssignableFrom(c)) {
        fail(service, "Provider " + cn + " not a subtype");
    }try {
        S p = service.cast(c.newInstance()); 
        providers.put(cn, p); return p; 
    } catch (Throwable x) {
        fail(service, "Provider " + cn + " could not be instantiated", x); 
    }throw new Error(); // This cannot happen 
}
5.5 自定义类加载器

问问自己,什么时候需要自定义类加载器

  • 1)想加载非 classpath 随意路径中的类文件

  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计

  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类

  2. 要遵从双亲委派机制,重写 fifindClass 方法注意不是重写 loadClass 方法,否则不会走双亲委派机制

  3. 读取类文件的字节码

  4. 调用父类的 defifineClass 方法来加载类

  5. 使用者调用该类加载器的 loadClass 方法

示例:

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:略

package cn.itcast.jvm.t3.load;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2);//true,类被放到了缓冲空间中,不会重复加载

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3);//false,类加载器不同,不会被人为是同一个类

        c1.newInstance();//使用放射条用c1
    }
}

class MyClassLoader extends ClassLoader {

    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[] -> *.class,从第0个字节开始
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

6. 运行期优化
6.1 即时编译
分层编译

(TieredCompilation)

先来个例子

public class JIT1 { 
    public static void main(String[] args) { 
        for (int i = 0; i < 200; i++) { 
            long start = System.nanoTime(); 
            for (int j = 0; j < 1000; j++) { 
                new Object(); 
            }
            long end = System.nanoTime(); 
            System.out.printf("%d\t%d\n",i,(end - start)); 
        } 
	} 
}

运行结果略,就是运行时间越来越短

原因是什么呢?

JVM 将执行状态分成了 5 个层次:

0 层,解释执行(Interpreter)

  • 1 层,使用 C1 即时编译器编译执行(不带 profifiling)

  • 2 层,使用 C1 即时编译器编译执行(带基本的 profifiling)

  • 3 层,使用 C1 即时编译器编译执行(带完全的 profifiling)

  • 4 层,使用 C2 即时编译器编译执行

profifiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的

回边次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释

  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需

    再编译

  • 解释器是将字节码解释为针对所有平台都通用的机器码

  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运

行;另一方面,对于仅占据小部分的热点代码(多次执行的代码),我们则可以将其编译成机器码,以达到理想的运行速

度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由

来),优化之

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。

如:new Object(); 是不会被外部调用的,所以在c2阶段可能会被优化掉

可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果

参考资料:https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4

方法内联

(Inlining)如果方法代码太长就不会内联

private static int square(final int i) { 
    return i * i; 
}
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、

粘贴到调用者的位置:

System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化

System.out.println(81);

// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印inlining(内联) 信息

// -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining ,禁用JIT2类的square 方法

// -XX:+PrintCompilation 打印编译信息

字段优化

JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/

创建 maven 工程,添加依赖如下

<dependency> 
    <groupId>org.openjdk.jmh</groupId> 
    <artifactId>jmh-core</artifactId>
    <version>${jmh.version}</version> 
</dependency> 

<dependency> 
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>${jmh.version}</version>
    <scope>provided</scope> 
</dependency>

编写基准测试代码:

package test; 
import org.openjdk.jmh.annotations.*; 
import org.openjdk.jmh.runner.Runner; 
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options; 
import org.openjdk.jmh.runner.options.OptionsBuilder; 
import java.util.Random; 
import java.util.concurrent.ThreadLocalRandom; 

//进行预热,两轮
@Warmup(iterations = 2, time = 1) 
@Measurement(iterations = 5, time = 1) //测试5次
@State(Scope.Benchmark) 
public class Benchmark1 { 
    int[] elements = randomInts(1_000); 
    private static int[] randomInts(int size) { 
        Random random = ThreadLocalRandom.current(); 
        int[] values = new int[size]; 
        for (int i = 0; i < size; i++) { 
            values[i] = random.nextInt(); 
        }
        return values; 
    }
    
    //进行测试的类
    @Benchmark 
    public void test1() { 
        for (int i = 0; i < elements.length; i++) { 
            doSum(elements[i]); 
        } 
    }
    
    @Benchmark 
    public void test2() { 
        int[] local = this.elements; 
        for (int i = 0; i < local.length; i++) { 
            doSum(local[i]); 
        } 
    }
    
    @Benchmark 
    public void test3() { 
        for (int element : elements) { 
            doSum(element); 
        } 
    }
    static int sum = 0; 
    
    //可以设置是否进行内联
    @CompilerControl(CompilerControl.Mode.INLINE) 
    static void doSum(int x) { 
        sum += x; 
    }
    public static void main(String[] args) throws RunnerException { 
        Options opt = new OptionsBuilder() 
            .include(Benchmark1.class.getSimpleName()) 
            .forks(1) 
            .build(); 
        new Runner(opt).run();
    } 
}

首先启用 doSum 的方法内联,测试结果如下(每秒吞吐量,即每秒能调用多少次,分数越高的更好):

Benchmark Mode Samples Score Score error Units //得分还有得分误差,主要看得分误差
t.Benchmark1.test1 thrpt 5 2420286.539 390747.467 ops/s 
t.Benchmark1.test2 thrpt 5 2544313.594 91304.136 ops/s 
t.Benchmark1.test3 thrpt 5 2469176.697 450570.647 ops/s

接下来禁用 doSum 方法内联

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
    sum += x;
}

测试结果如下:

Benchmark Mode Samples Score Score error Units 
t.Benchmark1.test1 thrpt 5 296141.478 63649.220 ops/s 
t.Benchmark1.test2 thrpt 5 371262.351 83890.984 ops/s 
t.Benchmark1.test3 thrpt 5 368960.847 60163.391 ops/s

分析:

在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:

如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码):

@Benchmark
public void test1() {
    // elements.length 首次读取会缓存起来 -> int[] local
    for (int i = 0; i < elements.length; i++) {
        // 后续 999 次 求长度 <- local
        sum += elements[i];
        // 1000 次取下标 i 的元素 <- local
    }
}		

可以节省 1999 次 Field 读取操作

但如果 doSum 方法没有内联,则不会进行上面的优化

练习:在内联情况下将 elements 添加 volatile 修饰符,观察测试结果

6.2 反射优化
    public class Reflect1 {
            public static void foo() {
                System.out.println("foo...");
            }

            public static void main(String[] args) throws Exception {
                Method foo = Reflect1.class.getMethod("foo");//获得方法
                for (int i = 0; i <= 16; i++) {
                    System.out.printf("%d\t", i);
                    foo.invoke(null);//执行方法
                }
                System.in.read();
            }
        }

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现

package cn.itcast.jvm.t3.reflect;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

package sun.reflect; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import sun.reflect.misc.ReflectUtil;

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }

    public Object invoke(Object target, Object[] args) throws IllegalArgumentException, InvocationTargetException {
        // inflationThreshold 膨胀阈值,默认 15 
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右 
            MethodAccessorImpl generatedMethodAccessor = (MethodAccessorImpl) (
                new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(),
                                                              this.method.getName(), this.method.getParameterTypes(), 
                                                              this.method.getReturnType(), this.method.getExceptionTypes(),
                                                              this.method.getModifiers());
            this.parent.setDelegate(generatedMethodAccessor);
        }
        // 调用本地实现 
        return invoke0(this.method, target, args);
    }

    void setParent(DelegatingMethodAccessorImpl parent) {
        this.parent = parent;
    }

    private static native Object invoke0(Method method, Object target, Object[] args);
}

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到

类名为 sun.reflflect.GeneratedMethodAccessor1

可以使用阿里的 arthas 工具:

java -jar arthas-boot.jar 
[INFO] arthas-boot version: 3.1.1 
[INFO] Found existing java process, please choose one and hit RETURN. 
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1

选择 1 回车表示分析该进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QOtLrAdP-1650292635802)(D:\文档\学习资料\笔记\jvm.assets\image-20211217001153805.png)]

再输入【jad + 类名】来进行反编译

$ jad sun.reflect.GeneratedMethodAccessor1 
ClassLoader: 
+-sun.reflect.DelegatingClassLoader@15db9742
+-sun.misc.Launcher$AppClassLoader@4e0e2f2a 
+-sun.misc.Launcher$ExtClassLoader@2fdb006e 
Location:


/**
 * Decompiled with CFR 0_132.
 * ** Could not load the following classes:
 * * cn.itcast.jvm.t3.reflect.Reflect1
 */
package sun.reflect;
        import cn.itcast.jvm.t3.reflect.Reflect1;
        import java.lang.reflect.InvocationTargetException;
        import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    /**
     * Loose catch block
     * * Enabled aggressive block sorting
     * * Enabled unnecessary exception pruning
     * * Enabled aggressive exception aggregation
     * * Lifted jumps to return sites
     */
    public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
        // 比较奇葩的做法,如果有参数,那么抛非法参数异常
        block4:
        {
            if (arrobject == null || arrobject.length == 0) break block4;
            throw new IllegalArgumentException();
        }
        try {
            // 可以看到,已经是直接调用了😱😱😱 
            Reflect1.foo();
            // 因为没有返回值 
            return null;
        } catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        } catch (ClassCastException | NullPointerException runtimeException) {
            throw new IllegalArgumentException(Object.super.toString());
        }
    }
}

    Affect(row-cnt:1) cost in 1540ms.

注意

通过查看 ReflectionFactory 源码可知

sun.reflflect.noInflflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首

次生成比较耗时,如果仅反射调用一次,不划算)

sun.reflflect.inflflationThreshold 可以修改膨胀阈值


七,内存模型

1. java 内存模型

很多人将【java 内存结构】与【java 内存模型】傻傻分不清,【java 内存模型】是 Java Memory

Model(JMM)的意思。

关于它的权威解释,请参考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf?AuthParam=1562811549_4d4994cbd5b59d964cd2907ea22ca08b

简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序

性、和原子性的规则和保障

1.1 原子性

原子性在学习线程时讲过,下面来个例子简单回顾一下:

问题提出,两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

1.2 问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操

作。

例如对于 i++ 而言(

i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值 
iconst_1 // 准备常量1 
iadd // 加法 
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值 
iconst_1 // 准备常量1 
isub // 减法 
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:(类似于局部变量表和操作数操,因为进程交替进行,数值返回时可能将另一个进程的操作抹掉,所以才会产生这种结果)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dH1odqYX-1650292635802)(D:\文档\学习资料\笔记\jvm.assets\image-20211217002635007.png)]

1.3 解决方法

synchronized (同步关键字)

语法

synchronized( 对象 ) { 要作为原子操作代码 }

用 synchronized 解决并发问题:

static int i = 0;
    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            //若将synchronized加在for外面,可以减少加锁解锁次数,相当于一个优化
            for (int j = 0; j < 5000; j++) {
                synchronized (obj) {
                    i++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                synchronized (obj) {
                    i--;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

如何理解呢:你可以把 obj 想象成一个房间,线程 t1,t2 想象成两个人。

当线程 t1 执行到 synchronized(obj) 时就好比 t1 进入了这个房间,并反手锁住了门,在门内执行

count++ 代码。

这时候如果 t2 也运行到了 synchronized(obj) 时,它发现门被锁住了,只能在门外等待。

当 t1 执行完 synchronized{} 块内的代码,这时候才会解开门上的锁,从 obj 房间出来。t2 线程这时才

可以进入 obj 房间,反锁住门,执行它的 count-- 代码。

注意:上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象,如果 t1 锁住的是 m1 对

象,t2 锁住的是 m2 对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。

2. 可见性
2.1 退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

static boolean run = true; 
public static void main(String[] args) throws InterruptedException { 
    Thread t = new Thread(()->{ 
        while(run){ 
            // .... 
        } 
    }); 
    t.start(); Thread.sleep(1000); 
    run = false; // 线程t不会如预想的停下来 
}

为什么呢?分析一下:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

  1. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高

    速缓存中,减少对主存中 run 的访问,提高效率

  2. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读

    取这个变量的值,结果永远是旧值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8YDX2oBV-1650292635803)(D:\文档\学习资料\笔记\jvm.assets\image-20211217004347939.png)]

2.2 解决方法

volatile(易变关键字)//直接加在变量最前面

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到

主存中获取它的值,线程操作 volatile 变量都是直接操作主存

2.3 可见性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一

个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况:

上例从字节码理解是这样的:

getstatic run // 线程 t 获取 
run true getstatic run // 线程 t 获取 
run true getstatic run // 线程 t 获取 
run true getstatic run // 线程 t 获取 
run true putstatic run // 线程 main 修改 run 为 false, 仅此一次 
getstatic run // 线程 t 获取 run false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解

决指令交错

注意

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是

synchronized是属于重量级操作,性能相对更低

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也

能正确看到对 run 变量的修改了,想一想为什么?//因为println源码中出现了synchronized

3. 有序性
3.1 诡异的结果
int num = 0; 

boolean ready = false; 

// 线程1 执行此方法 

public void actor1(I_Result r) { 

    if(ready) { 

        r.r1 = num + num; 

    } else { 

        r.r1 = 1; 

    } 

}

// 线程2 执行此方法 

public void actor2(I_Result r) { 

    num = 2; 

    ready = true; 

}

结果有可能等于0

这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行

num = 2

相信很多人已经晕了 😵😵😵

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:

借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress

//用mvn命令创建项目

mvn archetype:generate -DinteractiveMode=false - 
DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test- archetype -DgroupId=org.sample -DartifactId=test -Dversion=1.0

创建 maven 项目,提供如下测试类

@JCStressTest 
//意料中的结果,放入Expect.ACCEPTABLE
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") 
//意料外的结果,放入Expect.ACCEPTABLE_INTERESTING
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!") 

@State 

public class ConcurrencyTest { 

    int num = 0; 

    boolean ready = false; 

    @Actor 

    public void actor1(I_Result r) { 

        if(ready) { 

            r.r1 = num + num; 

        } else { 

            r.r1 = 1; 

        } 

    }

    @Actor 

    public void actor2(I_Result r) { 

        num = 2; 

        ready = true; 

    } 

}

执行

mvn clean install 

java -jar target/jcstress.jar

会输出我们感兴趣的结果,摘录其中一次结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Onf9btqz-1650292635804)(D:\文档\学习资料\笔记\jvm.assets\image-20211217010220096.png)]

可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了。

3.2 解决方法

volatile 修饰的变量,可以禁用指令重排

@JCStressTest 

@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") 

@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!") 

@State 

public class ConcurrencyTest { 

    int num = 0; 

    volatile boolean ready = false; 

    @Actor 

    public void actor1(I_Result r) { 

        if(ready) { 

            r.r1 = num + num; 

        } else { 

            r.r1 = 1; 

        } 

    }

    @Actor 

    public void actor2(I_Result r) { 

        num = 2; 

        ready = true; 

    } 

}
3.3 有序性理解

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i; 

static int j; 

// 在某个线程内执行如下赋值操作 

i = ...; // 较为耗时的操作 

j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行

时,上下两条指令可以互换位置

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性,例如著名的 double-checked

locking 模式实现单例

public final class Singleton { 

    private Singleton() { } 

    private static Singleton INSTANCE = null; 

    public static Singleton getInstance() { 

        // 实例没创建,才会进入内部的 synchronized代码块 

        if (INSTANCE == null) { 

            synchronized (Singleton.class) { 

                // 在加锁时可能有其他进程经过了上一层的if判断,进入阻塞队列,所以需要在判断一次 

                if (INSTANCE == null) { 

                    INSTANCE = new Singleton(); 

                } 

            } 

        }

        return INSTANCE; 

    } 

}

以上的实现特点是:

  • 懒惰实例化

  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

但在多线程环境下,上面的代码是有问题的, INSTANCE = new Singleton() 对应的字节码为:

0: new #2 // class cn/itcast/jvm/t4/Singleton 
3: dup 
4: invokespecial #3 // Method "<init>":()V 
7: putstatic #4 // Field 
INSTANCE:Lcn/itcast/jvm/t4/Singleton;

其中 4 7 两步的顺序不是固定的,也许 jvm 会优化为:先将引用地址赋值给 INSTANCE 变量后,再执行

构造方法,如果两个线程 t1,t2 按如下时间序列执行:
时间1 t1 线程执行到 INSTANCE = new Singleton(); 
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处) 
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null(7 处) 
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != null(synchronized块外),直接 
返回 INSTANCE 
时间5 t1 线程执行Singleton的构造方法(4 处)

这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将

是一个未初始化完毕的单例

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才

会真正有效

3.4 happens-before

happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,

抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变

量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x; 

static Object m = new Object(); 

new Thread(()->{ 

    synchronized(m) { 

        x = 10; 

    } 

},"t1").start(); 

new Thread(()->{ 

    synchronized(m) { 

        System.out.println(x); 

    } 

},"t2").start(); 
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x; 

new Thread(()->{ 

    x = 10; 

},"t1").start(); 

new Thread(()->{ 

    System.out.println(x); 

},"t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x; 

x = 10; 

new Thread(()->{ 

    System.out.println(x); 

},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或t1.join()等待它结束)
static int x; 

Thread t1 = new Thread(()->{ 

    x = 10; 

},"t1"); 

t1.start(); 

t1.join(); 

System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
static int x; 

public static void main(String[] args) { 

    Thread t2 = new Thread(()->{ 

        while(true) { 

            if(Thread.currentThread().isInterrupted()) { 

                System.out.println(x); 

                break; 

            } 

        } 

    },"t2"); 

    t2.start(); 

    new Thread(()->{ 

        try {

            Thread.sleep(1000); 

        } catch (InterruptedException e) { 

            e.printStackTrace(); 

        }

        x = 10; 

        t2.interrupt(); 

    },"t1").start(); 

    while(!t2.isInterrupted()) { 

        Thread.yield(); 

    }

    System.out.println(x);

}
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z

变量都是指成员变量或静态成员变量

参考: 第17页

4. CAS 与 原子类
4.1 CAS

CAS 即 Compare and Swap ,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执

行 +1 操作:

// 需要不断尝试 

while(true) { 

    int 旧值 = 共享变量 ; // 比如拿到了当前值 0 

    int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1 

    /*

    这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候 

    compareAndSwap 返回 false,重新尝试,直到: 

    compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰 

    */

    if( compareAndSwap ( 旧值, 结果 )) { 

        // 成功,退出循环 

    } 

}

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无

锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一

  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,下面是直接使用 Unsafe 对象进

行线程安全保护的一个例子

import sun.misc.Unsafe; 

import java.lang.reflect.Field; 

public class TestCAS { 

    public static void main(String[] args) throws InterruptedException { 

        DataContainer dc = new DataContainer(); 

        int count = 5;

        Thread t1 = new Thread(() -> { 

            for (int i = 0; i < count; i++) { 

                dc.increase(); 

            } 

        }); 

        t1.start(); 

        t1.join(); 

        System.out.println(dc.getData()); 

    } 

}

class DataContainer { 

    private volatile int data; 

    static final Unsafe unsafe; 

    static final long DATA_OFFSET; 

    static { 

        try {

            // Unsafe 对象不能直接调用,只能通过反射获得 

            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); 

            theUnsafe.setAccessible(true); 

            unsafe = (Unsafe) theUnsafe.get(null); 

        } catch (NoSuchFieldException | IllegalAccessException e) { 

            throw new Error(e); 

        }

        try {

            // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性 

            DATA_OFFSET = 

                unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data")); 

        } catch (NoSuchFieldException e) { 

            throw new Error(e); 

        } 

    }

    public void increase() { 

        int oldValue; 

        while(true) { 

            // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解 

            oldValue = data; 

            // cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false 

            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue + 

                                         1)) { 

                return; 

            } 

        } 

    }

    public void decrease() { 

        int oldValue; 

        while(true) { 

            oldValue = data; 

            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - 

                                         1)) { 

                return; 

            } 

        } 

    }

    public int getData() { 

        return data; 

    } 

}
4.2 乐观锁与悲观锁
  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

4.3 原子操作类

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、

AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

可以使用 AtomicInteger 改写之前的例子:

// 创建原子整数对象

private static AtomicInteger i = new AtomicInteger(0); 

public static void main(String[] args) throws InterruptedException { 

    Thread t1 = new Thread(() -> { 

        for (int j = 0; j < 5000; j++) { 

            i.getAndIncrement(); // 获取并且自增 i++ 

            // i.incrementAndGet(); // 自增并且获取 ++i 

        } 

    });

    Thread t2 = new Thread(() -> { 

        for (int j = 0; j < 5000; j++) { 

            i.getAndDecrement(); // 获取并且自减 i-- 

        } 

    }); 

    t1.start(); 

    t2.start(); 

    t1.join(); 

    t2.join(); 

    System.out.println(i); //结果为0

}
5. synchronized 优化

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存

储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指

针 、 重量级锁指针 、 线程ID 等内容

5.1 轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻

量级锁来优化。这就好比:

学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没

有竞争,继续上他的课。

如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程 A 随即升级为重量级锁,

进入重量级锁的流程。

而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来

假设有两个方法同步块,利用同一个对象加锁

static Object obj = new Object(); 

public static void method1() { 

    synchronized( obj ) { 

        // 同步块 A 

        method2(); 

    } 

}

public static void method2() { 

    synchronized( obj ) { 

        // 同步块 B 

    } 

}

每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sQX8YCxL-1650292635806)(D:\文档\学习资料\笔记\jvm.assets\image-20211217015151493.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RexRbiDe-1650292635807)(D:\文档\学习资料\笔记\jvm.assets\image-20211217015206491.png)]

5.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻

量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj = new Object(); 

public static void method1() { 

    synchronized( obj ) { 

        // 同步块 

    } 

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DxzeOJRK-1650292635808)(D:\文档\学习资料\笔记\jvm.assets\image-20211217015638024.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LcZDipHM-1650292635808)(D:\文档\学习资料\笔记\jvm.assets\image-20211217015728091.png)]

5.3 重量锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退

出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能

性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)

  • Java 7 之后不能控制是否开启自旋功能

自旋重试成功的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AMQENhHp-1650292635809)(D:\文档\学习资料\笔记\jvm.assets\image-20211217020046022.png)]

自旋重试失败的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OyE8aFqd-1650292635810)(D:\文档\学习资料\笔记\jvm.assets\image-20211217020102486.png)]

5.4 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁

来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID

是自己的就表示没有竞争,不用重新 CAS.

撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)

访问对象的 hashCode 也会撤销偏向锁

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,

重偏向会重置对象的 Thread ID

撤销偏向和重偏向都是批量进行的,以类为单位

如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的

可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁

可以参考这篇论文:https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

假设有两个方法同步块,利用同一个对象加锁

static Object obj = new Object(); 

public static void method1() { 

    synchronized( obj ) { 

        // 同步块 A 

        method2(); 

    } 

}

public static void method2() { 

    synchronized( obj ) { 

        // 同步块 B 

    } 

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sz7RNvoP-1650292635811)(D:\文档\学习资料\笔记\jvm.assets\image-20211217020644892.png)]

5.5 其它优化

1. 减少上锁时间

同步代码块中尽量短

2. 减少锁的粒度

将一个锁拆分为多个锁提高并发度,例如:

  • ConcurrentHashMap //只锁链表头

  • LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值//用于累加的类

  • LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高

3. 锁粗化

多次循环进入同步块不如同步块内多次循环

另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,

没必要重入多次)

new StringBuffer().append("a").append("b").append("c");

4. 锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候

就会被即时编译器忽略掉所有同步操作。

5. 读写分离

//正常读,写的话会复制到一个新的数组,加锁只需要加写的部分就可以

CopyOnWriteArrayList

ConyOnWriteSet
;

static { 

    try {

        // Unsafe 对象不能直接调用,只能通过反射获得 

        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); 

        theUnsafe.setAccessible(true); 

        unsafe = (Unsafe) theUnsafe.get(null); 

    } catch (NoSuchFieldException | IllegalAccessException e) { 

        throw new Error(e); 

    }

    try {

        // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性 

        DATA_OFFSET = 

            unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data")); 

    } catch (NoSuchFieldException e) { 

        throw new Error(e); 

    } 

}

public void increase() { 

    int oldValue; 

    while(true) { 

        // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解 

        oldValue = data; 

        // cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false 

        if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue + 

                                     1)) { 

            return; 

        } 

    } 

}

public void decrease() { 

    int oldValue; 

    while(true) { 

        oldValue = data; 

        if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - 

                                     1)) { 

            return; 

        } 

    } 

}

public int getData() { 

    return data; 

} 

}


##### **4.2** **乐观锁与悲观锁**

- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

##### **4.3** **原子操作类**

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、

AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

可以使用 AtomicInteger 改写之前的例子:

// 创建原子整数对象 

```java
private static AtomicInteger i = new AtomicInteger(0); 

public static void main(String[] args) throws InterruptedException { 

    Thread t1 = new Thread(() -> { 

        for (int j = 0; j < 5000; j++) { 

            i.getAndIncrement(); // 获取并且自增 i++ 

            // i.incrementAndGet(); // 自增并且获取 ++i 

        } 

    });

    Thread t2 = new Thread(() -> { 

        for (int j = 0; j < 5000; j++) { 

            i.getAndDecrement(); // 获取并且自减 i-- 

        } 

    }); 

    t1.start(); 

    t2.start(); 

    t1.join(); 

    t2.join(); 

    System.out.println(i); //结果为0

}
5. synchronized 优化

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存

储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指

针 、 重量级锁指针 、 线程ID 等内容

5.1 轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻

量级锁来优化。这就好比:

学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没

有竞争,继续上他的课。

如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程 A 随即升级为重量级锁,

进入重量级锁的流程。

而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来

假设有两个方法同步块,利用同一个对象加锁

static Object obj = new Object(); 

public static void method1() { 

    synchronized( obj ) { 

        // 同步块 A 

        method2(); 

    } 

}

public static void method2() { 

    synchronized( obj ) { 

        // 同步块 B 

    } 

}

每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

[外链图片转存中…(img-sQX8YCxL-1650292635806)]

[外链图片转存中…(img-RexRbiDe-1650292635807)]

5.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻

量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj = new Object(); 

public static void method1() { 

    synchronized( obj ) { 

        // 同步块 

    } 

}

[外链图片转存中…(img-DxzeOJRK-1650292635808)]

[外链图片转存中…(img-LcZDipHM-1650292635808)]

5.3 重量锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退

出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能

性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)

  • Java 7 之后不能控制是否开启自旋功能

自旋重试成功的情况

[外链图片转存中…(img-AMQENhHp-1650292635809)]

自旋重试失败的情况

[外链图片转存中…(img-OyE8aFqd-1650292635810)]

5.4 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁

来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID

是自己的就表示没有竞争,不用重新 CAS.

撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)

访问对象的 hashCode 也会撤销偏向锁

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,

重偏向会重置对象的 Thread ID

撤销偏向和重偏向都是批量进行的,以类为单位

如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的

可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁

可以参考这篇论文:https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

假设有两个方法同步块,利用同一个对象加锁

static Object obj = new Object(); 

public static void method1() { 

    synchronized( obj ) { 

        // 同步块 A 

        method2(); 

    } 

}

public static void method2() { 

    synchronized( obj ) { 

        // 同步块 B 

    } 

}

[外链图片转存中…(img-Sz7RNvoP-1650292635811)]

5.5 其它优化

1. 减少上锁时间

同步代码块中尽量短

2. 减少锁的粒度

将一个锁拆分为多个锁提高并发度,例如:

  • ConcurrentHashMap //只锁链表头

  • LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值//用于累加的类

  • LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高

3. 锁粗化

多次循环进入同步块不如同步块内多次循环

另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,

没必要重入多次)

new StringBuffer().append("a").append("b").append("c");

4. 锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候

就会被即时编译器忽略掉所有同步操作。

5. 读写分离

//正常读,写的话会复制到一个新的数组,加锁只需要加写的部分就可以

CopyOnWriteArrayList

ConyOnWriteSet

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值