java virtual machine 笔记 (Notion 导出)

JVM

  • 虚拟机参数

    • -XX:+<option> —表示开启option选项
    • -XX:-<option> —表示关闭option选项
    • -XX:<option>= —表示将option选项的值设置为value
    • -XX:+TraceClassLoading —用于追踪类的加载信息并打印出来
    • -XX:+TraceClassUnloading —用于追踪类的卸载信息并打印出来
    • -XX:+HeapDumpOnOutOfMemoryError —内存溢出时打印出堆转储文件
    • -Xms20m —设置堆空间的初始容量为20m
    • -Xmx20m —设置堆空间的最大容量为20m
    • -Xmn10m —设置新生代的容量为10m
    • -verbose:gc —输出垃圾回收的日志
    • -XX:+PrintGCDetails —打印垃圾回收的详细信息
    • -XX:+PrintGCDateStamps —
    • -XX:SurvivorRatio=8 —Eden和Survivor的比例
    • -XX:TargetSurvivorRatio=60 —当Survivor空间中的对象占据的空间超过60%,会重新计算阈值
    • -XX:PretenureSizeThreshold=4194304 —对象大小超过阈值直接在老年代进行分配,搭配SerialGC才能起作用
    • -XX:+UseSerialGC —使用SerialGC
    • -XX:MaxTenuringThreashold=5 —设置晋升到老年代的最大的存活年龄,默认值为15,CMS中默认为6,G1中默认为15
    • -XX:+PrintTenuringDistribution —打印对象的年龄
    • -XX:MaxGCPauseMillis=x —设置启动应用程序暂停的时间
    • -XX:+UseG1GC —使用G1 GC
  • Java内存区域与内存溢出异常

    • 运行时数据区域
      • 程序计数器(Program Counter Register)
        • 线程私有,是程序控制流的指示器,保证线程正常切换。
      • Java虚拟机栈
        • 线程私有,声明周期和线程同步。
        • 栈的元素是栈帧,栈帧包含局部变量表、操作数栈、动态连接、返回地址等。
        • 每个方法执行的时候会创建一个帧。
        • 局部变量表存放了编译器可知的基本数据类型和对象引用。以变量槽为最小单位。
      • 本地方法栈
        • 与虚拟机栈类似,本地方法栈为Native方法服务。
      • Java堆
        • 所有线程共享的内存区域。在虚拟机启动时创建。
        • 堆是垃圾收集器管理的内存区域,现代几乎所有的垃圾收集器都是采用的分代收集算法,堆也基于这一点进行了划分。
          • 新生代和老年代、Eden空间、From Survivor空间和To Survivor空间
        • Java堆可以处于物理上不连续的内存空间中,但在逻辑上是连续的。
      • 方法区
        • 所有线程共享的内存区域。
        • 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
        • 使用元空间(Meta space)
      • 运行时常量池
        • 是方法区的一部分
        • Class文件中的常量池表(用于存放编译器生成的各种字面量和符号引用)在类加载后存放在方法区的运行时常量池
        • 由符号引用翻译出来的直接引用也存储在运行时常量池中
      • 直接内存
        • 不是虚拟机运行时数据区的一部分
        • JVM通过DirectByteBuffer对象作为这块内存的引用进行操作
    • Java对象创建的过程
      • new关键字创建对象的步骤
        • 在堆内存中创建出对象的实例
        • 为对象的实例成员变量赋初值
        • 将对象的引用返回
      • 创建对象时堆内存的分配方式
        • 堆内存是绝对规整的,使用一个指针作为分界点的指示器,分配内存只需要移动指针即可,这种分配方式称为“指针碰撞”。
        • 堆内存是不规整的,维护一个列表,记录哪些内存块是可用的,在分配时划分一块足够大的未被使用的内存,同时更新列表的记录。这种分配方式称为”空闲列表“。
        • Java堆是否规整由采用的垃圾收集器是否带有空间压缩整理的能力决定。
      • 对象的内存布局
        • 对象头(存储对象自身的运行时数据、对象指向它的类型元数据的指针)
        • 实例数据(类中声明的成员变量的信息)
        • 对齐填充(可选)
      • 引用访问对象的方式
        • 使用句柄访问。Java堆中会划分一块内存作为句柄池,句柄包括到对象实例数据的指针和到对象类型数据的指针
          • 指针是对象在内存中的地址
          • 引用时对象的别名,是功能受限但更安全的指针
          • 句柄是指针的指针
        • 使用直接指针访问,直接指向堆中的对象实例数据
    • OutOfMemoryError
      • 使用jvisualvm查看堆转储文件
      • Java堆溢出
        • 不断的创建对象,总容量触及最大堆容量限制时产生内存溢出异常。
        • 添加参数-XX:+HeapDumpOnOutOfMemoryError打印出堆转储文件
      • 虚拟机栈和本地方法栈溢出
        • 线程请求的栈深度大于虚拟机所玉奴徐的最大深度,抛出StackOverflowError
        • 如果虚拟机的栈内润允许动态扩展,扩展栈容量无法申请到足够的内存时抛出OutOdMemoryError
      • 方法区和运行时常量池溢出
      • 本机直接内存溢出
  • 垃圾收集器与内存分配策略

    • 判断对象是否存活
      • 引用计数器(Reference Counting)算法
        • 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时,计数器值减一。
        • 任何时刻计数器为0的对象就是不能再被使用的。
        • 需要考虑很多例外情况,如对象之间相互循环引用的问题。
      • 根搜索(Roots Tracing)算法
        • 主流的语言(Java、C#等)都使用跟搜索算法判定对象是否存活
        • 通过一系列的”GC Roots“的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径为”引用链“(Reference Chain),如果某个对象到GC Roots间没有任何的引用链相连,则证明此对象时不可能再被使用的。
        • 固定可作为GC Roots的对象
          • 在虚拟机栈(栈帧中的本地变量表)中引用的对象
          • 在方法区中类静态属性引用的对象
          • 在方法区中常量引用的对象
          • 本地方法栈中JNI引用的对象
          • Java虚拟机内部的引用
          • 被同步锁持有的对象
        • 除了固定的GC Roots集合,还可以有其他对象临时性的加入GC Roots集合。
      • 引用的概念
        • 强引用:指程序代码中普遍存在的引用赋值
        • 软引用:描述一些还有用但非必须的对象。如果将要发生内存溢出异常,会把这些对象列入垃圾回收的范围进行二次回收。
        • 弱引用:非必须的对象,强度比软引用更弱。这些对象只能生存到下一次垃圾收集发生为止。
        • 虚引用:为了在对象被收集器回收时收到一个系统通知。虚引用不影响对象的生存时间,也无法通过虚引用取得对象实例。
      • finialize()方法
        • 当对象变成(GC Roots)不可达时,GC会先判断该对象是否覆盖了finalize()方法,如果没有,则直接回收。如果覆盖了finalize()方法但是已经被JVM调用过了,也直接回收。
        • 如果没有finalize()方法还没被调用过,将其放入F-Queue队列,由一条虚拟机自动建立的、低调度优先级的Finalizer线程区执行他们的finalize()方法。执行完毕后,GC会再次判断该对象是否可达,如果不可达则进行回收,否则对象复活。
      • 方法区的回收
        • Java虚拟机规范不要求在方法区实现垃圾收集
        • 主要回收废弃的常量和不再使用的类型
        • 废弃常量的回收和Java堆中对象的回收类似
        • 不再使用的类的回收需要满足三个条件
          • 该类的所有实例都已经被GC
          • 加载该类的ClassLoader已经被GC
          • 该类对应的java.lang.Class对象没有在任何地方被引用
    • 垃圾收集算法
      • 分代收集理论(Generational Collecting)
        • 分代假说
          • 弱分代假说:绝大多数对象都是朝生夕灭的
          • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
          • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数
        • 垃圾收集器设计原则:应该将Java堆划分出不同的区域,将回收对象按年龄(熬过垃圾收集过程的次数)分配到不同区域进行存储
        • 一般把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域
        • JVM 6 中共划分三个代:新生代、老年代、永久代
          • 新生代
            • 新生成的对象都放在新生代。新生代用复制算法进行GC
            • 新生代分为三个区。一个Eden区,两个Survivor区。默认比例为8:1:1
            • 对象在Eden区中生成,当Eden区满时,还存活的对象被复制到一个Survivor区
            • 当这个Survivor区满时,此区存活的对象被复制到另外一个Survivor区
            • 当第二个Survivor区也满了时,从第一个Survivor区复制过来的并且此时还存活的对象将被复制到老年代
          • 老年代
            • 存放了经过一次或多次GC还存活的对象
            • 一般采用Mark-Sweep或者Mark-Compact算法进行GC
            • 有多种根据GC算法设计的垃圾收集器
          • 永久代(已废弃)
            • 并不属于堆,但GC也会涉及到
            • 存放每个Class的结构信息,与GC要收集的Java对象关系不大
        • 垃圾收集的时机
          • Scavenge GC(Minor GC)
            • 触发时机:新对象生成,Eden空间满了
            • 理论上Eden区大多数对象会在Scavenge GC回收,复制算法效率很高
          • Full GC
            • 对整个JVM进行整理
            • 触发时机:Old满了、Perm满了、System.gc()
            • 效率很低,尽量减少Full GC
      • 标记清除算法(Mart-Sweep)
        • 分为两个阶段
          • 标记:标记出所有需要回收的对象
          • 清除:回收掉所有被标记的对象
        • 缺点
          • 执行效率不稳定,两个过程的效率随着对象数量的增长而降低
          • 内存空间的碎片化问题,标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致后续使用无法找到足够的连续内存而提前出发另一次垃圾收集
      • 复制算法(Copying)
        • 将可用内存划分成两块,每次只是用其中的一块,当半区内存用完了,就将还存活的对象复制到另一块内存上,然后一次清理掉使用过的半区内存。
        • 缺点
          • 可用内存缩小为了原来的一般,空间浪费太多了
        • 商用JVM采用这种算法回收新生代
        • Appel式回收
          • 把新生代分为一块较大的Eden空间和两块较小的Survivor空间
          • 每次分配内存只是用Eden和其中一块survivor
          • 发生垃圾收集时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor中,然后清理Eden和用过的Survivor
          • 分配担保:当Survivor空间不足以容纳一次GC之后存活的对象时,可以将这些多的对象存入老年代
        • 对象存活率高时效率下降
      • 标记整理算法(Mark-Compact)
        • 针对老年代对象的特征设计(对象存活率高)
        • 标记:标记出所有需要回收的对象
        • 整理:让所有存活的对象向内存空间一段移动,然后清理掉边界外的内存
        • 没有内存碎片,耗费更多时间进行整理
    • HotSpot的算法实现细节
      • 根节点枚举
        • 逐个检查根节点可达的引用耗时太长,且需要暂停用户线程
        • HotSpot使用一组称为OopMap的数据结构来直接得到哪些地方存放着对象的引用
        • 收集器在扫描时就可以直接得知信息,不需要真正的根节点开始查找
      • 安全点
        • OopMap只是在“特定的位置”记录了这些信息,这些位置称为安全点(SafePoint)。
        • 太少会导致GC等待时间过长,太多导致增加内存负荷
        • 指令序列的复用会导致程序“长时间执行”,如方法调用、循环跳转、异常跳转等。具有这些功能的指令才会产生安全点
        • 在GC发生时,要让所有线程都跑到最近的安全点,然后停顿下来。
          • 抢占式中断
            • 先中断全部线程,如果有线程不在安全点上,就回复这条线程的执行
          • 主动式中断
            • 不直接对线程操作,而是设置一个标志位,线程执行时不停的主动轮询标志,一旦中断的标志为真,就主动在最近的安全点中断挂起
            • 轮询标志的地方和安全点是重合的,再加上创建对象需要分配内存的地方
      • 安全区域
        • 安全点机制保证了程序执行时,在不太长的时间就能进入GC的安全点
        • 程序不执行时,线程无法响应JVM的中断请求,通过安全区域来解决。
        • 安全区域是只能够确保在某一段代码片段之中,引用关系不会发生变化,因此在这个区域的任何地方开始GC都是安全的。
        • 如果线程执行到安全区域的代码时,先标识自己进入了Safe Region,这是JVM发起GC就不用管Safe Region中的线程。
        • 在线程离开安全区域时,需要先检查是否完成了整个GC过程,如果没有就一直等待收到可以安全离开Safe Region的信号
    • 垃圾收集器
      • 并行(Parallel):多个收集器的线程同时工作,到那时用户线程处于等待状态
      • 并发(Concurrent):收集器在工作的同时,可以允许用户线程工作。并发并不代表解决了GC停顿的问题,在关键步骤还是要停顿。
      • 吞吐量
        • 在一个指定的时间内,最大化一个应用的工作量
        • 衡量方式:
          • 在一个小时内同一个事物(或任务、请求)完成的次数(tps)
          • 数据库一小时可以完成多少次查询
        • 对关注吞吐量的系统,卡顿时可以接受的
      • 响应能力
        • 一个程序或系统对请求是否能够及时响应
          • 桌面UI响应事件的时间
          • 网站返回页面请求的时间
          • 数据库返回查询数据的时间
        • 对响应能力敏感的场景,长时间的停顿是难以接受的
      • Serial收集器
        • 单线程收集器,收集时会暂停所有工作线程(Stop The World),使用复制算法
        • 在新生代采用复制算法,在老年代采用标记整理算法。
        • 简单而高效
        • 虚拟机运行在Client模式的默认的新生代收集器
      • ParNew收集器
        • 是Serial的多线程版本
        • 虚拟机运行在Server模式的默认新生代收集器
        • 只有多CPU时效率比Serial高
      • Parallel Scavenge收集器
        • 也是多线程收集器,也是使用复制算法
        • 其他收集器尽可能的缩短垃圾收集时用户线程的停顿时间不同
        • Parallel Scavenge收集器的目标是达到可控制的吞吐量(Throughput)
        • 吞吐量越大,GC时间占总运行时间最小
      • Serial Old收集器
        • Serial收集器的老年代版本
        • 单线程收集器,使用标记整理算法
        • 供Client模式下的HotSpot虚拟机使用
      • Parallel Old收集器
        • Parallel Scacenge收集器的老年代版本
        • 多线程收集器,使用标记整理算法
        • 注重吞吐量
      • Concurrent Mark-Sweep(CMS)收集器
        • 以最短停顿时间为目标的收集器
        • 只针对老年区,一般结合ParNew使用
        • GC线程和用户线程尽量并发工作
        • 使用标记清除算法
          • 初始标记(Stop The World)
            • 标记GC Roots直接关联到的对象,速度快
          • 并发标记
            • 并发的遍历整个对象图
          • 并发预清理(Concurrent Preclean)
            • 标记并发过程中,引用发生变化之后的,新的可到达的对象
          • 并发可丢弃的预清理(Concurrent Abortable Preclean)
            • 并发的尽量承担(STW)最终标记的工作
          • 重新标记(Stop The World)
            • 修正并发标记期间用户执行导致标记变动的那一部分对象的记录,时间稍长
          • 并发清除
            • 清理删除标记判断死亡的线程
          • 并发重置
            • 重置CMS内部的数据结构
          • 时间长的并发标记和并发清除过程都是可并发的,因此CMS的内存回收可以看作和用户线程一起执行
        • 缺点
          • 对CPU资源非常敏感。以牺牲CPU资源的代价减少用户线程的停顿。CPU个数小于4时可能对吞吐量影响非常大
          • 无法处理浮动垃圾,在并发阶段用户西安测绘给你会产生新的垃圾,这些垃圾无法当次处理,。因此需要预留空间给用户线程使用
          • 会产生大量空间碎片,碎片过多容易频繁触发Full GC
        • 空间分配担保
          • 在Minor GC之前,虚拟机会先检查老年代的最大可用连续空间是否大于新生代所有对象总空间。当大量对象在Minor GC之后仍然存活,就需要老年代进行空间分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代判断剩余空间不足,就会进行一次Full GC。
        • RSet记录老年代指向新生代的引用,扫描时只扫RSet
      • Grabage First收集器
        • 面向服务端的垃圾收集器,适用于多核处理器、大内存容量的服务器系统
        • 缩短gc停顿时间的同时达到较高的吞吐量
        • JDK7以上版本
        • 与应用线程同时工作,几乎不需要STW,整理剩余空间,不产生内存碎片,GC停顿更可控,不牺牲系统的吞吐量,GC不要求额外的内存空间
        • 实现细节
          • 将整个堆划分成多个大小相等的独立区域(Region),每个Region可以根据需要扮演Eden、Survivor、老年代空间的角色。
          • 有特殊的Humongous区域专门存放大对象,大小超过一个Region一般的对象,G1的大多数行为会将Humongous区域当老年代的一部分看待
          • 对每个角色的数量没有限定,每种分代的内存大小可以动态变化
          • 代成为逻辑上的概念,方便复用逻辑
          • G1使用GC停顿可预测的模型,根据设定的停顿时间,会自动的选择清除哪些Region,一次清除多少个Region
          • 从多个Region中复制存活的对象,然后集中放入一个Region中,同时整理、清除内存(复制算法)
          • G1会跟踪各个Region中垃圾的多少,回收得到的空间和回收所需时间,然后在后台维护一个优先级队列,每次GC优先处理垃圾最多收益最高的Region
          • 在新生代满了的时候对整个新生代进行回收,在回收老年代的分区时,会将存活的对象从一个分区拷贝到另一个可用分区
          • Collection Set(CSet):一组可被回收的分区的集合,在CSet中的存活数据会在GC过程中被激动到另一个可用分区
          • Remembered Set(RSet):记录其他Region中的对象引用本Region中对象的关系,RSet其实是一个hash tale,key是别的region的起始地址,value是一个集合,元素是card table的index。
          • Snapshot-At-The-Beginning(SATB):G1 GC在并发标记阶段使用的增量式的标记算法,并发标记是并发多线程的,并发线程同一时刻只扫描一个分区
        • G1相对于CMS的优势
          • G1从整体看是基于标记整理算法实现的,局部上看是基于标记复制的,不会产生内存碎片。
          • G1可以指定最大停顿时间
          • G1可以在Young GC中使用
        • Young GC
          • CSet选定所有年轻代里的Region,通过控制年轻代Region的个数来控制Young GC的时间开销
          • 在Eden充满时触发,对整个新生代进行回收,在回收之后所有之气那属于Eden的区块全部变成空白,不属于任何一个分区
          • 采用暂停所有应用线程的方式,将存活对象拷贝到老年代或Survivor空间
          • Eden空间移动到Survivor空间,Survivor区的数据移动到新的Survivor中,部分晋升到老年代。
          • 复制算法
          • 根扫描、更新RSet、处理RSet、对象拷贝、处理引用队列
        • Mixed GC
          • CSet选定所有年轻代里的Region,加上根据global concurrent marking统计得出收集收益最高的若干老年代Region。
          • global concurrent marking的执行过程类似于CMS,但是在G1 GC中,它主要的作用是为Mixed GC提供标记服务,不是一次GC过程的必须环节
            • 初始标记(STW):标记从GC Root开始直接可达的对象。公用Young GC的暂停,是伴随Young GC发生的。
            • 并发标记
            • 最终标记(STW):标记在并发阶段发生变化的对象
            • 清理:清除空Region
          • 全局并发标记之后,拷贝存活对象
          • 将对象从一个区域复制到另一个区域就完成了清理工作
          • 由参数控制触发
          • G1HeapWastePercent:每次Young GC之后和再次Mixed GC之前,检查垃圾占比是否达到此参数,如果达到,下次发生Mixed GC
          • G1MixedGCLiveThreasholdPercent:老年代region中存活对象的占比,没达到的region会加入CSet。
          • G1MixedGCCountTarger:一次global concurrent marking之后,最多执行Mixed GC的次数
          • G1OldCSetRegionThreasholdPercent:一次Mixed GC中能选入CSet的最多的老年代region的数量
        • 三色标记算法
          • 黑色:根对象,或者该对象与它在子对象都被扫描过了
          • 灰色:对象本身被扫描了,但还没扫描完该对象的子对象
          • 白色:未被扫描的对象,扫描完成所有对象之后,最终为白色的为不可达的对象
          • 可能会漏标,未扫描的队友在并发时又由扫描过的黑色对象引用了,可达但是白色。
          • Snapshot At The Beginning(SATB)解决漏标的问题,删除的时候记录所有的对象
            • 在开始标记的时候生成一个快照图,标记存活对象
            • 在并发标记的时候,所有被改变的对象入队
            • 可能存在浮动垃圾,在下次被收集
          • SATB对从灰色对象移除的目标引用对象标记为灰色,由黑色引用的新产生的对象标记为黑色
        • 在Young GC和Mixed GC之间不断切换运行,同时定期的做并发标记
        • 设置暂停时间太短会导致跟不上垃圾产生的速度,最终退化成Full GC。
        • 不要设置新生代和老年代大小
        • 拷贝失败(Evacuation Failure)类似CMS的晋升失败,堆空间垃圾太多,退化成Full
      • Full GC是采用(serial GC)来收集整个GC heap。
    • Java内存泄漏的经典原因
      • 对象定义在错误的范围
        • 只被一个方法使用的变量定义直接定义在类中
      • 异常处理不当
        • 资源关闭需要放在finally中
      • 集合数据管理不当
        • 使用基于数组的结构,尽量创建时确定容量,减少resize可以避免没有必要的array copying
    • 经历多次GC之后,存活对象会在From Survivor到To Survivor之间来回存放,前提是由足够的空间存放。GC算法会计算每个对象的年龄大小,如果达到某个年龄的对象的总大小超过了一个Survivor空间的50%,就不再等待到默认的15次GC再晋升,所以会自动动态调整阈值让存活对象尽快晋升。如果之后的对象占用的空间没有达到50%,会再次调整阈值。
  • 类文件结构

    IDEA插件安装jclasslib就可以方便的查看字节码

    • 常见java字节码助记符

      • getstatic —调用类静态变量
      • putstatic —给类静态变量赋值
      • invokestatic —调用类的静态方法
      • ldc —表示将int,float或String类型的常量值从常量池中推送至栈顶
      • bipush —表示将单字节 (-128 ~ 127) 的常量值推送至栈顶
      • sipush —表示将一个短整型常量值 (-32768 ~ 32767) 推送至栈顶
      • iconst_1 —表示将int类型1推送至栈顶 (iconst_m1 ~ iconst_5 → -1 ~ 5)
      • newarray —表示创建一个基本数据类型类型(的数组,并将其引用值压入栈顶
      • anewarray —表示创建一个引用类型(类、接口、数组)的数组,并将其引用值压入栈顶
    • java字节码整体结构图

      image

    • 字节码的各个结构

      • 魔数(Magic Number):所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE。

      • 版本号(Version):4个字节,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)。

      • 常量池(constant pool):2+n个字节。

        • 常量池中的11中数据类型的结构总表

          image

        • 一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作Class文件的资源仓库。

        • 常量池中主要存储两类常量:字面量和符号引用。

          • 字面量:如文本字符串,Java中声明为final的常量值等
          • 符号引用:如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等
        • 常量池的总体结构:主要由常量数量和常量池数组(常量表)构成。常量数量在主版本号之后,占2个字节。

        • 常量池数组在常量数量之后,常量池数组中不同的元素的类型、结构都是不同的,长度也是不同的。但每一种元素的第一个数据都是一个u1类型,该数据是标志位,占一个字节。JVM在解析常量池时会根据u1类型来获取元素的具体类型。

        • 常量池中元素个数 = 常量池数量 - 1 (为了满足某些常量池索引值的数据在特定情况下需要表达不引用任何一个常量池的含义,索引为0是一个保留常量,不位于常量表中,对应的是null值。)

      • 访问标志(Access Flags):2个字节,包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。

        image

      • 当前类的类名(This Class Name):2个字节,引用指向常量池中的常量。

      • 父类的类名(Super Class Name):2个字节,引用指向常量池中的常量。

      • 接口(Interfaces):2+n个字节。前2个字节interfaces_counts表示接口的个数。

      • 字段表(Fields):2+n个字节。前2个字节fields_counts表示成员变量的个数。

        • 描述类和接口中声明的变量。包含类变量和实例变量。

        • 访问权限、名称索引、描述符索引、属性个数,每一个属性都是一个属性表

          image

      • 方法(Methods):2+n个字节。前2个字节methods_counts表示成员变量的个数。

        • 访问权限、名称索引、描述符索引、属性个数,每一个属性都是一个attribute_info,每一个方法都有一个code属性。

        • 每个非静态的方法都有一个隐藏的局部变量this。

          image

          image

      • 属性表(Attributes):2+n个字节。前2个字节表示属性的个数。

      • code属性的结构

        • Code attribute的作用是保存该方法的结构,如所对应的字节码。

          image

        • attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。

        • max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度

        • max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。

        • code_length表示该方法所包含的字节码的字节数以及具体的指令码。具体字节码即该方法被调用时,虚拟机所执行的字节码。

        • exception_table存放的是处理异常的信息。

          • start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
          • handler_pc表示处理异常的代码的开始处
          • catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时表示处理所有的异常。
      • 附加属性

        • LineNumberTable:表示code数组中的字节码位置和Java代码行数之间的关系。可以用来在调试的时候定位代码执行的行数。

          image

        • 局部变量表

    • JVM规范中,每个变量/字段都有描述信息,描述信息的主要作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。

      • 根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示。(B - byte、C - char、D - double、F - float、I - int、j - long、S - short、Z - boolean、V - void、L - 对象类型如Ljava/lang/String)。
      • 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]记录为[I,String [][]被记录为[[Ljava/lang/String。
      • 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内。
    • 可以使用javap -verbose命令分析一个字节码文件。

    • 非静态变量的初始化赋值操作是在构造方法里面执行的(重排序)。将赋值操作放在了每一个构造方法的最开始。

    • 每个对象都有一个监视器(monitor)。

    • 是对象构造器方法,在new对象时才会执行。是类构造器方法,在jvm进行类加载过程时的初始化阶段会调用,对静态代码进行初始化。

    • 每个非静态的方法都有一个隐藏的局部变量this。这个操作是在编译期间完成的。

    • 字节码处理try-catch异常的方式:方法部分Code属性中采用异常表处理,存在finally语句块时,将finally语句块的字节码拼接到每一个catch块后面。

    • 字节码处理throws异常的方式:方法部分Exception属性处理,和Code属性并列。

  • 虚拟机类加载机制

    • 在以下情况下,Java虚拟机将结束生命周期
      • 执行了System.exit()方法
      • 程序正常执行结束
      • 程序在执行过程中遇到了异常或错误而异常终止
      • 由于操作系统出现错误而导致Java虚拟机进程终止
    • 在Java代码中,类型的生命周期分为五个步骤
      • 加载:把二进制形式的java类型读入java虚拟机中。
        • 类的加载指的是将类的.class文件中的二进制读入到内存中,将其放在运行时数据区的方法区内(JDK1.8的元空间),然后在内存中创建一个java.lang.Class对象用来封装类在方法区内的数据结构。
        • 类的加载的最终产品是存在内存中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
        • 加载.class文件的方式
          • 从本地系统直接加载
          • 通过网络下载.class文件
          • 从zip,jar等归档文件中加载.class文件
          • 从专有数据库中提取.class文件
          • 将Java源文件动态编译为.class文件
      • 连接:将已经读入内存的类的二进制数据合并到虚拟机的运行时环境中。
        • 验证:确保被加载类的正确性。
          • 类文件的结构检查
          • 语义检查
          • 字节码验证
          • 二进制兼容性的验证
        • 准备:为类的静态变量分配内存,并将其初始化为默认值。
        • 解析:在类型的常量池中寻找类、接口、字段和方法的符号引用,将符号引用替换成直接引用。
      • 初始化:为类的静态变量赋予正确的初始值。
        • 假如这个类还没有被加载和连接,那就先进行加载和连接。
        • 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
        • 假如类中存在初始化语句,那就一次执行这些初始化语句。在程序中静态变量的初始化有两种途径,在静态变量的声明处进行初始化,以及在静态代码块中进行初始化。
        • 类的初始化时机
          • Java程序对类的使用方式分为主动使用和被动使用,所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化它们。
          • 主动使用
            • 创建类的实例
            • 访问某个类或接口的静态变量,或者对该静态变量赋值
            • 调用类的静态方法
            • 反射,反射获取类的对象是对类的主动使用
            • 初始化一个类的子类
            • Java虚拟机启动时被标名为启动类的类(main)
            • JDK1.7开始提供的动态语言支持
          • 一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
          • 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。
          • 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
        • 常量(final修饰)在编译阶段会存入到调用这个常量的方法所在的类的常量池中,调用类并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,程序运行时会主动使用这个常量所在的类导致此类被初始化。
      • 使用:程序通过类来创建对象,调用类的方法。
        • 类实例化过程
          • 为新的对象分配内存
          • 为实例变量赋默认值
          • 为实例变量赋正确的初始值
          • java编译器为它编译的每一个类都至少生成一个实例初始化方法,在java的class文件中,这个实例化方法被称为""。针对源代码中每一个类的构造方法,java编译器都产生一个方法。
      • 卸载
        • 当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载。
        • 由Java虚拟机自带的 类加载器所加载的类,在虚拟机的声明周期中,始终不会被卸载。由用户自定义的类加载器所加载的类是可以被卸载的。
        • 调用System.gc()手动卸载声明周期结束的类。
    • 类加载器
      • 类加载器的类型
        • Java虚拟机自带的加载器
          • 根类加载器(Bootstrap)
            • 负责加载虚拟机的核心类库
            • 加载 JRE\lib\rt.jar 或者 -Xbootclasspath 选项指定的Jar包
            • 属于虚拟机实现的一部分,没有继承java.lang.ClassLoader类
          • 扩展类加载器(Extension)
            • 父加载器位根类加载器
            • 加载 JRE\lib\ext*.jar 或 -Djava.ext.dirs 指定目录下的Jar包
            • 是java.lang.ClassLoader类的子类
          • 系统(应用)类加载器(System)
            • 父加载器是扩展类加载器
            • 加载 CLASSPATH 或 -Djava.class.path 所指定的目录下的类和Jar包
            • 是用户自定义的类加载器的默认父加载器
            • 是java.lang.ClassLoader类的子类
        • 用户自定义的类加载器
          • java.lang.ClassLoader的子类
          • 用户可以定制类的加载方式
      • 双亲委托机制
        • 在一个类加载器加载类前会先委托给它的父加载器。每次加载都会一直委托到根加载器。
        • 在ClassLoader类中,定义了ClassLoader类型的parent属性,用来指向父加载器。
        • 可以确保Java核心库的类型安全:所有的Java应用都至少会引用java.lang.Object类,运行期java.lang.Object类就会被加载到Java虚拟机中;如果这个加载过程是由Java应用自己的类加载器完成的,那么JVM中可能存在多个版本的不兼容的java.lang.Object类。
        • 可以确保Java核心类库所提供的类不会被自定义的类所替代。
      • ClassLoader类 java doc阅读
        • 对于数组类的class对象来说,不是由类加载器创建的,是由JVM根据需要在运行期动态创建的,表示为[Lclass形式,动态生成的类型的父类为Object。
        • 通过反射Class.getClassLoader()返回的数组类型的类加载器和数组元素类型的类加载器是一样的。如果元素类型是原始类型(基本数据类型),则这个数组类没有类加载器。
        • ClassLoader类默认是可以并行加载的,但是继承的子类需要register之后才能并行。
        • 自定义类加载器:继承ClassLoader类,重写findClass(String className),返回二进制类名对应的class对象。实现loadClassData()。
        • 自定义类加载器使用时,根据父亲委托机制,会先让父加载器加载。
      • 线程上下文类加载器(Context Classloader)
        • 线程上下文加载器就是当前线程的Current ClassLoader。
        • 类Thread中的getContextClassLoader() 和 setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器。
        • 如果没有通过set方法设置的话,线程将继承父线程的上下文类加载器,Java运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类和资源。
        • 父ClassLoader可以使用当前线程Thread.currentThread().getContextLoader()所指定的classloader加载的类,这就改变了父ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况。
        • 在双亲委托模型下,对于SPI(Service Provider Interface),有些接口是Java核心类库提供的(启动类加载器加载的),而接口的实现来自不同的jar包,导致双亲委托模型无法满足要求。通过给当前线程设置上下文类加载器,就可以实现对接口实现类的加载。
      • 类加载器不需要等到某个类被首次主动使用再加载它。
      • JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件确实或存在错误,类加载去必须在程序首次主动使用该类时才报告错误(LinkageError)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
      • 真正负责加载类的类加载器称为定义类加载器,所有能成功返回Class对象引用的类加载器都称为初始类加载器。
      • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父类加载器所加载的类组成。同一个命名空间中,不会出现类的完整名字相同的两个类。子加载器的命名空间包含所有父加载器的命名空间。
      • 子加载器所加载的类能够访问父加载器加载的类,但父加载器加载的类无法访问子加载器加载的类,因为所属的命名空间不同。
      • 不同的类加载器可以被相同名称的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载即可。不同类加载器所加载的类是不兼容的,相当于在Java虚拟机中创建了一个又一个相互隔离的Java类空间。
      • JVM启动时,启动类加载器(Bootstrap)会加载扩展类加载器和系统类加载器。
      • 启动类加载器不是Java类,是特定于平台的机器指令,负责开启整个加载过程。
      • 内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,以及java.util与java.lang包中的类等等。
      • 调用getSystemClassLoader()方法时,会先创建系统类加载器,然后将此加载器作为创建该加载器线程的上下文加载器。
      • 当前类加载器(Current Classloader):每个类都会使用自己的类加载器(加载自身的类加载器)来加载其他类(指的是依赖的类)。

    复习:p34-p36

  • 虚拟机字节码执行引擎

    • 栈帧(stack frame)
      • 用于帮助虚拟机执行方法调用与方法执行的数据结构。
      • 封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈。
      • 符号引用(常量池中),直接引用
    • 方法调用
      • invokeinterface:调用接口中的方法,在运行期决定调用实现接口的哪个对象的方法
      • invokestatic:调用静态方法
      • invokespecial:调用自己的私有方法,构造方法以及父类的方法
      • invokevirtual:调用虚方法,运行期动态查找的过程
      • invokedynamic:动态调用方法。
    • 静态解析的情形
      • 静态方法
      • 父类方法
      • 构造方法
      • 私有方法
      • 这四种方法称为非虚方法。
    • 方法的重载是一种静态的行为。
    • 方法的重写是一种动态的行为。
posted @ 2021-06-08 16:17  这是一个ID  阅读(276)  评论(0编辑  收藏  举报