jvm学习

时间:2024-10-14 16:46:56

JVM学习

概述

java虚拟机——java运行环境

优势

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

概念比较

  • JRE:JVM+基础类库
  • JDK:JVM+基础类库+编译工具
  • JAVASE:JDK+IDE工具
  • JAVAEE:JDK+应用服务+IDE工具

基本结构

  • 在这里插入图片描述

面试

  • 理解底层的实现原理
  • 中高级程序猿的必备技能

JVM内存结构

程序计数器——Program Counter Register

概述
  • java源代码->二进制字节码->解释器->机器码->CPU

  • 记住下一条JVM指令的执行地址(解释器中)

特点
  • 线程私有的
    • 线程切换,各个线程的程序计数器记住下一行指定的地址
  • 唯一不会存在内存溢出

虚拟机栈——Java Virtural Machine Stacks

概述
  • 每个线程运行需要的内存空间
  • 栈帧:一个栈由多个栈帧组成
    • 每个方法运行时需要的内存
      • 参数
      • 局部变量
      • 返回地址
  • 每个线程只能有一个活动的栈帧,对应正在执行的方法
  • 方法执行完成,栈帧出战,对应方法的执行完成
特点
  • 线程私有
问题辨析
  • 垃圾回收是否涉及栈内存?

    • 不需要,方法调用完成,栈帧弹出,对应的栈帧内存就会自动回收
  • 栈内存分配越大越好吗?

    • 内存划分:-Xss size
    • 默认:1024k
    • 不是。
      • 栈内存更大,只能够增加方法递归调用的数量,对方法的执行效率没有帮助。
      • 由于物理内存的大小固定,栈内存分配的越大,可运行的线程数就会减少。
  • 方法内的局部变量是否是线程安全的?

    • 是线程安全的

      • 如果方法内局部变量没有逃离方法的作用返回,是线程安全的

      • 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全(可能被其他线程访问修改)

      • 每个线程都有自己的方法栈

      • 作用于栈帧内局部变量是线程私有的

      • static变量不是——不是存在栈中?

栈内存溢出*
  • 栈帧过多导致栈溢出——递归调用没有正确结束
  • 栈帧多大,大于栈内存——不容易出现
线程运行诊断
  • CPU占用过多
    • 定位
      1. 使用top定位到哪个进程对CPU占用过高
      2. 使用ps H -eo pid,tid,%cpu |grep 进程id,定位哪个线程英气的cpu占用过高
      3. 使用jstack 进程id
        • 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行数
  • 运行了很长时间不返回结果——死锁
    • 使用jstack

本地方法栈——Native Method Stacks

  • 调用不是由java编写的方法
  • 线程私有

堆——Heap

概述

通过new关键字,创建对象都会使用堆内存

特点
  • 线程共享,所以堆中的对象要考虑线程安全问题

  • 有垃圾回收机制

内存溢出
  • OutOfMemoryError
  • 大小修改
    • Xmxsize
堆内存诊断
  • jps
    • 查看当前系统中有哪些java进程
  • jmap -heap pid
    • 查看堆内存占用情况(非实时)
  • jconsole
    • 图形界面的,多工能的检测工具,可以连续监测
案例
  • 垃圾回收jconsole GC后,堆内存占用率还是很高

    • 可能相互引用的无法回收

    • jvisualvm可视化工具(JDK8后不自带自行下载

      • 监视->堆dump——获取堆内存快照
      • 各个实例对象的占用大小

方法区

  • 所有java虚拟机线程共享

  • 虚拟机启动时被创建

  • 逻辑上属于堆的一部分(不同厂商不同)

    • 永久代——JDK1.6堆中

    • 元空间——操作系统内存1.8

存储内容
  • Class相关信息
    • 成员变量
    • 成员方法
    • 构造器方法代码
    • 方法数据
    • 。。。
  • 常量池
运行时常量池
  • 常量池:常量池是字节码文件的一部分,运行时,常量池中的信息会被加载到运行时常量池,此时各种变量都只是符号,只有运行到的时候才会变成对象

  • 注意:s1+s2是使用stringBuffer利用new创建的对象,变量存在于堆中,而s= ‘xxx’,s存在于字符串池中,所以两种变量的地址不同,内容相同

  • s = ‘a’+‘b’ 和 s1 = ”ab“都是直接存在在字符串值,地址相同,内容相同

  • 延迟加载对象:字符串对象加载的时候,只有执行到了才会加载字符串内容,并且如果有,则不会再穿件

  • new的字符串对象和string穿件的对象不是同一个,两者存储位置一个在堆(不是引用串,是字符串就在堆中,s.intern可以将堆中的字符串放入串池(有则不会放入——返回串池对象,但不会修改堆对象应用、没有则放入——返回串池对象并引用串池中的对象)),一个在串池

StringTable
特性
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量的拼接原理是StringBuilder(1.8)——堆中
  • 字符串常量拼接的原理是编译期优化——串池中
  • 可以使用inter方法,主动将串池中还没有的字符串放入串池
    • 1.8将这个字符串对象放入串池,如果有则不会方法,如果没有则会放入,并把串池对象返回值
    • 1.6将这个字符串放入串池,如果有则不会方法,如果没有会把此对象复制一份放入串池,把串池对象返回
    • 都返回串池对象,但1.8堆中的字符串对象会转入串池,1.6堆中的对象还是堆中的对象
位置
  • 1.6存放在永久代常量池中
  • 1.8存放在堆中
垃圾回收机制
  • 工具
    • -XX:+PrintStringTableStatistics:查看串池统计信息
    • -XX:+PrintGCDetils -verbose:gc:打印垃圾回收信息
  • 程序运行完成,字符串还是存在于串池中,当串池达到容量时,会触发GC自动回收,将未被使用的字符串空间回收
stringtable性能调优
  • stringtable底层时一个hashtable(数组+链表)
  • 数组的长度就是桶数量,使用-XX:StringTableSize设置
  • 调优思想
    • 如果字符串数量多,可以增加StringTableSize大小,防止Hash冲突,降低时间因为要检查是否存在该字符串,Hash冲突的话需要对链表注意检查
    • 考虑将字符串对象的是否放入池中s.intern推中有很多重复的字符串,入池能够减少内存的使用
元空间内存溢出
  • 演示创建多个类占用方法区
    • ClassLoader——可以用来加载类的二进制字节码(动态定义类)
  • OutOfMemoryError:Metaspace
  • 元空间内存大小修改
    • -XX:MaxMetaspaceSize=8m
  • 场景
    • cglib代理,动态生成代理类

直接内存

概述
  • 不属于JVM内存,是操作系统内存
特点
  • 常见于NIO操作时,用于数据缓冲区不是传统的阻塞IO操作

    • 在这里插入图片描述

    • 在这里插入图片描述

    • 为什么更快——直接内存Java和系统都能访问,不用多次复制

      • 在这里插入图片描述

      • 在这里插入图片描述

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

  • 不受JVM内存回收管理

    • 内存溢出:OutOfMemoryError:Direct buffer memory
    • 释放原理:由ReferenceHandler线程通过Cleanerclean方法使用 unsafe.freeMemory()释放,实际对象被java回收后触发前面直接内存的回收的过程。 gc只能释放java内存
    • 注意-XX:+DisableExplicitGC禁用显示的垃圾回收System.GC()(是一种FULL GC),这样也会禁用直接内存的释放,可以直接使用unsafe.freeMemory()释放直接内存,不用手动GC回收java内存从而触发直接内存回收

垃圾回收

如何判断对象可以回收?
  • 引用计数法
    • 某个对象被引用则计数+1,计数为0则可以被回收
    • 缺点:无法解决循环引用——循环对象引用计数无法为0
  • 可达性分析算法(java使用)
    • 判断一个对象是否直接或者间接被根对象引用,没有则可回收
    • 扫描堆中的对象,看是否能够沿着GC ROOT对象为起点的引用链找到对象
    • 如何确定GC ROOT?哪些对象可以成为GC ROOT
      • 使用Memory Analyzer分析堆快照
      • 系统类相关对象
      • 本地方法相关对象
      • 活动线程相关对象
      • 加锁的对象相关对象
  • 四种引用
    • 强引用
      • 只有所有GC ROOTs对象都不通过强引用该对象的时候,该对象才能被垃圾回收
    • 软引用
      • 仅有软引用引用该对象的时候,在垃圾回收后,内存仍不足时就再次触发垃圾回收,回收软引用对象

      • 可以配合引用队列来释放软引用自身ReferenceQueue<T>

      • 软引用释放后,软引用对应数据会变为null,可以定义ReferenceQueue,当软引用对象被回收,该引用会被自动加入到队列中(用于删除无用的软引用)在这里插入图片描述

      • 场景:存放较大的非核心数据——强引用引用软引用(SoftReference对象),软引用引用非核心数据

    • 弱引用
      • 仅有若引用引用该对象的时候,在垃圾回收时,无论内存是否充足,都会回收若引用对象
      • 可以配合引用队列ReferenceQueue<T>释放弱引用自身
      • 场景:list引用WeakReference->byte[]
    • 虚引用
      • 必须配合引用队列ReferenceQueue<T>使用,主要配合Byte Buffer使用,被引用对象回收时,会将徐引用入队,由ReferenceHandler线程调用虚引用相关方法释放直接内存
    • 终结器引用
      • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收)再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
垃圾回收算法
  • 在实际的JVM中,更具不同的情况使用不同的算法回收垃圾
  • 标记清除算法
    • 1.先标记没有被GC ROOT引用的对象(可清除的)
    • 2.清除:将内存空间的起始、终止地址记录到空闲地址列表中不会清空数据
    • 优点
      • 速度快:不需要清除数据
    • 缺点
      • 容易产生内存碎片:不会做空间整理
  • 标记整理算法
    • 1.先标记没有被GC ROOT引用的对象(可清除的)
    • 2.清除整理:将可用(存活)对象重新整理
    • 优点
      • 避免内存碎片
    • 缺点
      • 处理速度慢(对象移动、需要将所有变量的地址进行改)
  • 复制算法
    • 将内存纷争两个等大的空间
    • 1.标记垃圾
    • 2.将可用对象复制移动到另一块空白区域(占用连续空间),原空间所有的空间清空(全是无用的对象)
    • 优点
      • 没有内存碎片
    • 缺点
      • 需要双倍的空间
分代垃圾回收
  • 分区结构

    • 新生代def new generation
      • 伊甸园eden
      • 幸存区Fromfrom
      • 幸存区Toto
    • 老年代tenured generation
  • 工作流程

    • 1.新创建的对象首先分配在伊甸园区域
    • 2.新生代空间不足,触发minor GC,伊甸园区和From区存活对象使用复制算法到To区中,存活对象寿命+1,并交换From区和To区引用地址
    • 3.如果对象寿命超过指定阈值,该对象移至老年代 (最大15,对象头4bit存储)
    • 4.当老年代空间不足,会先尝试触发minior GC,如果空间仍然不足,那么触发full GC
    • 大对象直接晋升老年代:如果新生代容量肯定放不大对象,老年代可以,则直接带入老年代
    • 注意:
      • minio GC 会引发stop the world:暂停用户线程运行,垃圾回收线程进行垃圾回收
      • 线程内的内存溢出中断不会引起主线程终端
  • 相关VM参数

    • 堆初始大小
      • -Xms
    • 堆最大大小
      • -Xms\-XX:MaxHeapSize=size
    • 新生代大小
      • -Xmn\(-XX:NewSize= size+ -XX:MaxNewSize = size)
    • 幸存去比例(动态)
      • -XX:InitialSurvivorRatio=ratio-XX:+UserAdapativeSizePolicy
    • 幸存区比例
      • -XX:SurvivorRatio=ratio
    • 晋升阈值
      • -XX:MaxTrnuringThreshold=threshold
    • 晋升详情
      • -XX:+PrintTenuringDistribution
    • GC详情
      • -XX:+PrintGCDetial -verbose:gc
    • FullGC前MiniorGC
      • -XX:+ScavengeBeforeFullGC
垃圾回收器
  • 串行

    • 特点
      • 单线程回收
      • 堆内存较小,适合个人电脑(CPU核少)
    • 流程
      • -XX:+UseSerialGC=Serial+SerialOld
        • Serial:新生代+复制算法
        • SerialOld:老年代+标记整理
  • 吞吐量优先(JDK8默认)

    • 特点
      • 多线程
      • 堆内存大的情况,多核CPU支持
      • 单位时间内,STW的时间最短——单位时间内GC的时间总和要少
    • 流程
      • -XX:+UseParallelGC——新生代复制算法和-XX:+UseParallelOldGC——老年代标记整理算法
      • 默认线程数和CPU核数一样的 -XX:ParallelGCThreads=n
      • -XX:+UserAdapativeSizePolicy——新生代自适应调整大小
      • 调优参数
        • (目标)-XX: GCTimeRatio=ratio——调整垃圾回收时间和总是简单的占比达到1/(1+ratio)(堆增大)
        • (目标)-XX:MaxGCPauseMillis=ms——默认200ms,最大STW(堆减小)
  • 响应时间有限

    • 特点
      • 多线程
      • 堆内存大的情况,多核CPU支持
      • 垃圾回收的时候,单次STW时间尽可能最短——可以多GC,单每次都要小
    • 流程
      • -XX: +UseConcMarkSweepGC(老年代——标记清除——有碎片——CMS并发失败会使用SerialOld垃圾回收器)-XX:+UseParNewGC(新生代)()

      • 调优参数

        • XX:ParallelGCThreads=n一般设为核数,-XX:ConcGCThreads=threads 并发回收线程数(设置为前者的1/4,其余运行用户线程)
        • -XX: CMSInitiatingOccupancyFraction=percent——合适进行垃圾回收(不能满之后再回收,需要流出空间用于并发时的浮动垃圾)
        • -XX:+CMSScavengeBeforeRemark——重新标记阶段前做先对新生代gc
      • 注意:

        • 老年代使用并发标记清除,会产生大量的空间碎片,导致并发失败,那么该回收器会退化为串行垃圾回收老年代——标记清除整理,这会导致STW时间很长
        • 会扫描整个堆内存
      • G1

        • 历程

          • JDK6——体验
          • JDK7——官方支持
          • JDK9——默认——取代CMS
        • 适用场景

          • 同时注重吞吐量(Throughput)和低延迟(Lowlatency),默认的暂停目标是200ms
          • 超大堆内存,会将堆划分为多个大小相等的Region
          • 整体上是标记+整理算法两个区域之间是复制算法
        • 相关参数

          • -XX:+UseG1Gc——开启G1、默认
          • -XX:G1HeapRegionSize=size——设置regin大小:必须设置为2^n
          • -XX:MaxGCPauseMillis=time——暂停目标设置,默认200ms
        • 回收过程

          • 新生代回收Young Collection
            • 新创建的对象被放入伊甸园分区中
            • 达到伊甸园分区占比,触发新生代回收
            • 新生代回收,STW,伊甸园对象被复制整理进幸存区
            • 幸存区达到晋升条件的复制进老年区
            • 新生代回收的跨域引用(确定GC ROOT)——老年代引用新生代
              • 老年代被分为512K的卡表
              • 引用了新生代对象的表被标记为脏表
              • 查找GC Root的时候,从脏卡列表remenbered Set中查询,提高速速
              • 每次引用变更的时候,通过post-write barrier+dirty card queue异步进行更新
          • 新生代回收Young Collection+并发标记Consurrent Collection
            • Young GC的时候会进行GC Root的初始标记
            • 当老年代区数量达到一定阈值比例,触发并发标记(不会STW)
            • -XX: InitiatingHeapOccupancyPercent=percent(默认45%)
          • 混合回收Mixed Collection
            • 在这个过程中会对E、S、O进行全面垃圾回收
            • 1.最终标记(发生STW)
            • 2.拷贝存活(发生STW)
            • 对老年代区域进行回收的时候,只会选取部分回收价值高的老年区进行回收(为了达到-XX:MaxGCPauseMi11i5=ms这一目标 )
  • full GC

    • SerialGC
      • 新生代内存不足发生的垃圾收集-minorgc
      • 老年代内存不足发生的垃圾收集-fiullgc
    • ParallelGC
      • 新生代内存不足发生的垃圾收集-minorgc
      • 老年代内存不足发生的垃圾收集-fullgc
    • CMS
      • 新生代内存不足发生的垃圾收集-minor gc
      • 老年代内存不足
        • 回收速度>用户线程产生垃圾的速度——并发垃圾收集,有STW,但很短,称为minor gc
        • 回收速度<用户线程产生垃圾的速度——退化为串行收集,有STW,很慢,称为Full Gc
        • 空间碎片太多——Full GC
    • G1
      • 新生代内存不足发生的垃圾收集-minorgc
      • 老年代内存不足
        • 回收速度>用户线程产生垃圾的速度——并发垃圾收集,有STW,但很短,称为minor gc
        • 回收速度<用户线程产生垃圾的速度——退化为串行收集,有STW,很慢,称为Full Gc
  • 标记流程

    • 从GC Rootk开始处理对象,检查对象是否被请引用,如果没有被引用,则会被标记为垃圾
    • 在并发标记中,假设C对象已经被标记为垃圾,标记完成,但被另一个对象引用了,那么此时仍然回收C不合理
    • 解决方案:当被标记的对象的 引用变更
      • 1.触发写屏障指令pre-write barrier:将C加入队列satb_queue,将C标记为未未处理完成
      • 2.最终/重新标记:STW,重新检查队列,如果为强应用被标记为强应用——不回收
  • G1优化

    • JDK8u20字符串去重
      • 优点:节省了大量内存
      • 缺点:略微多占用了CPU时间,新生代回收时间略微增加
      • -XX:+UseStringDeduplication
      • 工作流程
        • String s1 = new String("hello")底层一个char[]{}数组,s1->String->char[]
        • 将所有新分配的字符串放入一个队列
        • 当新生代回收的时候,G1并发检查是否有字符串重复,如果值是一样的,String对象将引用同一个char[]
        • 注意:与String.intern()不一样
          • Stirng.intern()关注的是字符串对象
          • 而字符串去重关注的是char[]
          • 在JVM内部,使用了不用的字符串表
          • 去重后,虽然指向了同一个char[],但是变量指向的是String对象,不是直接指向char[],即不同变量指向不同的String对象,String对象指向相同的char[]
          • 所以Stirng.intern关注的是String对象,字符串去重关注的是字符串对象引用的char[]数组,纬度不同
    • JDK 8u40 并发标记类卸载
      • 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
      • -XX: +ClassUnloadingWithConcurrentMark ——默认启用
    • JDK 8u60 回收巨型对象
      • 如果一个对象大于一个region一般的时候,就被称为巨型对象在这里插入图片描述

      • G1不会对巨型对象进行拷贝——拷贝用时太大

      • 回收时被优先考虑

      • G1会更总老年代所有的incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代回收时处理掉

      • 老年代没有对象引用巨型对象,巨型对象就会在新生代被回收

    • JDK9并发标记起始时间的调
      • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
      • JDK 9之前需要使用-XX:InitiatingHeapoccupancyPercent
      • JDK9可以动态调整
        • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
        • 进行数据采样并动态调整
        • 总会添加一个安全的空档空
    • JDK9更高效的回收
      • 250+增强
      • 180+bug修复
      • https://docs.oracle.com/en/java/javase/12/gctuning
垃圾回收调优
  • 调优领域
    • 内存
    • 锁竞争
    • cpu占用
    • io
  • 调优目标
    • 低延迟还是高吞吐量,选择合适的回收器
      • CMS、G1,ZGC——低延迟——互联网
      • ParallelGC——高吞吐量——科学计算等
  • 最快的GC是不发生GC
    • 查看FUllGC前后的内存占用,考虑以下的问题
      • 数据是不是太多?——如大量sql中的数据加载到java内存再筛选
    • 数据表示是不是太臃肿?
      • 对象图——用到什么就查询什么
      • 对象大小
    • 是否存在内存泄漏?
      • static map不断放入对象
      • 解决:软弱引用、第三方缓存实现
  • 新生代调优
    • 新生代的特点
      • 所有new操作的内存分配非常廉价(伊甸园中)
        • TLAB thread-local allocation buffer——每个线程都会在伊甸园中分配一个私有区域TLAB(线程局部分配缓冲区)
      • 死亡对象的回收代价是零
      • 大部分对象用过即死——新生代回收的时候,大部分对象都是死的(所以minorGC的标记时间<复制时间———少量复制时间短)
      • Minior GC的时间远远低于Full GC
    • 新生代调优
      • -Xmn不是越大越好
        • 新生代太大,老年代越小,会引发老年代触发Full GC
        • 建议25%~50%
        • 并发量*(请求-响应)
      • 幸存区达到能够保留当前活跃对象+需要晋升对象
        • [幸存区大小]如果幸存区不够,JVM可能会提前晋升对象到老年代——需要Full GC才能清除
        • [晋升阈值]同时也需要合理配置晋升阈值,让长时间存活的对象尽快晋升(否则minor GC会不断复制对象,本来该过程最占时间的就是复制对象)
          • -XX:MaxTenuringThreshold=threshold
          • -XX:+PrintTenuringDistribution
    • 老年代调优CMS为例
      • 并发标记清除:由于是并发清除,所以需要预留空间给浮动垃圾,一旦清理速度<垃圾产生速度,会触发STW,退化为串行清理FUll GC
      • [老年代大小]越大越好
      • 先尝试不做调优,如果没有full GC就不用,都则先尝试调优新生代
      • [流出更多的空间给浮动垃圾——避免Full GC]观察full GC时老年代内存的占用,将老年代内存预设调大1/4~1/3
        • -XX:CMSInitaitingOccupancyFraction=percent——老年代占多少的时候进行回收(75%~80%)预留一定空间给浮动垃圾

类文件.class

类文件结构

  • 查看二进制.class文件:od -t xC Demo1.class

在这里插入图片描述

  • 文件信息
    • magic
      • 0~3字节
      • 类文件类型
    • minion_version
      • 2字节
      • 小版本
    • major_version
      • 两字节
      • JDK版本信息
    • constant_pool_cout
      • 两字节
      • 常量池项数:注意#0不计入,只计入从#1开始
    • constant_pool
      • 常量池信息
    • access_flags
      • 2字节
      • 访问标识符
    • this_class
      • 2字节
      • 这个类的信息
    • super_class
      • 2字节
      • 父类信息
    • interfaces_count
      • 2字节
      • 接口数量
    • interfaces
      • 2字节
      • 接口
    • fileds_count
      • 2字节
      • 成员变量数量
    • fileds
      • 成员连变量信息
    • method_count
      • 2字节
      • 方法数量
    • methods
      • 方法信息
    • sttributes_count
      • 2 字节
      • 类额外属性数量
    • sttributes
      • 类的额外信息

字节码文件可视化

  • javap——可视化字节码文件javap -v xxx.class

代码执行过程

  • 1.常量池载入运行时常量池(类文件的常量池->方法区中的运行时常量池)
  • 2.方法字节码载入方法区
    • 简单的数据直接存放在方法区,大的数据存放在运行时常量池
  • 3.main方法开始运行,分配栈帧内存(局部变量表locals大小+操作数栈stack大小)
  • 4.执行引擎执行代码

常用指令

局部变量
  • 局部变量表使用istore_x,x为槽位号——从操作数栈中弹出放入指定槽位
  • push从方法区/运行时常量池将数押入操作数栈
  • iload从槽位加载数押入操作数栈
运算
  • 先将需要的操作数押入操作数栈
  • 执行引擎从操作数栈取出操作数,进行运算,运算结果入栈
自增运算
  • iinc,在槽位上进行自增

  • i++iload再在槽位自增iinc——栈中不影响

  • ++i先iinc 再iload

  • x=x++/++x都是先运算右边的,最后使用操作数栈中的数赋值,所以注意时先自增还是先load

  • 以上为非静态变量的自增,静态变量的自增为:

    getstatic		i     //获取静态变量i的值
    iconst_1					//准备常量1
    iadd							//执行加操作(操作数栈获取)
    putstatic					//栈顶元素放入静态变量槽位
    
条件判断
  • 注:byte、short、char都会按int比较,因为操作数栈都是4字节
  • goto用来进行挑战到指定行号的字节码
循环控制指令
  • 判断+goto实现
构造方法
  • <cinit>()V——类的构造方法
    • static静态代码块、静态成员变量赋值代码,合并为特殊的方法<cinit>()V
    • 从上至下的顺序
  • <init>()V——实例对象的构造方法
    • 收集所有的初始化代码块{}、成员变量赋值代码块、构造方法代码,形成新的构造方法 (原始构造方法内的代码总是在最后面)
    • 从上至下的顺序、构造方法代码在最后
方法的调用
  • 调用方法的指令类型
    • invokespecial——构造方法、私有方法、final方法(类唯一确定)
    • invokevirtual——普通publiv方法(不唯一,动态绑定,可以重写)
    • invokestatic——静态方法(类唯一确定)
  • 方法调用过程
    • new过程——可能出现指令重排
      • new:堆中分配中间
      • dup:复制到操作数栈(两份)
      • invokespecial:出栈调用构造方法
      • astore:出栈存储到局部变量表
    • invokespecialinvokevirtual过程
      • 对象入栈
      • 对象出栈调用方法
    • invokestatic过程
      • 通过对象调用静态方法
        • 对象入栈
        • 对象出栈
        • 调用方法invokestatic
        • 静态方法调用和对象无关,前两步指令多余
      • 通过类调用静态方法
        • 调用方法invokestatic
invokevirtual多态调用原理(确定应该调用哪个方法,方法的地址?)
  • 当执行invokevirtual指令的时候:
  • 1.先通过栈帧中的对象引用找到对象
  • 2.分析对象头,找到对象的实际class
  • 3.Class结构中有vtable(虚方法表:记录了虚方法的入口地址——该带调用哪个类的方法),在类加载的链接阶段就已经根据方法的重写规则生成好了
    • 连接过程:将.class文件加载到内存中,并创建对应class类
  • 4.查表过的方法的具体地址
  • 5.执行方法的字节码
异常处理try-catch
  • 多了一个异常表

    • 监控[from,to)的代码块,如果发生异常,和type匹配,匹配成功执行target代码
    • 局部变量表中也会存储异常变量先istore异常变量,在catch代码
    • 如果是多catch结构,局部变量表也只会有一个槽位(只可能发生一种catch分支)
    • 示例
    public static void main(String[] args) {
            int i = false;
    
            try {
                i = true;
            } catch (Exception var3) {
                i = true;
            }
    
        }
    
     Code:
          stack=1, locals=3, args_size=1
             0: iconst_0
             1: istore_1
             2: bipush        10
             4: istore_1
             5: goto          12
             8: astore_2
             9: bipush        20
            11: istore_1
            12: return
          Exception table:
             from    to  target type
                 2     5     8   Class java/lang/Exception
          LineNumberTable:
            line 13: 0
            line 15: 2
            line 18: 5
            line 16: 8
            line 17: 9
            line 19: 12
          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
    
    
fianlly
  • finally中的指令分别复制到try块中和catch块中(末尾)

  • 还包括一个未捕获的异常(不匹配)的异常代码块在最后,finally代码块也会复制到此处

  • 无论正常执行、发生匹配的异常或者发生了未捕获的异常都会执行finally

  • 面试题

    • finallycatch中出现return——无论发生异常还是不发生异常,最终都会ireturnfinally中的数据
    • finally中修改需要返回的值,tryreturn返回值——return i会执行以下操作
      • 1.iload_x 将返回值入栈
      • 2.istore 将返回值存入一个新的局部变量槽位固定返回值,防止后续修改
      • 3.执行finally代码块
      • 4.将上面istore的值iload
      • 5.ireturn 栈顶元素
  • 示例

    • java代码

      public static void main(String[] args) {
              int i = 0;
              try {
                  i = 10;
              }catch(Exception e){
                  i = 20;
              }finally{
                  i = 30;
              }
          }
      
    • 指令

       Code:
            stack=1, locals=4, args_size=1
               0: iconst_0
               1: istore_1
               2: bipush        10
               4: istore_1
               5: bipush        30    //finally
               7: istore_1
               8: goto          27
               
              11: astore_2
              12: bipush        20
              14: istore_1
              15: bipush        30    //finally
              17: istore_1
              18: goto          27
              
              21: astore_3						//发生其他未匹配异常
              22: bipush        30		//finally代码块
              24: istore_1						
              25: aload_3							//加载其他异常到操作数栈
              26: athrow							//抛出异常
              27: return
       Exception table:
               from    to  target type
                   2     5    11   Class java/lang/Exception
                   2     5    21   any
                  11    15    21   any
            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
      
      
      
synchronized代码块
  • 流程

    • 1.锁对象入栈
    • 2.对象复制(一个给monitor Enter使用,一个给Monitorexit使用)
    • 3.存储最上面的锁对象到新槽位
    • 4.monitorenter消耗剩下的一个锁对象
    • 5.执行业务代码
    • 6.加载存储的锁对象
    • 7.monitorexit使用对象结果
    • 无论业务代码是否异常,或者6、7这两部发生异常,都会重复6.7这两步进行解锁(6.7实际就是finally一样的逻辑都是复制指令,只是会对6.7进行异常监控)。
  • 示例

    • java代码

      public static void main(String[] args) {
            Object o = new Object();
            synchronized (o){
                System.out.println("ok");
            }
        }
      
    • 指令

      Code:
            stack=2, locals=4, args_size=1
            //=========创建对象
               0: new           #2                  // class java/lang/Object
               3: dup
               4: invokespecial #1                  // Method java/lang/Object."<init>":()V
               7: astore_1
            //=============sychronized
               8: aload_1									//锁对象入栈(synchronized开始)
               9: dup        							//多对象复制
              10: astore_2								//出栈,分配新的槽位(后续monitorexit使用)
              11: monitorenter						//监控lock,moniterenter使用剩下的锁对象
              //-------业务代码
              12: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
              15: ldc           #13                 // String ok
              17: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
              //--------业务代码结束
              20: aload_2							//锁对象取出
              21: monitorexit					//退出监控
              22: goto          30    //跳转到30行结束
              //=======业务代码异常的话执行
              25: astore_3
              26: aload_2						//取出锁对象
              27: monitorexit				//退出监控
              28: aload_3						//加载异常对象
              29: athrow						//抛出
              30: return
            Exception table:
               from    to  target type
                  12    22    25   any
                  25    28    25   any      //如果发生异常,解锁阶段还是异常,重新解锁
            LocalVariableTable:
              Start  Length  Slot  Name   Signature
                  0      31     0  args   [Ljava/lang/String;
                  8      23     1     o   Ljava/lang/Object;
      

编译期处理(语法糖)

编译器会在编译的时候会自动生成一些代码

默认构造器
  • 当自定义的类中不存在任何构造器,java编译之后的.class会自动生成一个默认的无参构造器

  • 示例

    • java代码

      public class DefaultConstruct {
      }
      
    • 编译器生成的代码

      public class DefaultConstruct {
          public DefaultConstruct() {    //编译自动生成的默认无参构造器
          }
      }
      
自动拆装箱
  • 在使用包装类和基本类型自动转换的时候,编译器会自动添加拆装箱的代码

  • 注:Integer在-127~128的时候会复用对象,超出这个返回才会new创建

  • 示例

    • java代码

      public static void main(String[] args) {
              Integer i = 1;
              int y = i;
          }
      
    • 编译器生成的字节码反编译

      public static void main(String[] var0) {
              Integer i = Integer.valueOf(1);
              int y = i.intValue();
          }
      
      public static void main(String[] var0) {
              Integer var1 = 1;
              int var2 = var1;
          }
      
范型擦除
  • 在编译的时候,会将范型擦除,全部当作Object类型进行编译在编译的时候不关注范型的类型,统一当作Object类

  • 在取范型的时候会经历两个阶段

    • 1.当作Object取范型对象
    • 2.使用checkcast强制转换为范型类型
  • 擦除的是字节码code上的范型信息,但多出了一张表LocalVariableTypeTable,signiture保存了范型的具体类型

  • 通过反射只能获取返回值类型、参数类型的范型类型,而局部变量的范型类型无法通过反射获取

  • 示例

    • java代码

      public static void main(String[] args) {
              ArrayList<Integer> list = new ArrayList<>();
              list.add(3);
              Integer i = list.get(0);
          }
      
    • 反编译代码

      public static void main(String[] var0) {
              ArrayList var1 = new ArrayList();    //没有范型,统一当作var类型(Object)
              var1.add(3);
              Integer var2 = (Integer)var1.get(0);  //取范型对象使用强制转换
          }
      
    • 指令

      Code:
            stack=2, locals=3, args_size=1
               0: new           #7                  // class java/util/ArrayList
               3: dup
               4: invokespecial #9                  // Method java/util/ArrayList."<init>":()V
               7: astore_1
               8: aload_1
               9: iconst_3
              10: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
              13: invokevirtual #16                 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z   ——范型擦除,当作Object类
              16: pop
              17: aload_1
              18: iconst_0
              19: invokevirtual #20                 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;					//取对象,还是当作Object类
              //然后强制转换
              22: checkcast     #11                 // class java/lang/Integer
              25: astore_2
              26: return
      
可变参数
  • 方法的传入参数为String... strs,编译之后实际为一个String[]

  • 如果不传参数,相当于传入了一个空对象new String[]{},而不是null

  • 示例

    • java代码

      public static void foo(String... strs){
              String[] str = strs;
              System.out.println(Arrays.toString(str));
      
          }
      
          public static void main(String[] args) {
              foo("hello","world");
              foo();
          }
      
    • 反编译

      public static void foo(String[] strs){
              String[] str = strs;
              System.out.println(Arrays.toString(str));
      
          }
          public static void main(String[] args) {
              foo(new String[]{"hello","world"});
              foo(new String[]{});
          }
      
for-each循环
  • 普通数组遍历

    • foreach底层还是使用的是for-i

    • 示例

      • java代码

        public static void main(String[] args) {
                int[] nums = {1,2,3,4,5,6,7,8};    //1.创建编译后还是使用new
                for (int num:nums){    					//2.for-each反编译还是使用for-i
                    System.out.println(num)