深入理解JAVA虚拟机——个人阅读笔记

239 阅读33分钟

阅读前说明

  1. 因为这里原本没想过分享出来, 做笔记时对个别不会的概念直接引用了部分的网址,如果构成侵权或者有侵权的嫌疑,无论是作者亦是读者,请联系我,将立刻删除该部分,并表示道歉。
  2. 这里笔记只是面向我个人的一个阅读记录,这里只是作为分享的用途,不喜勿喷。
  3. 这里基本都是书中的原话精简版,未读过这本书的小白可能看不懂我写啥,读过这本书的如果是忘记了某个概念可以尝试在这里中尝试寻找。
  4. 如果对我做笔记有什么更好的建议可以在评论区分享,一定虚心请教。
  5. 这里只是这本书的一部分,并不是全部。

自动内存管理机制

Java 与 C++ 之间有一堵由动态内存分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

Java内存区域与内存溢出异常

运行时数据区域

程序计数器

  1. 用来记录当前执行的字节码指令(通俗的说就是执行到哪一行代码)。
  2. 一个线程拥有一个独立的程序计数器。

JAVA虚拟机栈

  1. JAVA虚拟机栈是线程私有的(即一个线程拥有一个独立的JAVA虚拟机栈)。
  2. JAVA虚拟机栈的生命周期与线程相同。
  3. JAVA虚拟机栈用于存储局部变量法,操作数栈,动态链接,方法出口等信息。
    1. 局部变量:基本数据类型
    2. 对象引用
    3. returnAdress类型:指向一条字节码的指令。
  4. 两种异常状况
    1. JAVA虚拟机栈不可动态拓展,栈深度大于允许深度,则抛出StackOverflowError。
    2. JAVA虚拟机栈可以动态拓展,栈无法申请到足够的内存(受物理条件限制),则抛出OutOfMemoryError。

本地方法栈

  1. 与虚拟机栈相似
  2. 本地方法栈为虚拟机使用到的Native方法服务(虚拟机栈为java方法服务)。
  3. 与虚拟机栈一样会抛出StackOverflowError异常和OutOfMemoryError异常。

JAVA堆

  1. 所有线程共享一片JAVA堆区域。
  2. JAVA堆的唯一目的是存放对象,所有对象实例以及数组都在JAVA堆中分配。
  3. JAVA堆是垃圾收集的主要区域,也被称为“GC堆”。
  4. JAVA堆可以被细分为多个区域,划分的目的是为了更好的回收内存,或更快的分配内存。
  5. JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。
  6. 若JAVA堆中没有内存完成实例分配,则抛出OutOfMemoryError异常。

方法区(Method Area)

  1. 方法区是各个线程共享的内存区域,用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。
  2. Java虚拟机规范对方法区的限制非常宽松,可以选择内存大小是否可拓展、是否实现垃圾收集。
  3. 一般来说,该区域的垃圾回收难以达到满意的效果,类型卸载的条件相当苛刻,但该区域实行垃圾回收确实必要。
  4. 若方法区无法满足内存分配需求,则抛出OutOfMemoryError异常。

运行时常量池(Runtime Constant Pool)

  1. 运行时常量池属于方法区的一部分。
  2. Class文件包含信息之一是常量池,用于存放生成的各种字面量和符号引用,这部分将在类加载后进入方法区的运行时常量池中存放。
  3. 一般来说,除了Class文件的描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
  4. 符号引用和直接引用的介绍
  5. 若方法区中没法再申请到内存,则抛出OutOfMemoryError异常。

直接内存(Direct Memory)

  1. 直接内存不是虚拟机运行时的数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁的使用。
  2. 部分方法可以调用堆外内存,从而显著提高性能(NIO类)。
  3. 虽然不受Java堆的限制,但仍然受本机物理条件限制,有可能抛出OutOfMemoryError异常。

HotSpot 虚拟机对象探秘

对象的创建

  1. 当遇到new指令时,首先将去检查这个指令的参数是否能在常量池定位到一个类的符号引用,并检查该符号引用的类是否加载、解析和初始化过,若没有,则需要先执行类加载过程。
  2. 类加载检查通过后,需要为新生对象分配内存。
    • 两种分配方法:
      • “指针碰撞”分配法:若Java堆内存绝对规整(用过的内存放在一边,没用过的内存放在另一边,中间放着一个指针作为分界点指示器),那么分配内存仅仅是把那个指针向空闲部分挪动与对象大小相等的距离。
      • “空闲列表”分配法:若Java堆内存不规整,则虚拟机需要维护一张表,用于记录那些内存块可用,在分配时从列表中划分出一块足够大的内存给予实例,并更新表上的记录。
    • 并发问题:并发情况下线程不安全
      • 对分配内存空间的动作进行同步处理————实际上虚拟机采用CAS配上失败重试的方法保证更新操作的原子性。
      • 内存分配动作按照线程不同划分在不同空间区域中进行,该区域内存叫本地线程分配缓冲TLAB)。若某块线程的TLAB内存被分配完,则再重新分配新的TLAB,重新分配时需要同步锁定。(个人理解:该方法可以减少同步锁定的次数。)
  3. 分配内存后,虚拟机把分配到的内存空间都初始化为零值(对象头除外),保证了对象实例在Java代码中不赋初始值就可以使用。
  4. 虚拟机对对象进行必要的设置,如对象的所属类,对象哈希码,对象的GC分代年龄等信息,放到对象头之中。在根据虚拟机当前运行状态,对对象头进行设置。
  5. 对于虚拟机来说,目前新的对象已经产生。对于Java程序来说,还需要执行方法,使对象按照程序员的意向初始化。

对象的内存布局

  • HotSpot虚拟机中,对象在内存存储分为3个区域:对象头、示例数据和对齐单元。
    • 对象头(Head):分为两部分:
      • 第一部分:用于存储对象自身的运行时数据:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,官方称为“Mark Word”。
      • 第二部分:类型指针。虚拟机通过该指针确定这个对象是哪个类的实例。若该对象是数组,那对象头还必须有一块记录数组长度的数据。
    • 实例数据(Instance Data):存储程序代码中所定义的各种字段内容(无论父类还是子类的定义都记录。)。这部分的存储数据收到虚拟的的分配策略参数和字段在Java源代码定义顺序的影响。
    • 对齐填充:仅充当占位符作用。HotSpot自动内存管理系统要求对象起始地址必须是8字节的整数倍。

对象的访问定位

  • Java程序是通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有两种:
    • 句柄访问:Java堆会划分出一块内存来作为句柄池,reference中存储的就是该对象的句柄地址,而句柄中半酣了对象实例数据和类型数据各自的具体地址信息。
      • 优点:对象被移动时(垃圾收集移动对象非常普遍),不需要经常改变reference的值,只需要更新句柄实例数据的指针。
    • 直接指针访问:reference中存储的是对象地址。
      • 优点:速度更快。

垃圾收集器与内存分配策略

概述

  • 栈不与要过多的考虑垃圾回收的问题。
  • Java堆和方法区的内存分配和回收是动态的,垃圾收集器苏哦关注的是这部分内存。

对象已死吗

引用计数法

  • 实现方法:给对象添加一个引用计数器,每当一个地方引用它,则计数加1,若计数器为0,则表示对象不可能再被引用。
  • 优点:判定效率高
  • 缺点:它很难解决对象之间相互循环引用的问题。
  • 当前主流Java虚拟机没有选用引用计数法来管理内存的。

可达性分析算法

  • 可达性分析(Reachability Analysis):从一系列被称为"GC Roots"的对象出发向下搜索,经过的路径称为引用链(Reference Chain),若存在对象与任意"GC Roots"对象没有引用链,则该对象不可引用。
    • GC Roots对象包括:虚拟机栈中引用的对象,方法区中静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。

再谈引用

  • 引用分为4个等级(从上到下依次减弱效果):
    • 强引用:垃圾收集器永远不会收集强引用的对象。
    • 软引用:在内存即将溢出之前,会把软引用对象列入回收范围进行二次回收,若仍然没有足够内存,再抛出异常。
    • 弱引用:该对象只能生存到下一次垃圾收集发生之前。
    • 虚引用:相当于没有被引用。设置虚引用的唯一目的是能在这个对象被收集器回收时收到以恶搞系统通知。

生存还是死亡

  1. 被回收前需要达成两个标记:
    1. 没有引用链。
    2. 若该对象覆盖了finalize()方法,则必须被执行过。
  2. 若finalize()使对象重获引用链,可以避免被回收。
  3. 每个对象的finalize()仅且只会被系统调用一次(只能通过该方法拯救一次)。
  4. 虚拟机调用线程执行finalize()方法并不承诺等待该方法结束,避免该方法执行过慢甚至又死循环等阻碍垃圾回收机制执行。
  5. finalize()方法不推荐使用!
    • 运行代价大
    • 不确定性大
    • 无法保证各对象调用顺序

回收方法区

  1. 永久代的垃圾收集主要回收两部分内容:
    1. 废弃常量:用类似Java堆垃圾回收的方式判断常量是否被引用,并进行回收。
    2. 无用的类:类需要同时满足下面3个条件才能算是“无用的类”:
      1. 该类所有的实例都被回收,即Java堆中不存在该类的任何实例。
      2. 加载该类的ClassLoader已经被回收。
      3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  2. 虚拟机并不是满足“无用的类”三个回收条件就一定回收该类。
  3. 在大量使用放射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载的功能,以保证永久代不会溢出。

垃圾收集算法

标记——清除算法

  • 标记——清除算法(最基础):分为"标记"和"清除"两个阶段。
  • 主要两大不足:
    • 效率问题:两个阶段的效率都不高。
    • 空间问题:容易产生大量不连续的内存碎片。

复制算法

  • 复制算法:把内存平均分成两块,每次只是用一块,当其中一块用完后,则把当前存活的对象复制到另一块内存上(实现了整齐排列的效果),再直接把当前整块内存清理掉(效率高)。
  • 主要不足:把内存平均分,代价太大。
  • 现在的商业虚拟机都采用这种方法来回收新生代,但因为大量的对象都会被回收,所以回收前(Eden)和回收后(Survivor)的区域并非平均分,通常为8:1.若Survivr区域内存不够,则向老年代分配担保。

标记——整理算法

  • 标记——整理算法:先标记,再把存活对象向一端移动,然后清理掉边界以外内存。
  • 用于所有对象都存活的极端现象,且内存充分利用,适用老年代。

分代收集算法

  • 分代收集算法:根据对象存活周期长短把内存划分为几块。再根据各个年代特点选取适当的收集算法。
  • 当前的商业虚拟机都采用“分代收集”(Generational Collection)算法。

HotSpot的算法实现

枚举根节点

  1. 在可达性分析中,很多应用方法区非常大,逐个检查引用会很消耗时间。
  2. 可达性分析堆执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——GC进行时,必须停顿所有的Java执行线程。
  3. 虚拟机不需要一个不漏的检查GC Roots,应当有办法直接得知哪些地方存放着对象引用。

安全点(Safepoint)

  1. HotSpot只在特定的位置生成OopMap,这些位置叫安全点,并停顿下来执行GC。
  2. 让所有线程都到达安全点进行GC的两种方案:
    • 抢先式中断:GC发生时,所有线程中断,并让未到达安全点的线程恢复进行,直到它们都到达安全点。
    • 主动式中断:设置一个标志,各线程主动去轮询该标志,若发现中断标志则主动停下。该标志与安全点重合。

安全区域(Safe Region)

  1. 问题:若程序没有分配到CPU时间,则无法短时间去到安全点挂起。而让虚拟机等待该线程不现实。
  2. 安全区域是指在一段代码片段中,引用关系不会发生变化。
  3. 当JVM发起GC时,不需要理会处于安全区域的线程,线程离开安全区域时会检测JVM是否完成根节点枚举过程,若完成才会离开。

垃圾收集器

  • 没有最好的收集器,没有万能的收集器,只有具体应用下最合适的收集器。

Serial收集器

  1. Serial收集器是最基本、发展历史最悠久的收集器。
  2. Serial收集器使用复制算法收集新生代,使用标记——整理算法收集老年代垃圾。
  3. Serial收集器进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束。
  4. Serial优于其他收集器的地方:简单而高效。
  5. 它依然是虚拟机运行在Client模式下的默认新生代收集器。

ParNew收集器

  1. ParNew收集器是Serial收集器的多线程版本,使用的收集算法与Serial相同。
  2. 除了Serial收集器,目前只有ParNew收集器能够与CMS收集器配合工作。(ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器。)

Parallel Scavenge 收集器

  1. Parallel Scavenge 收集器与ParNew收集器相似,但侧重点不同。
  2. Parallel Scavenge 收集器的目标是达到可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。由于与吞吐量关系密切,Parallel Scavenge 收集器也被称为“吞吐量优先”收集器。
  3. Parallel Scavenge 收集器的三个参数
    1. 控制最大垃圾收集停顿时间的参数:收集器将尽可能控制在该时间内完成。(该参数过小可能会牺牲吞吐量作为代价。)
    2. 控制吞吐量大小的参数:该参数相当于吞吐量的倒数。
    3. 控制内存自适应开关:参数打开后,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整各个内存区域大小以提供最合适的停顿时间或者最大吞吐量,这种调节方式称为GC自适应的调整策略。(GC自适应的调整策略是Parallel Scavenge 收集器与ParNew收集器的重要区别。)

Serial Old 收集器

  1. Serial Old 收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。
  2. 使用标记——整理算法
  3. 它主要两大用途:
    1. 在JDK1.5之前与Parallel Scavenge 收集器搭配使用。
    2. 作为CMS的后续方案,在并发发生Concurret Mode Failure时使用。

Parallel Old 收集器

  1. Parallel Old 收集器是Parallel Scavenge 收集器的老年代版本。
  2. 使用“标记——整理”算法。
  3. 主要用于与新生代收集器Parallel Scavenge 收集器搭配。

CMS 收集器

  1. CMS 收集器是以获取最短回收时间为目的的收集器。
  2. CMS 收集器基于“标记——删除”算法。
    1. 初始标记:标记与GC Roots能直接关联的对象。
    2. 并发标记:进行GC Roots Tracing的过程。
    3. 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
    4. 并发清除
  3. CMS 收集器的3个缺点
    1. CMS 收集器对CPU资源非常敏感。
    2. CMS 收集器暂时无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败导致另一次Full GC产生。
    3. 该收集算法可能产生大量碎片空间。
      1. 一个开关参数(默认开启):产生Full GC时开启内存碎片整合。该过程不能并发,等待时间变长。
      2. 一个参数:这个参数用于执行多少次不压缩的Full GC后跟一次带压缩的。
  4. G1 收集器
    1. G1 收集器是当今收集器技术发展的最前沿成果之一。
    2. G1 收集器是一款面向服务端应用的垃圾收集器。与其他收集器相比,具有以下特点:
      • 并行与并发
      • 分代收集
      • 空间整合
      • 可预测的停顿:G1 收集器能够建立可预测的停顿时间模型。
    3. G1 收集器将Java堆划分为多个相等的独立区域(Region),G1跟踪各个区域里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域,提高收集效率。

理解GC日志

反正我还是看不懂,在书上P89页...

垃圾收集器参数总结

  • 书本90-91页...
  • 我还是不知道这些参数在哪里设置...

内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。

对象优先在Eden分配

  • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
    • 新生代GC(Minor GC):发生在新生代的垃圾收集动作,一般速度快。
    • 老年代GC(Major GC/Full GC):发生在老年代的垃圾收集,速度比新生代GC慢10倍以上。

大对象直接进入老年代

  • 大对象:需要大量连续内存的对象。
  • 一个参数:令大于该参数值的对象直接在老年代分配。避免了复制算法时发生大量的内存复制。

长期存活对象将进入老年代

  • 虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor区容纳,则该对象被移动到Survivor区并年龄增加1岁。
    • 一个参数:若年龄大于该参数,则该对象进入老年代。

动态对象年龄判定

  • 若Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,则年龄大于或等于改年龄的对象就可以直接进入老年代,无需达到参数指定年龄。

空间分配担保

  • 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,那么Minor GC可以确保是安全的。否则,则虚拟机查看是否允许担保失败(一个开关参数),若允许,则检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC会有风险;若小于,或虚拟机不允许冒险,则改为进行一次Full GC。
  • 再JDK 6 Update 24之后,该开关参数失效,虚拟机默认打开的状态(不可调)。

虚拟机性能监控与故障处理工具

给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。

JDK命令行工具

  1. jps:虚拟机进程状况工具
  2. jstat:虚拟机统计信息检测工具
  3. jinfo:JAVA配置信息工具
  4. jmap:JAVA内存映射工具
  5. jhat:虚拟机对转储快照分析工具
  6. jstack:JAVA堆栈跟踪工具
  7. HSDIS:JIT生成代码反汇编

JDK的可视化工具

JConsole:Java 监视与管理控制台

VisualVM:多合一故障处理工具

调优案例分析与实战

  • 特么我要是看得懂我...

虚拟机执行子系统

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

类文件结构

//TODO 复杂的内容需要放在日后慢慢细读和消化。 ——沃·兹基·硕得

虚拟机类加载机制

概述

  1. 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java机的类加载机制。

类加载的时机

  1. 类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称连结。
  2. 虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”:
    1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。
    2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则先需要触发其初始化。
    3. 当初始化一个类的时候,如果发现其父类没有进行过初始化,则需要先触发其父类的初始化。
    4. 当虚拟机启动时,用户要指定一个要执行的主类,虚拟机会先初始化这个类。
    5. JDK7特性,我看不懂,而且很长,不想打......
  3. 几种情况说明:
       public void static main(String[] args){
           SuperClass[] sca = new SuperClass[10];
       }
    
    1. 申明一个数组:虚拟机并没有触发SuperClass的初始化阶段,但出发了另一个类的初始化阶段,这个类代表了SuperClass的一维数组,数组中的应有属性和方法都是现在这个类里。该类会检查数组是否会越界。
        public class ConstClass{
        
            static{
                System.out.plintlin("ConstClass init!");
            }
            
            public static final String HELLOWORLD = "hello world";
        }
        
        public class NotInitialization{
            public void static main(String[] args){
                ConstClass.HELLOWORLD;
            }
        }
    
    1. 上面代码没有输出"ConstClass init!"。因为在便宜阶段通过了常量传播优化,已经将此常量值存储到了NotInitialization类的常量池中,以后NotInitialization对ConstClass.HELLOWORLD的引用实际都转化为NotInitialization类对自身常量池的引用。
  4. 接口的加载过程与类加载有些不同,接口也有初始化的过程,编译器会为接口生成"()"类构造器,用于初始化接口中所定义的成员变量。
    • 接口的初始化在实现它的类初始化时会被一同初始化,父接口并不一定初始化,只有真正使用到父接口的时候才会初始化。

类加载的过程

  • 加载、验证、准备、解析、初始化这5个阶段的具体动作。

加载

  1. “加载”阶段虚拟机完成3件事情:
    1. 通过一个类的全限定名来获取这个类的二进制字节流。
    2. 将这个类所代表的静态存储结构转化为方法区运行时的数据结构。
    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  2. “加载”阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器完成。
  3. 对于数组而言,数组类本身不由类加载器创建,而是由Java虚拟机直接创建。创建过程遵循以下规则:
    1. 若数组组建是引用类型,则先采用类加载机制加载这个组件类型,该数组将在加载该组建类型的类加载器的类名称空间上被标识。(问题:既然组件类型被加载,为什么上面的代码里该数组的类没有被加载?)
    2. 若该组件类型是基本类型,则Java虚拟机会把数组标记为与引导类加载器关联。
    3. 数组类型的可见性与组件类型的可见性一致,若组件类型不是引用类,则默认为public。
  4. 加载阶段和连结阶段的部分内容是交叉进行的,但开始执行的顺序是固定的。

验证

  1. 这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。
  2. 验证大致分为4个阶段:
    1. 文件格式验证:字节流是否符合Class文件规范,并且能被当前版本的Java虚拟机处理。
    2. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
    3. 字节码验证:主要通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。第二阶段对元数据的数据类型进行检验后,这个阶段将对类的方法体进行检验分析,保证不会做出危害虚拟机安全的事情。
      • 一个程序即使通过了字节码验证也不能说一定安全,通过程序去校验程序是无法做到绝对精准的。
    4. 符号引用验证:将符号引用转化为直接引用,这个转化动作将在“解析阶段”中发生。
  3. 对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要的阶段(对程序运行期没有影响)。
    1. 一个开关参数:用来关闭大部分类验证措施,减少类加载时间。

准备

  1. 准备阶段是正式为类变量分配内存并设置类变量初始值(这里的初始值一般指零值,而不是代码中指定的值)的阶段,这些变量使用的内存都会在方法区中分配。
  2. 特殊情况:若类字段属性表中存在ConstantValue属性(同时符合3个条件:static、final、基本类型或String),则准备阶段该字段就会被初始化为代码所指定的值。
public static final int value = 123;

解析

  1. 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
    • 符号引用:符号引用以一组符号描述所引用的目标。
    • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
  2. 对同一符号引用进行多次解析请求是很常见的事情,虚拟机实现可以对第一次解析的结果进行缓存。(invokedynamic指令除外)
  3. 对于invokedynamic指令,上面规则不成立。它所对应的引用称为“动态调用点限定符”,这里“动态”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能执行。
  4. 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
  5. 4种引用的解析过程:
    1. 类或接口解析:假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用。
      1. 若C不是数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。一旦加载过程出现了任何异常,解析过程宣布失败。
      2. 若C是一个数组类型,并且数组的元素类型为对象,那么会先按照第一点的规则加载数组类型。接着由虚拟机生成一个代表此数组维度和元素的数组对象。
      3. 确认D是否具备对C的访问权限。若不具备,抛出java.lang.IllegalAceessError异常。
    2. 字段解析:将字段所属类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索:
      1. 如果C本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回这儿字段的直接引用,查找结束。
      2. 否则,若在C中实现了接口,则将按照继承关系从下到上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回字段的直接引用,查找结束。
      3. 否则,若C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回字段的直接引用,查找结束。
      4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。
      5. 如果查找成功返回引用,若不具备字段访问权限,则抛出java.lang.IllegalAceessError异常。
    3. 类方法解析:将字段所属类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索:
      1. 类方法和接口方法符号引用的常量类型定义是分开的,若类方法表中发现class_index中索引的C是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
      2. 如果通过了第一步,在类C中查找是否有简单名称和字段描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
      3. 否则,在类C的父类中递归查找是否有简单名称和字段描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
      4. 否则,在类C实现的接口列表及其它们的父接口之中递归查找是否有简单名称和字段描述符都与目标相匹配的方法,如果有则说明类C是一个抽象类,这时查找结束,抛出java.lang.AbstractMethodError异常。
      5. 否则,宣布方法查找失败,抛出java.lang.NoSuchMethodError异常。

初始化

  1. 初始化阶段才真正执行类中定义的 Java程序代码。
  2. 初始阶段是执行类构造器()方法的过程。
    1. ()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块(static{})中语句合并产生的,编译器收集顺序由源文件出现的顺序所决定。
    2. ()方法与实例构造器不同,它不需要显示的调用父类构造器,虚拟机会保证子类的()方法执行之前,父类的()方法已经执行完毕。
    3. ()方法不是必须的,若没有静态语句块和类变量赋值动作,则不会生成()方法。
    4. 接口也有()方法(有初始化赋值操作),但只有该接口变量被调用时才会触发接口的()方法。
    5. 虚拟机会保证一个类的()方法在多线程下被正确的加锁、同步。同一个类加载器,一个类型只会初始化一次。

类加载器

  1. 通过一个类的全限定名来获取描述此类的二进制字节流,实现这个动作的代码模块叫“类加载器”。

类与类加载器

  1. 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都与一个独立的类名称空间。

双亲委派模型

  1. 从Java虚拟机来讲,只有两种类加载器:
    1. 启动类加载器(Bootstrap ClassLoader):这个类加载器使用C++语言实现,是虚拟机自身一部分。读取<JAVA_HOME>/lib目录中的类库。
    2. 其他类加载器:由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。 1.拓展类加载器(Extension ClassLoader):读取<JAVA_HOME>/lib/ext目录中的类库 2. 应用程序类类加载器(Application ClassLoader):读取ClassPath上的类库。
  2. 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
  3. 双亲委派模型工作过程:当获取到类加载请求时,先交给父类加载器加载,若父类加载器无法完成加载请求,子子加载器再尝试加载。
  4. 好处:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

破坏双亲委派模型

//TODO 有三次破坏的情况,反正我都看不懂......

虚拟机字节码执行引擎

概述

  1. 执行引擎是Java虚拟机的最核心组成部分之一。

运行时栈帧结构

  1. 栈帧(Stack Frame)是由于虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
  2. 对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。执行引擎运行的所有字节码指令都是只针对当前栈帧进行操作。

局部变量表

  1. 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
  2. 局部变量表的最小单位为容量槽(Variable Solt)。
  3. 虚拟机通过索引定位的方式使用局部变量表,所引致的范围从0开始至局部变量表最大Solt的数量。
  4. 在方法执行的时候,索引值为0的Solt用于存储该方法所属的实例的引用,从索引值为1开始的Solt开始按顺出存储方法的参数,剩余的Solt存储方法内部的局部变量。
  5. 为了节省栈帧的空间,虚拟机允许对Solt的复用。当方法某个变量由PC计数值得知超出了作用域,该局部变量的的Solt将交给其他变量使用。
  6. 局部变量没有被赋初值不能使用(没有经历初始化阶段)。

动态连接

  1. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
  2. 符号引用在编译期间转化为直接引用的为静态解析,在运行期间转化为直接引用的为动态连接

方法返回地址

  1. 当一个方法执行后只有两种退出方式。
    1. 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,具体情况会根据返回指令来决定
    2. 异常完成出口:另一种退出方式,方法执行遇到异常,且该异常没有在方法内处理,会导致方法退出。
    3. 方法正常退出时,调用者PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数值;异常退出时,返回地址是要通过异常处理表来确定,栈帧一般不保存这部分信息。

附加信息

  • 虚拟机允许具体虚拟机实现中增加一些规范没有的描述的信息到栈帧中。
  • 一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

方法调用

  1. 方法调用不等同于方法执行,方法调用阶段唯一任务就是确定被调用方法的版本(确定调用哪个方法),暂时不涉及方法内部具体运行过程。

解析

1.方法调用的目标方法,在Class文件里都是一个常量池里的符号引用,在类加载的解析阶段,会将其中的一部分转化为直接引用。 2. 上述解析成立前提:调用目标在程序代码写好、编译器进行编译时就必须确定下来。这种类方法的调用称为解析。 3. Java语言符合“编译期可知,运行期不可变”的方法主要是静态方法私有方法。 4. Java虚拟机提供的5条方法调用字节码指令: + invokestatic + invokespecial