深入理解Java虚拟机——JVM

2021/8/7 11:06:13

本文主要是介绍深入理解Java虚拟机——JVM,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Java内存区域和内存溢出异常 运行时数据区域 JVM在执行Java程序时会把它所管理的内存划分为若干个不同数据类型,有的区域随着JVM进程的启动一直存在,有的区域则是依赖用户线程的启动和结束而建立和销毁 程序计数器 一块较小的内存空间,可看作是当前线程所执行的字节码的信号指示器,字节码解释器通过改变这个计数器的值来选取下一条要执行的字节码指令,是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等功能都依赖计数器完成 为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储 JVM栈 JVM栈也是线程私有的,生命周期与线程相同。JVM栈描述的是Java方法执行的线程内存模式:每个方法被执行的时候,JVM都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息 局部变量表存放了编译器可知的各种JVM基本数据类型,对象引用(reference),returnAdress类型(指向一条字节码指令的地址)。数据类型在局部变量中的存储空间以局部变量槽(slot)表示,long和double启用两个slot,其余占用一个 内存区规定了两个异常状况:如果线程请求的栈深度大于JVM所允许的深度,将抛出StackOverflowError异常;如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常 本地方法栈 与JVM栈作用相似,区别是JVM栈为JVM执行执行Java方法服务,本地方法栈是为JVM使用到的本地方法服务 Java堆 对于Java应用程序来说,Java堆是JVM所管理的内存中最大的一块,在JVM启动时创建,Java堆时垃圾收集器管理的内存区域,也被称作“GC堆” 将Java堆细分的目的是为了更好地回收内存,或更快地分配内存,没有Java堆中没有完成实例分配,并且堆也是无法扩展时,JVM将会抛出OutOfMemoryError异常 方法区 和Java堆一样,是各个线程共享的内存区域,用于存储已被JVM加载的类型信息、常量、静态变量、即时编译器编译后代码缓存等数据 方法区除了和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集 运行时常量池 是方法区的一部分,Class文件除了有类的版本,字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用 运行时常量池相对于Class文件常量池的另外一个重要特性是具备动态性,Java语言并不要求常量一定有编译期才能产生,运行期也可以将新的常量放入池中,当常量池无法再申请到内存时会抛出OutOfMemoryError异常 直接内存 直接内存的分配不会受到Java堆大小的限制,但还是会收到本机总内存大小以及处理器寻址空间的限制 HotSpot虚拟机对象探秘 对象的创建 JVM遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,先执行相应的类加载过程 类加载检查通过后,JVM将为新生对象分配内存。假设Java堆中的内存是绝对规整的,所有被使用过的内存放在一边,空闲的内存放在一边,中间放着一个指针作为分界点的指示器,所分配的内存就仅仅把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“空闲列表” 当使用Serial、ParNew等压缩整理过程的收集器时,采用的分配算法是指针碰撞,简单又高效 可能出现的问题:对象创建在JVM中是非常频繁的行为,即使修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B同时使用可原来的指针来分配内存的情况 解决方案:1、对分配内存空间的动作进行同步处理,实际上JVM是采用CAS配上失败重试的方法保证更新操作的原子性 2、把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,为本快线程分配缓冲 接下来,JVM还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何找到类的元数据信息、对象的Hash码、对象的GC分代年龄等信息 对象的内存布局 在HotSpotJVM里,对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据、对齐填充 对象头部分包括两类信息:1、用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志等 2、类型指针,JVM通过这个指针来确定该对象哪个类的实例 接下来实例数据部分是对象真正存储的有效信息,即在程序代码里所定义的各种类型的字段内容,这部分的存储顺序会受到JVM分配策略参数(-XX:FieldsAllocationStyle)和字段在Java源码中定义顺序的影响 对象的访问定位 主流访问方式有使用句柄和直接指针 如果使用句柄访问的化,Java堆中将可能会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息  好处:reference存储的是稳定句柄地址,reference本身不需修改 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何设置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销   好处:速度快,节省一次指针定位的时间开销, 实战:OutOfMemoryError异常 Java堆溢出 Java堆用于储存对象实例,不断地创建对象,并且保证GCRoots到对象之间有可选路径来避免垃圾回收机制清除这些对象,随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常 常规的处理方法是先通过内存映像分析工具,堆Dump出来的堆转储快照进行分析,第一步应确认内存中导致OOM的对象是否是必要的,分清到底是出现内存泄漏还是内存溢出,若是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GCRoots相关联,根据泄漏对象的类型信息以及它们的GCRoots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏代码的位置;如果不是内存泄漏,就应当检查JVM的对参数(-Xmx与-Xms)设置,看是否还有向上调整的空间,而从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗 JVM栈和本地方法栈溢出 两种异常: 1、如果线程请求的栈深度大于JVM所允许的最大深度,将抛出StackOverflowError异常 2、如果JVM的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常 使用-Xss参数减少栈内存容量   结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小 定义了大量的本地变量,增大此方法帧中本地变量表的长度   结果:抛出StackOverflowError异常,异常出现时输出堆栈深度相应减少 方法区和运行时常量池溢出 String::intern()是一个本地方法,作用是如果字符串常量池已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会为此String对象包含的字符串添加到常量池中,并且返回此String对象的引用 -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小 -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整 -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致的垃圾收集的频率 本机直接内存溢出 直接内存的容量大小可通过 -XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值一致 真正申请分配内存的方法是Unsafe::allocateMemory() 垃圾收集器和内存分配策略 Java堆和方法区这两个区域有着明显的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所抛行的不同条件分支所需要的内存也可能不一样,只有处于运行期才知程序会创建哪些对象,创建多少对象,这部分内存的分配和回收是动态的 引用计数算法 判断对象存活的算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加一;当引用失效时,计数器值减一 主流的JVM没有选用引用计数器算法来管理内存,原因是这个看似简单的算法有很多例外情况需要考虑,须配合大量额外处理才能保证正确地工作,单纯的引用计数很难解决对象之间相互循环引用的问题 可达性分析算法 通过一系列称为“GCRoots”的根对象作为起始节点集,根据引用关系向下搜索,如果某个对象到GCRoots间没有任何引用链相连,证明此对象不能再被使用 可作为GCRoots的对象包括以下几种: 1、在JVM栈中引用的对象,如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等   2、在方法区中常量引用的对象,如字符串常量池里的引用  3、在本地方法栈中JNI(即通常所说的Native方法)引用的对象    4、JVM内部的引用,如基本数据类型对象的Class对象,一些常驻的异常对象(NullPointException、OutofMemoryError)等 5、所有被同步锁(synchronized关键字)持有的对象   6、反映JVM内部情况的JMXBea、JVMTI中注册的问题,本地代码缓存等 再谈引用 4钟:强引用,软引用,弱引用,虚引用(强度依次减弱) 强引用:指代码之中普遍存在的引用赋值,即类似“Object object = new Object()”这种引用关系,只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象 软引用用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收没有足够的内存,才会抛出内存溢出异常 弱引用用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象  虚引用:不会对对象生存时间构成影响,也无法取得一个对象实例,虚引用的目的是为了能在这个对象被收集器回收时收到一个系统通知 生存还是死亡? 真正宣告一个对象死亡,至少要经历两个标记过程:如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假设对象没有覆盖finalize()方法,或finalize()方法已被JVM调用过,那JVM将这两种情况视作“没有必要执行”;若被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的列队之中,并在稍后有一条由JVM自动建立的、低调度优先级的Finalizer线程去执行它们的finalized()方法 任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()不会被再次执行 回收方法区 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型 判断常量是否“废弃”相对简单,要判定类型是否属于“不再使用类型”有3个条件    1、该类所有的实例都已被回收  2、加载该类的类加载已经被回收   3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 垃圾收集算法 划分为“引用计数式垃圾收集”和“追踪式垃圾”收集两大类,也被称作i“直接垃圾收集”和“间接垃圾收集” 分代收集理论 收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储 Java堆分为新生代和老年代,在新生代钟,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象将会逐步晋升到老年代钟存放 部分收集:指目标不是完整收集整个Java堆的垃圾收集,又分为 新生代收集:指目标只是新生代的垃圾收集   老年代收集:指目标只是老年代的垃圾收集,目前只有CMS收集器会有单独收集老年代的行为 混合收集:指目标是收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为 标记—清除算法: 两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象 两个缺点:1、执行效率不稳定,如果Java堆中包含大量对象,其中大部分是需要被回收的,这时必须进行大量标记和清楚的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低 2、内存空间碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作 标记—复制算法 将可用内存按容量划分为大小相等的两块。每次只使用其中的一块,当这一块的内存用完了,就将还存活者的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉 缺点:将可用的内存缩小为原来的一半,空间浪费过多 标记—整理算法 标记过程仍然与“标记—清除算法”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存 标记—清除算法和标记—整理算法的本质差异在于前一种是一种非移动式的回收算法,后一种是移动式的;从垃圾收集停顿时间来看,不移动对象停顿时间更短,甚至可以不停顿,但从整个程序的吞吐量来看,移动对象会更划算 HotSpot的算法细节实现 枚节点枚举 所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,还必须在一个能保障一致性的快照中才得以进行 安全点 HotSpot没有为每条指令都生成OopMap只是在“特定的位置”记录了这些信息,这些位置称为安全点;安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷 还需要考虑如何在垃圾收集发生时让所有线程都跑到最近的安全点,两种方案:抢先式中断和主动式中断 抢先式中断不需要线程的执行代码主动区配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现用户线程中断的地方不在安全点上,就恢复这条线程执行,让它再重新中断,直到跑到安全点中 主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时不停地主动去轮询这个标志,一旦发现中断标志为真时,就自己再最近的安全点上主动中断挂起 安全区域 指能够确保在某一代码片段之中,引用关系不会发生变化,这个区域中任意地方开始垃圾收集都是安全的,可视作安全点被拉伸了 当用户线程执行到安全区域里面的代码时,首先会标识自己已进入安全区域,线程要离开安全区域时,要检查JVM是否已经完成了根节点枚举,如果已完成,线程继续执行,否则它就必须一致等待,直到收到离开安全区域的信号为止 记忆集与卡表 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构;垃圾收集的场景中,并不需要了解跨代指针的全部细节,下面列举了三种记忆精度 1、字长精度:每个记录精确到一个机器字长,该字包含跨代指针 2、对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针 3、卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针 卡表标记逻辑:CARD_TABLE [this address >> 9 ] = 0; 字节数组 CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,内存块称为卡页,大小是以2的N次幂的字节数 一个卡页的内存通常包含不止一个对象,只需卡页内有一个(或更多)对象的字段存在着跨代指针,将对应卡表的数组元素的值标识为1,称这个元素变脏,没有标识为0 写屏障 可看作在JVM层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环开通知,供程序执行额外的动作 应用写屏障后,JVM会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代的引用,每次只要对引用进行更新,就会产生额外的开销 并发的可达性分析 之前提到过,垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活,要求全过程都基于一个能保障一致性的快照中才能进行分析,为解释此原因引入可三色标记 白色:表示对象尚未被垃圾收集器访问过 黑色:表示对象已经被垃圾收集器访问过,且这个对象所有引用都已经扫描过 灰色:表示对象已经被垃圾收集器访问过,但这个对象至少存在一个引用还没有被扫描过 以下两个条件被满足时,会产生“对象消失”问题 1、赋值器插入了一条或多条从黑色对象到白色对象的新引用 2、赋值器删除了全部从灰色对象到该白色对象的直接或间接引用 解决并发扫描时对象消失两种方案:增量更新,原始快照 增量更新破坏了第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次 原始快照破坏了第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次 经典垃圾收集器 不存在“万能”的收集器,选择的都是对具体应用最合适的收集器 Serial收集器 一个单线程工作的收集器,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束,是HotSpotJVM运行在客户端模式下的默认新生代收集器,简单而高效,并且额外内存消耗是最小的 ParNew收集器 实质上是Serial收集器多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有参数控制(如 -XX:SurvivorRatio、-XX:PretenureSizeThreshold等)、收集算法、Stop The World等都与Serial收集器完全一致 并行:多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同的工作,通常默认此时用户线程是出于等待状态 并发:垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行 Parallel Scavenge收集器 是一款新生代收集器,基于标记—复制算法实现的收集器,也是能并行收集的多线程收集器,该收集器的目标是达到一个可控制的吞吐量 吞吐量=运行用户代码时间/运行用户代码时间+运行垃圾收集时间 Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数以及直接设置吞吐量大小的-XX:GCTimeRatio -XX:MaxGCPauseMills:参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户的设定值 -XX:GCTimeRatio:参数的值则是一个大于0小于100的整数,也就是垃圾收集器时间占总时间的比率,相当于吞吐量的倒数 Serial old收集器 Serial收集器的老年版本,使用标记—整理算法,供客户端模式下的HotSpotJVM使用,在服务端模式下,有两种用途,1、是在JDK5及之前版本与Parallel Scavenge收集器搭配使用  2、是作为CMS收集器发生失败时的后备预案 Parallel old收集器 Parallel Scaveng收集器的老年代版本,支持多线程并发收集,基于标记—整理算法实现,在注重吞吐量或处理器资源较为稀缺的场合,有限考虑 Parallel Scaveng加Parallel Old收集器这个组合 CMS收集器 一种以获取最短回收停顿时间为目标的收集器,运行过程分为4个步骤:初识标记,并发标记,重新标记,并发清除 初始标记,重新标记仍然需要“Stop The World”,初识标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;并发标记是从GCRoots的直接关联对象开始遍历整个对象图的过程;重新标记阶段是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录;清除阶段清理删除掉标记阶段判断的已经死亡的对象 优点:并发收集,低停顿   缺点:对处理器类源非常敏感,无法处理“浮动垃圾”,有大量空间碎片产生 Garbage First收集器 简称G1,是一款主要面向服务端应用的垃圾收集器 G1不再坚持用固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region可以根据需要扮演新迭代的Eden空间、Survivor空间,或老年代空间 G1能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集的内存空间都是Region大小的整数倍,可以有计划地避免在整个Java堆中进行全区域的垃圾收集 运行过程分为4个阶段 初始标记:仅仅只是标记一下GCRoots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时能正确地在可用的Region中分配新对象 并发标记:从GCRoots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,可与用户程序并发执行 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期待的停顿时间来指定回收计划,可以自由选择多个Region构成会收集,然后把决定回收的那一部分Region的存活对象复制到空的Region,再清理掉整个旧Region的全部空间 G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都比CMS高,目前小内存应用上CMS的表现大概率会优于G1,在大应用上G1则大多能发挥其优势,优劣势的Java堆容量平衡点通常在6~8GB之间 低延迟垃圾收集器 衡量垃圾收集器三项最重要的指标:内存占用、吞吐量、延迟 Shenandoah收集器 是一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器,相较于CMS和G1,Shenandoah不仅要进行并发的垃圾标记,还要并发地进行对象清理后的整理动作 Shenandoah默认不使用分代收集,不会有专门的新生代Region或者老年代Region的存在,摒弃了在G1耗费大量内存和计数资源去维护的记忆集,改用名为“连接矩阵“的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率 Shenandoah收集器 工作过程可分为9个阶段 初始标记:与G1一样,首先标记与GCRoots直接关联的对象,仍是”Stop The World“的,停顿时间与堆大小无关,只与GCRoots数量有关 并发标记:与G1一样,遍历对象图,标记出全部可达的对象,该阶段与用户线程一起并发,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度 最终标记:与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集 并发清理:用于清理那些整个区域内连一个存活对象都没有找到的Region 并发回收:Shenandoah要把回收集里面的存活对象先复制一份到其它未被使用的Region之中 初试引用更新:建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已经完成分配给它们的对象自动任务 并发引用更新:与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,不再需要沿着对象图来搜索,只需按照没错物理地址的顺序线性搜索出引用类型,把旧值改为新值即可 最终引用更新:解决了堆中引用更新后,还要修正存在于GCRoots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GCRoots的数量有关 并发清理:经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Region了,最后再调用一次并发清理过程来回收这些Region内存空间,供以后新对象分配使用 ZGC收集器 ZGC也采用基于Region的堆内存布局,不同的是ZGC的Region具有动态性——动态创建 和销毁,以及动态的区域容量大小 ZGC核心问题:并发整理算法的实现 ZGC标志性设计是采用染色指针技术,直接把标记信息记在引用对象的指针上 染色指针3大优势: 1、可以使得一旦某个Region的存活对象被移走之后,这个Region立即能被释放和重用,不必等待整个堆中所有指向该Region的引用都被修正后才能清理 2、可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是与写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,可以省略去一些专门的记录操作 3、染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重新定位进程相关的数据,以便日后进一步提高性能 ZGC运行过程: 并发标记:与G1、Shenandoah不同的是,ZGC标记是在指针上而不是对象上进行的,标记阶段会更新染色指针中的Marked0、Marked1标志位 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成分配集;ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1记忆集的维护成本 并发分配(核心阶段):把分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系 并发重映射:修正整个堆中指向重分配集中旧对象的所有引用 选择合适的垃圾收集器 Epsilon收集器 不能够进行垃圾收集的收集器,应用只要运行数分钟甚至数秒,只要JVM能正确分配内存,在堆耗尽之前就退出,可以选择运行负载极小,没有任何回收行为的Epsilon JVM及垃圾收集器日志 HotSpot所有功能的日志都收归到了“-Xlog”参数上,这个参数的能力也相应被极大扩展了:-Xlog[:[selector][:output][:[decorators][:out-options]]]] 命令行中最关键的参数是选择器(selector),由标鉴(Tag)和日志级别(Level)共同组成,标鉴可理解为JV吗某个功能模块的名字,它告诉日志框架用户希望得到JVM哪些功能的日志输出;日志级别从低到高有Trace Debug Info Warning Error Off六种级别,日志级别决定了输出信息的详细程序,默认级别为Info 实战:内存分配和回收策略 Java技术体系的自动内存管理,最根本的目标是自动化解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存 对象优先在Eden分配 大多数情况下,对象在新生代Eden中分配,在Eden区没有足够空间进行分配时,JVM将发起一次MinorGC HotSpotJVM提供了-XX:+PrintGCDetails这个收集器日志参数,通知JVM在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况 大对象直接进入老年代 大对象是指需要大量连续内存空间的Java对象,如很长的字符串,或元素数量很庞大的数组 JVM要避免大对象的原因:在分配空间时,容器导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好大对象,而当复制对象时,大对象就意味着高额的内存复制开销。HotSpotJVM提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,目的是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作 长期存活的对象将进入老年代 对象通常在Eden区里诞生,如果经过第一次MinorGC后仍然存活,并且能够被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值可通过参数-XX:MaxTenuringThreshold设置 动态对象年龄判定 HotSpotJVM并不是永远要求对象的年龄必须达到 -XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 -XX:MaxTenuringThreshold要求的年龄 空间分配担保 新生代使用复制收集算法,但为了内存利用率,使用其中一个Survivor空间来作为轮换备份,若出现内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。老年代进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间比较,决定是否进行Full GC来让老年代腾出更多空间 JVM性能监控、故障处理工具 基本故障处理工具 根据软件可用性和授权的不同,可把它们划分为2类: 1、商业授权工具:主要是JMC及它要使用到的JFR 2、正式支持工具:被chang'qi支持的工具,不同平台、不同版本的JDK之间,这类工具可能略有差异,但不会出现某一个工具突然小时的情况 jps:JVM进程状况工具 可以列出正在运行的JVM进程,并显示JVM执行主类(Main Class,mail()函数所在的类)名称以及这些进程的本地JVM唯一ID,是使用频率最高的JDK命令行工具 jps命令格式  jps[options] [hostid] jps还可以通过RMI协议查询开启了RMI服务的远程JVM进程状态,参数hostid为RMI注册表中注册的主机名 jstat :JVM统计信息监视工具 jstat是用于监视JVM各种运行状态信息的命令行工具。可以显示本地或远程JVM进程中的类加载、内存、垃圾收集、即时编译等运行期数据,在没有GUI图形界面,只提供纯文本控制环境的服务器上,将是运行期定位JVM性能问题的常用工具 jstat命令格式  jstat [option vmid [interval [s1ms] [count]]] jinfo:Java配置信息工具 作用是实时查看和调整JVM各项参数,但是jps命令的-V参数可以查看JVM启动时显式指定的参数列表,若想知道未被显式指定的参数的默认值,只能使用jinfo的-flag选项进行查询,jinfo还可以使用-sysprops选项把JVM进程的System.getProperties()内容打印出来 jinfo命令格式  jinfo [option] pid jmap :Java内存映像工具 不仅能获取堆转储快照,还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率,当前用的是哪种收集器等 在Windows平台下是受限的,除了生成堆转储快照的-dump选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都可以使用之外,其余选项都只能在Linux/Solaris中使用 jmap命令格式: jmap [option] vmid jhat:JVM堆转储快照分析工具 JDK提供jhat命令分析堆转储快照文件,一是分析过程是一个耗时而且耗费硬件资源的过程;二是jhat分析功能相对来说比较简陋 jstack:Java堆栈跟踪工具 jstack用于生成JVM当前时刻线程快照。线程快照是当前JVM内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环,请求外部类源导致的长时间挂起等,都是导致线程长时间停顿的常见原因 线程出现停顿通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到滴在后台做什么事情,或等待什么资源 jstack命令格式: jstack [option] vmid 可视化故障处理工具 JDK除了附带大量的命令行工具之外,还提供了几个功能集成度更高的可视化工具,用户可以使用这些可视化工具以更加便捷的方式进行过程故障诊断和调试工作 这些工具主要包括:JConsole  JHSDB  VisualVM  JMC   JHSDB:基于服务性代理的调试工具 JHSDB是一款基于服务型代理实现的进程外调试工具。服务性代理是HotSpotJVM中一组用于映射JVM运行信息的,主要基于Java语言(含少量JNI代码)实现的API集合。通过服务性代码的API,可以在一个独立JVM的进程分析其他HotSpotJVM的内部数据,或者从HotSpotJVM进程内存中dump出来的转储快照里还原出它的运行状态细节 JConsole :Java监视和管理控制台 一款基于JMX的可视化监视、管理工具。主要功能是通过JMX的MBean对系统进行信息收集和参数动态调整。JMX是一种开放性技术,不仅可以用在JVM本身的管理上,还可以运行于JVM之上的软件中,如中间间大多也基于JMX来实现管理和监控。JVM对于JMX MBean的访问也是完全开放的,可以使用代码调用API,支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问 启动JConsole 通过JDK/bin目录下的jconsole.exe启动JCon-sole后,会自动搜索出本机运行的所有JVM进程,而不需要用户自己使用jps查询 内存监控: “内存”页签的作用相当于可视化的jstat命令。用于监视被收集器管理的JVM内存的变化趋势 线程监控:功能相当于可视化的jstack命名,遇到线程停顿的时候可以使用这个页签的功能进行分析 VisualVM:多合-故障处理工具(功能最强大的运行监视和故障处理程序之一) 优点:不需要被监视的程序基于特殊Agent去运行,通用性很强,对应用程序实际性能的影响也较小 Java Mission Control :可持续在线的监视工具 JM从不仅可以下载到独立程序,更常见的是作为Eclipse的插件使用。JMC与JVM之间同样采取JMX协议进行通信,JMC一方面作为JMX控制台,显示来自JVM MBean提供的数据;另一方面作为JFR的分析工具,展示来自JFR的数据 HSDIS:JIT生成代码反汇编 是一款被官方推荐的HotSpotJVM即时编译代码的反汇编插件,包含在HotSpotJVM的源码当中 HDIS插件的作用是让HotSpot的-XX:+PrintAssembly指令调用它来把即时编译器动态生成的本地代码还原为汇编代码输出,同时还会自动产生大量非常有价值的注释 调优案例分析与实战 大内存硬件上的程序部署策略 目前单体应用在较大内存的硬件上主要的部署方式有两种:1、通过一个单独的JVM实例来管理大量的Java堆内存  2、同时使用若干个JVM,建立逻辑集群来利用硬件资源 集群间同步导致的内存溢出 在服务使用过程中,往往一个页面会产生数次乃至数十次的请求,因此这个过滤器导致集群各个节点之间网络交换非常频繁,当网络情况不能满足传送要求时,数据在内存中不断堆积,很快就产生了内存溢出 堆外内存导致的溢出错误 垃圾收集进行时,JVM虽会对直接内存进行回收,但直接内存却不像新生代、老年代那样,发现空间不足就主动通知收集器进行垃圾回收,它只能等待老年代满后Full GC出现后,“顺便”帮它清理掉内存的废弃对象,否则就一直等到抛出内存溢出异常时,先捕获到异常,再在Catch块里面通过System.gc()命令来触发垃圾收集 如果JVM再打开了-XX:DisbaleExplicitGC开关,禁止人工触发垃圾收集的话,堆中会有许多空间内存,不得不抛出内存溢出异常 不恰当数据结构导致内存占用过大 在分析数据文件期间,800MB的Eden空间很快被填满引发垃圾收集,但Minor GC之后,新生代中绝大部分对象依然是存活的。如ParNew收集器使用的是复制算法,算法的高效是建立在大部分对象都“朝生夕灭”的特性上的,如果存活对象过多,把这些对象复制到Survivor并维持对象引用的正确性就是一个沉重的负担,因此导致垃圾收集的暂停时间明显变长 类文件结构 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步 无关性的基石 各种不同平台的JVM以及所有平台都统一支持的程序存储格式—字节码是构成平台无关性的基石 实现语言无关性的基础仍然是JVM和字节码存储格式。JVM不与包括Java语言在内的任何语言绑定,只与“Class文件”这种特定的二进制文件格式所关联,Class文件包含了JVM指令集,符号表以及若干其他辅助信息 Class类文件的结构 任何一个Class文件都对应着唯一的一个类或接口的定义信息 Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,会按照高位在前的方式分割成若干个8个字节进行存储 魔数与Class文件的版本 每个Class文件的头4个字节称为魔数,唯一的作用是确定这个文件是否为一个能被JVM接受的Class文件;不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯,如图片格式,GIF或JPEG等在文件中都存有魔数。紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个字节是此版本号,第7个和第8个字节是主版本号 常量池 紧接着主次版本号之后的是常量池入口,相当于Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,还是在Class文件中第一次出现的表类型数据项目 常量池中常量的数量是不固定的,所以在常量池入口需要放置一项UL类型的数据,代表常量池容量计数值 Java代码在进行Javac编译的时候,在JVM加载Class文件的时候进行动态连接;在Class文件中不会保存各个方法,字段最终在内存中的布局信息,这些字段、方法的符号引用不经过JVM在运行期转换的话无法得到真正的内存入口地址,即无法直接被JVM使用。当JVM做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中 访问标志 在常量池结束之后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等 JVM类加载机制 JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这个过程称为JVM的类加载机制 类加载时机 一个类型从被加载到JVM内存中开始,到卸载出内存为止,整个生命周期经历了加载、验证、准备、解析、初始化、使用、卸载7个阶段,其中验证、准备、解析统称为连接。解析在某些情况下可以在初始化阶段之后再开始,是为了支持Java语言的运行时绑定特性 有且只有6钟情况必须立即对类进行“初始化” 1、遇到new getstatic  putstatic  invokestatic这4条字节码指令时,如果类型没有初始化,就需要先触发其初始化阶段。生成这4种指令的典型Java代码场景有:使用new关键字实例化对象的时候;读取或设置一个类型的静态字段;调用一个类型的静态方法的时候 2、使用java.lang.reflect包的方法对类型进行发射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化 3、在初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 4、当JVM启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),JVM先初始化这个主类 5、当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic   REF_putStatic  REF_invokeStatic  REF_newInvokeSpecial 4种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化 6、当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化 类加载的过程   5个阶段:加载、验证、准备、解析、初始化 加载 在加载阶段,JVM需要完成一下三件事情: 1、通过一个类的全限定名来获取定义此类的二进制字节流 2、将这个字节流所代表的静态存储结构软化为方法区的运行时数据结构 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口 加载阶段既可以使用JVM里内置的引导类加载器来完成,也可以由用户自定义的类加载器完成,开发人员通过定义自己的类加载器区控制字节流的获取方式,实现根据自己的想法来赋予应用程序获取运行代码的动态性 对于数组而言,情况就有所不同,数据类本身不通过类加载器创建,是由JVM直接在内存中动态构造出 验证 验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合《JVM规范》的全部约束要求,保证这些信息被当做代码运行后不会危害JVM自身的安全 验证大致会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证 文件格式验证 验证字节流是否符合Class文件格式的规范,并且能被当前版本的JVM处理,包括一下这些验证点(不止这些): 1、是否以魔数0xCAFEBABE开头  2、主次版本是否在当前JVM接受范围之内   3、常量池的常量中是否有不被支持的常量类型(检查常量Tag标志 )   4、指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量 元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,可能的验证点如下 1、这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)   2、这个类的父类是否继承了不允许被继承的类(被final修改的类)    3、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法 此阶段的目的是对类的元数据信息进行校验,保证不存在与《Java语言规范》定义相悖的元数据信息 字节码验证 验证过程最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。此阶段要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害JVM安全的行为 如:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况 符号引用验证 最后一个阶段发生在Java将符号引用转化为直接引用的时候,可以看作是对类自身以外的各类信息进行匹配性校验,通常需要校验一下内容 1、符号引用中通过字符串描述的全限定名是否能找到对应的类 2、在指定的类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。符号引用的目标是确保解析行为能正常执行,如果无法通过符号引用验证,JVM将会抛出一个java.lang.IncompatibleClassChangeError的子类异常 准备 正式为类中定义的类型(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段 首先进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:public static int value = 123 ; 变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值给123的putstatic指令是程序被编译后存活于类构造器<clinit>()方法之中,所以把value赋值给123的动作要到类的初始化阶段才会被执行 解析 是JVM将常量池内的符号引用替换为直接引用的过程 类或接口的解析 假设当前代码所处的类为D,如果把一个未解析过的符号引用N解析为一个类或接口C的直接引用,整个过程包括3个步骤 1、如果C不是一个数组类型,那JVM将会把代表N的全限定名传递给D的类加载器去加载这个类C。在类加载过程中,由于 元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作 2、如果C是一个数组类型,并且数组元素类型为对象,将会按照第一点的规则加载数组元素类型 3、如果上面两步没有出现任何异常,那么C在JVM实际上已经成为一个有效的类或接口了,但在解析完成前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常 字段解析 要解析一个未被解析过的字段符号引用,首先将会对字段表内class-index项中索引的CONSTAT_Class_info符号引用进行解析,如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那把这个字段所属的类或接口用C表示  步骤如下 1、如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束 2、否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束 3、否则,如果C不是java.lang.Object的话,将会按照继承从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束 方法解析 与字段解析一样先解析出方法表的class_index项中索引的方法所属的类或接口的符号引用,若解析成功,用C表示这个类 1、由于Class文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现class_index中索引的C是个接口的话,直接抛出java.lang.IncompatibleClassChangeError异常 2、如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束 3、否则,类C的父类递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束 4、否则,在类C实现的接口列表及它们的父类之中递归查找是否有简单名称和描述符都与目标相匹配的方法,若存在匹配的方法,说明类C是一个抽象类,此时查找结束,抛出java.lang.AbstractMethodError异常 5、否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError 初始化 初始化阶段就是执行类构造器<clinit>()方法的过程 <clinit>()方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问 JVM必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那只会有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法 类加载器 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在JVM中的唯一性,每一个类加载器都拥有一个独立的类名称空间 双亲委派模型 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己加载 使用双亲委派模型来组织类加载器之间的关系,好处是Java中的类随着它的类加载器一起具备了一种优先级的层次关系。例如java.lang.Object,它存放在rt.jar之中,无论哪一类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类 虚拟机字节码执行引擎 运行时栈帧结构 “栈帧”是用于支持JVM进行方法调用和方法执行背后的数据结构,也是JVM运行时数据区中的JVM栈的栈元素 局部变量表 一组变量值的存储空间,用于存放方法参数和方法局部定义的局部变量。在Java程序被编译为Class文件时,在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。局部变量表的容量以变量槽为最小单位 一个变量槽可以存放一个32位以内的数据类型,有boolean  byte  char  short  int  float  reference  returnAdress;其中reference类型表示对一个对象实例的引用;returnAddress很少见,为字节码指令jsr  jsr-w  ret服务,指向了一条字节码指令的地址 JVM通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量;当一个方法被调用时,JVM会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递 为了节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用 操作数栈 常称为操作栈,是一个后入先出栈,同局部变量表一样,操作数栈最大深度也在编译的时候被写入到code属性的max_stacks数据项之中 当一个方法刚开始执行的时候,方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,即出栈和入栈操作 动态连接 每个栈帧包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接 方法返回地址 每一次方法开始执行后,只有两种方式退出这个方法,第一种是执行引擎遇到任意一个方法返回的字节码指令,可能会有返回值传递给上层方法调用者,方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,该方式称为“正常调用完成”  第二种方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理,无论是JVM内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,会导致方法退出,这种方式称为“异常调用完成” 方法调用 唯一的任务就是确定被调用方法的版本,暂时还未涉及方法内部的具体运行过程 解析 调用目标在程序代码写好,编译器进行编译那一刻就已经确定下来,这类方法的调用被称作解析 JVM支持以下5种方法调用字节码指令 1、invokestatic用于调用静态方法    2、invokespecial用于调用实例构造器<clinit>()方法,私有方法和父类中的方法 3、invokevirtual用于调用所有的虚方法   4、invokeinterface用于调用接口方法,会在运行时再确定一个实现该接口的对象 5、invokedynamic先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法 解析调用一定是个静态过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期完成 分派 静态分派 静态类型和实际类型在程序种都可能会发生变化,区别是静态类型的变化仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可以确定,编译器在编译程序时不知一个对象的实际类型 单分派和多分派 单分派根据一个字量对目标方法进行选择,多分派是根据多于一个字量对目标方法进行选择 JVM动态分派的实现 在方法区建立一个虚放发表,使用虚方法表索引来代替元数据查找以提高性能

这篇关于深入理解Java虚拟机——JVM的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程