前言 Java是目前用户最多、使用范围最广的软件开发技术,Java的技术体系主要由支撑Java程序运行的虚拟机、提供各开发领域接口支持的Java类库、Java编程语言及许许多多的第三方Java框架(如Spring、MyBatis等)构成。在国内,有关Java类库API、Java语言语法及第三方框架的技术资料和书籍非常丰富,相比而言,有关Java虚拟机的资料却显得异常贫乏。 这种状况很大程度上是由Java开发技术本身的一一个重要优点导致的:在虚拟机层面隐藏了底层技术的复杂性以及机器与操作系统的差异性。运行程序的物理机千差万别,而Java虚拟机则在千差万别的物理机上面建立了统一的运行平台,实现了在任意一台Java虚拟机上编译的程序,都能在任何其他Java虚拟机上正常运行。这一极大的优势使得Java应用的开发比传统CC应用的开发更高效快捷,程序员可以把主要精力放在具体业务逻辑,而不是放在保障物理硬件的兼容性上。通常情况下,一个程序员只要了解了必要的Java类库API、Java语法,学习适当的第三方开发框架,就已经基本满足日常开发的需要了。虚拟机会在用户不知不觉中完成对硬件平台的兼容及对内存等资源的管理工作。因此,了解虚拟机的运作并不是普通开发人员必备的,或者说首要学习的知识。 其实,目前商用的高性能Java虚拟机都提供了相当多的优化参数和调节手段,用于满足应用程序在实际生产环境中对性能和稳定性的要求。如果只是为了入门学习,让程序在自己的机器上正常工作,那么这些特性可以说是可有可无的;但是,如果用于生产开发,尤其是大规模的、企业级的生产开发,就迫切需要开发人员中至少有一部分人对虚拟机的特性及调节方法具有很清晰的认识。所以在Java开发体系中,对架构师、系统调优师、高级程序员等角色的需求一直都非常大。 学习虚拟机中各种自动运作特性的原理也成为Java程序员成长路上最终必然会接触到的一课。通过本文,读者可以以一个相对轻松的方式学到虚拟机的运作原理。 参考:JVM规范,MemoriesofaJavaRuntime 堆:JVM启动时按Xmx,Xms大小创建的内存区域,用于分配对象、数组所需内存,由GC管理和回收 方法区:存储被JVM加载的类信息(字段、成员方法的字节码指令等)、运行时常量池(字面量、符号引用等)、JIT编译后的CodeCache等信息;JDK8前Hotspot将方法区存储于永久代堆内存,之后参考JRockit废弃了永久代,存储于本地内存的Metaspace区 直接内存:JDK1。4引入NIO使用NativeUnsafe库直接分配系统内存,使用Buffer,Channel与其交互,避免在系统内存与JVM堆内存之间拷贝的开销线程私有内存:程序计数器:记录当前线程待执行的下一条指令位置,上下文切换后恢复执行,由字节码解释器负责更新JVM栈描述Java方法执行的内存模型:执行新方法时创建栈帧,存储局部变量表、操作数栈等信息存储单位:变量槽slot,long,double占2个slot,其他基本数据类型、引用类型占1个,故表的总长度在编译期可知本地方法栈:执行本地CC方法2。2JVM对象创建对象 分配堆内存:类加载完毕后,其对象所需内存大小是确定的;堆内存由多线程共享,若并发创建对象都通过CAS乐观锁争夺内存,则效率低。故线程创建时在堆内存为其分配私有的分配缓冲区(TLAB:ThreadLocalAllocationBuffer)内存模型 分配流程 注:当TLAB剩余空间不足以分配新对象,但又小于最大浪费空间阈值时,才会加锁创建新的TLAB 零值初始化对象的堆内存、设置对象头信息、执行构造函数()V2。对象的内存布局对象头MarkWord:记录对象的运行时信息,如hashCode,GC分代年龄,尾部2bit用于标记锁状态 ClassPointer:指向所属的类信息数组长度(可选,对象为数组):4字节存储其长度 对象数据:各种字段的值,按宽度分类紧邻存储 对齐填充:内存对齐为1个字长整数倍,减少CPU总线周期 验证:openjdkjol检查对象内存布局publicclassUser{privateintage1;privateStringnameunknown;}javajarDownloadsjolclilatest。jarinternalscp。com。jol。UserOFFSZTYPEDESCRIPTIONVALUE08(objectheader:mark)0x0000000000000001(nonbiasable;age:0)84(objectheader:class)0xf8021e85User。class引用地址124intUser。age1基本类型则直接存储值164java。lang。StringUser。name(object)引用类型,指向运行时常量池中的String对象204(objectalignmentgap)有4字节的内存填充Instancesize:24bytes2。3内存溢出 堆内存:Xms指定堆初始大小,当大量无法被回收的对象所占内存超出Xmx上限时,将发生内存溢出OutOfMemoryError排查:通过EclipseMAT分析XX:HeapDumpOnOutOfMemory生成的。hprof堆转储文件,定位无法被回收的大对象,找出其GCRoot引用路径解决:若为内存泄露,则修改代码用null显式赋值、虚引用等方式及时回收大对象;若为内存溢出,大对象都是必须存活的,则调大Xmx、减少大对象的生命周期、检查数据结构使用是否合理等Xms20mXmx20mXX:HeapDumpOnOutOfMemoryErrorpublicclassHeapOOM{staticclassOOMObject{}publicstaticvoidmain(String〔〕args){ListOOMObjectvsnewArrayList();while(true)vs。add(newOOMObject());}} 分析GCRoot发现com。ch02。HeapOOM对象间接引用了大量的OOMObject对象,共占用15。4MB堆内存,无法回收最终导致OOM 栈内存:Xss指定栈大小,当栈深度超阈值(比如未触发终止条件的递归调用)、本地方法变量表过大等,都可能导致内存溢出StackOverflowError 方法区:XX:MetaspaceSize指定元空间初始大小,XX:MaxMetaspaceSize指定最大大小,默认1无限制,若在运行时动态生成大量的类,则可能触发OOM 运行时常量池:strObj。intern()动态地将首次出现的字符串对象放入字符串常量池并返回,JDK7前会拷贝到永久代,之后则直接引用堆对象Strings1java;类加载时,从字节码常量池中拷贝符号到了运行时常量池,在解析阶段初始化的字符串对象Strings2j;Strings3s2ava;堆上动态分配的字符串对象println(s3s1);falseprintln(s3。intern()s1);true已在字符串常量池中存在 直接内存:XX:MaxDirectMemorySize指定大小,默认与Xmx一样大,不被GC管理,申请内存超阈值时OOMch03。垃圾回收与内存分配 GC可分解为3个子问题:which(哪些内存可被回收)、when(什么时候回收)、how(如何回收)3。1GC条件1。引用计数算法(referencecounting) 原理:每个对象都维护一个引用计数器rc,当通过赋值、传参等方式引用它时rc,当引用变量修改指向、离开函数作用域等方式解除引用时rc,递减到0时说明对象无法再被使用,可回收。伪代码:assign(var,obj):incrref(obj)selfself先增再减,避免引用自身导致内存提前释放decrref(var)varobjincr(obj):obj。rcdecr(obj):obj。rcifobj。rc0:removeref(obj)断开obj与其他对象的引用关系gc(obj)回收obj内存 优点:思路简单,对象无用即回收,延迟低,适合内存少的场景 缺点:此算法中对象是孤立的,无法在全局视角检查对象的真实有效性,循环引用的双方对象需引入外部机制来检测和回收,如下图红色圈(图源:whatisgarbagecollection) 2。可达性分析算法(reachabilityanalysis) 原理:从肯定不会被回收的对象(GCRoots)出发,向外搜索全局对象图,不可达的对象即无法再被使用,可回收;常见可作为GCRoot的对象有:执行上下文:JVM栈中参数、局部变量、临时变量等引用的堆对象全局引用:方法区中类的静态引用、常量引用(如StringTable中的字符串对象)所指向的对象 优点:无需对象维护GC元信息,开销小;单次扫描即可批量识别、回收对象,吞吐高 缺点:多线程环境下对象间的引用关系随时在变化,为保证GCRoot标记的准确性,需在不变化的snapshot中进行,会产生StopTheWorld(以下简称STW)卡顿现象 3。四种引用类型 示例:限制堆內存50MB,其中新生代30MB,老年代20MB;依次分配5次10MB的byte〔〕对象,仅使用软引用来引用,观察GC过程publicstaticvoidmain(String〔〕args){softRefListSoftReference10MBbyte〔〕ListSoftReferencebyte〔〕softRefListnewArrayList();ReferenceQueuebyte〔〕softRefQueuenewReferenceQueue();无效引用队列for(inti0;i5;i){SoftReferencebyte〔〕softRefnewSoftReference(newbyte〔1010241024〕,softRefQueue);softRefList。add(softRef);for(SoftReferencebyte〔〕ref:softRefList)dump所有软引用指向的对象,检查是否已被回收System。out。print(ref。get()null?gced:ok);System。out。println();}Referencelt;?extendsbyte〔〕refsoftRefQueue。poll();while(ref!null){softRefList。remove(ref);解除对软引用对象本身的引用refsoftRefQueue。poll();}System。out。println(effectivesoftref:softRefList。size());2}javaverbose:gcXX:NewSize30mXms50mXmx50mXX:PrintGCDetailscom。ch02。DemoRefokokok分配第三个〔〕byte时,EdenGC无效,触发FullGC将一个〔〕byte晋升到老年区此时三个byte〔〕都只被软引用所引用,被标记为待二次回收(若为弱引用,此时Eden已被回收)〔GC(AllocationFailure)〔PSYoungGen:21893K21893K(27136K)〕21893K32141K(47616K),0。0046324secs〕〔FullGC(Ergonomics)〔PSYoungGen:21893K10527K(27136K)〕〔ParOldGen:10248K10240K(20480K)〕32141K20767K(47616K),〔Metaspace:2784K2784K(1056768K)〕,0。004secs〕okokok再次GC,前三个byte〔〕全部被回收〔GC(AllocationFailure)〔PSYoungGen:20767K20767K(27136K)〕31007K31007K(47616K),0。0007963secs〕〔FullGC(Ergonomics)〔PSYoungGen:20767K20759K(27136K)〕〔ParOldGen:10240K10240K(20480K)〕31007K30999K(47616K),〔Metaspace:2784K2784K(1056768K)〕,0。003secs〕〔GC(AllocationFailure)〔PSYoungGen:20759K20759K(27136K)〕30999K30999K(47616K),0。0007111secs〕〔FullGC(AllocationFailure)〔PSYoungGen:20759K0K(27136K)〕〔ParOldGen:10240K267K(20480K)〕30999K267K(47616K),〔Metaspace:2784K2784K(1056768K)〕,0。003secs〕gcedgcedgcedokgcedgcedgcedokok4。finalize 原理:若对象不可达,被标记为可回收后,会进行finalize()是否被重写、是否已执行过等条件筛选,若通过则对象会被放入FQueue队列,等待低优先级的后台Finalizer线程触发其finallize()的执行(不保证执行结束),对象可在finalize中建立与GCRoot对象图上任一节点的引用关系,来逃脱GC 使用:finalize机制与C中的析构函数并不等价,其执行结果并不确定,不推荐使用,可用tryfinally替代3。2GC算法分代收集理论 两个分代假说:符合大多数程序运行的实际情况弱分代假说:绝大多数对象是朝生夕灭,生存时间极短强分代假说:熬过越多次GC的对象,越可能被继续使用,越难以回收 对应地,JVM堆被划分为2个不同区域,将对象按年龄分类,兼顾了GC耗时与内存利用率新生代:大量对象将被回收,只关注仍存活的对象,逐步晋升老年代:大量对象不被回收,只关注要被回收的对象 跨代引用问题:老年代会引用新生代,新生代GC时需遍历老年代中大量的存活对象,分析可达性,时间复杂度高背景:相互引用的对象倾向于同时存亡,比如跨代引用关系中的新生代必然会逐步晋升,最终消除跨代关系假说:跨代引用相比同代引用只占极少数,无需全量扫描老年代实现:新生代维护全局数据结构:记忆集(RememberedSet),将老年代分为多个子块,标记存在跨代引用的子块,等待后续扫描;代价:为保证记忆集的正确性,需在跨代引用建立或断开时保持同步 1。标记清除:MarkSweep原理:标记不可达对象,统一清理回收,反之亦可缺点:执行效率不稳定,回收耗时取决于活跃对象的数量;内存碎片多,会出现内存充足但无法分配过大的连续内存(数组) 2。标记复制:MarkCopy理论:将堆内存切为两等份A,B,每次仅使用A,用完后标记存活对象复制到B,清空A后执行swap优点:直接针对半区回收,无内存碎片问题;分配内存只需移动堆顶指针,高效顺序分配缺点:当A区有大量存活对象时,复制开销大;B区长时间闲置,内存浪费严重实践:对于存活对象少的新生代,无需按1:1分配,而是按8:1:1的内存布局,其中Eden和From区同时使用,只有To区会被闲置(担保机制:若To区不够容纳MinorGC后的存活对象,则晋升到老年区) 3。标记整理:MarkCompact原理:标记存活对象后统一移动到内存空间一侧,再回收边界之外的内存优点:内存模型简单,无内存碎片,降低内存分配和访问的时间成本,能提高吞吐缺点:对象移动需STW同步更新引用关系,会增加延迟 3。3HotSpotGC算法细节1。发起GC:安全点与安全区域问题:为保证可达性分析结果的准确性,需挂起用户线程(STW),再从各线程的执行上下文中收集GCRoot,如何通知线程挂起?安全点:HotSpot内部有线程中断标记;在各线程的方法调用、循环跳转、异常跳转等会长时间执行的指令处,额外插入检查该标记的test高效指令;若轮询发现标记为真,线程会主动在最近的SafePoint处挂起,此时其栈上对象的引用关系不再变化,可收集GCRoot对象安全区域:引用关系不会变化的指令区域,可安全地收集GCRoot;线程离开此区域时,若GCRoot收集过程还未结束,则需等待 示意图 2。加速GC:CardTable问题:非收集区域(老年代)会存在到收集区域(新生代)的跨代引用,如何避免对前者的全量扫描? 卡表:记忆集的字节数组实现;将老年代内存划分为CardPage(512KB)大小的子内存块,若新建跨代引用,则将对应的Card标记为dirty,GC时只需扫描老年代中被标记为dirty的子内存块 写屏障:有别于volatile禁用指令重排的内存屏障,GC中的写屏障是在对象引用更新时执行额外hook动作的机制。简单实现:voidoopfieldstore(oopfield,oopnewval){oop:ordinaryobjectpointerprewritebarrier(field,newval);写前屏障:更新前先执行,使用oop旧状态fieldnewval;postwritebarrier(field,newval);写后屏障:更新完才执行} 使用写屏障保证CardTable的实时更新(图源:TheJVMWriteBarrierCardMarking) 3。正确GC:并发可达性分析参考演讲:Shenandoah:TheGarbageCollectorThatCouldbyAlekseyShipilev 问题:GCRoots的对象源固定,故枚举时STW时间短暂且可控。但后续可达性分析的时间复杂度与堆中对象数量成正相关,即堆中对象越多,对象图越复杂,堆变大后STW时间不可接受 解决:并发标记。引出新问题:用户线程动态建立、解除引用,标记过程中图结构发生变化,结果不可靠;证明:用三色法描述对象状态白色:未被回收器访问过的对象;分析开始都是白色,分析结束还是白色则不可达灰色:被回收器访问过,但其上至少还有1个引用未被扫描(中间态)黑色:被回收器访问过,其上引用全部都已被扫描,存在引用链,为存活对象;若其他对象引用了黑色对象,则不必再扫描,肯定也存活;黑色不可能直接引用白色 STW无并发的正确标记:顶部3个对象将被回收 用户线程并发修改引用,会导致标记结果无效,分2种情况:少回收,对象标记为存活,但用户解除了引用:产生浮动垃圾,可接受,等待下次GC 误回收,对象标记为可回收,但用户新建了引用:实际存活对象被回收,内存错误 论文《UniprocessorGarbageCollectionTechniquesPaulR。Wilson》3。2证明了实际存活的对象被标记为可回收必须同时满足两个条件(有时间序)插入一条或多条从黑色到白色的新引用删除所有灰色到该白色的直接、间接引用 为正确实现标记,打破其中一个条件即可(类比打破死锁四个条件之一的思想),分别对应两种方案:增量更新IncrementUpdate:记录黑到白的引用关系,并发标记结束后,以黑为根,重新扫描;A直接存活原始快照SATB(SnapshotAtTheBegining):记录灰到白的解引用关系,并发标记结束后,以灰为根,重新扫描;B为灰色,最后变为黑色,存活。需注意,若没有步骤3,则B,C变为浮动垃圾3。4经典垃圾回收器 搭配使用示意图: 1。Serial,SerialOld 原理:内存不足触发GC后会暂停所有用户线程,单线程地在新生代中标记复制,在老年代中标记整理,收集完毕后恢复用户线程 优点:全程STW简单高效 缺点:STW时长与堆对象数量成正相关,且GC线程只能用到1core无法加速 场景:单核CPU且可用内存少(如百兆级),JDK1。3之前的唯一选择 2。ParNew 原理:多线程并行版的Serial实现,能有效减少STW时长;线程数默认与核数相同,可配置 场景:JDK7之前搭配老年代的CMS回收器使用 3。Parallel,ParallelOld 垃圾回收有两个通常不可兼得的目标低延迟:STW时长短,响应快;允许高频、短暂GC,比如调小新生代空间,加快收集延迟(吞吐下降)高吞吐量:用户线程耗时(用户线程耗时GC线程耗时)高,GC总时间低;允许低频、单次长时间GC,(延迟增加) 原理:与ParNew类似都是并行回收,主要增加了3个选项(倾向于提高吞吐量)XX:MaxGCPauseTime:控制最大延迟XX:GCTimeRatio:控制吞吐(默认99)XX:UseAdaptiveSizePolicy:启用自适应策略,自动调整Eden与2个Survivor区的内存占比XX:SurvivorRatio,老年代晋升阈值XX:PretenureSizeThreshold 4。CMS CMS:ConcurrentMarkSweep,即并发标记清除,主要有4个阶段初始标记(initialmark):STW快速收集GCRoots并发标记(concurrentmark):从GCRoots出发检测引用链,标记可回收对象;与用户线程并发执行,通过增量更新来避免误回收重新标记(remark):STW重新分析被增量更新所收集的GCRoots并发清除(concurrentsweep):并发清除可回收对象 优点:两次STW时间相比并发标记耗时要短得多,相比前三种收集器,延迟大幅降低 缺点CPU敏感:若核数较少(4core),并发标记将占用大量CPU时间,会导致吞吐突降无法处理浮动垃圾:XX:CMSInitiatingOccupancyFration(默认92)指定触发CMSGC的阈值;在并发标记、并发清理的同时,用户线程会产生浮动垃圾(引用可回收对象、产生新对象),若浮动垃圾占比超过XX:CMSInitiatingOccupancyFration;若GC的同时产生过多的浮动垃圾,导致老年代内存不足,会出现CMS并发失败,退化为SerialOld执行FullGC,会导致延迟突增无法避免内存碎片:XX:CMSFullGCsBeforeCompaction(默认0)指定每次在FullGC前,先整理老年代的内存碎片5。G1 特点:基于region内存布局实现局部回收;GC延迟目标可配置;无内存碎片问题 跨代引用:各region除了用卡表标记各卡页是否为dirty之外,还用哈希表记录了各卡页正在被哪些region引用,通过这种双向指针机制,能直接找到Old区,避免了全量扫描(G1自身内存开销大头) G1GC有3个阶段(参考其GC日志)新生代GC:Eden区占比超阈值触发;标记存活对象并复制到Survivor区,其内可能有对象会晋升到Old区 老年代GC:Old区占比达到阈值后触发,执行标记整理初始标记:枚举GCRoots,已在新生代GC时顺带完成并发标记:并发执行可达性分析,使用SATB记录引用变更重新标记:SATB分析,避免误回收筛选回收:将region按回收价值和时间成本筛选组成回收集,STW将存活对象拷贝到空regions后清理旧regions,完成回收混合GC 参数控制(文档:HotSpotGCTuningGuide) 3。8内存分配策略 使用Serial收集器XX:UseG1GC演示1。对象优先分配在Eden区 新对象在Eden区分配,空间不足则触发MinorGC,存活对象拷贝到ToSurvivor,若还是内存不足则通过分配担保机制转移到老年区,依旧不足才OOMbyte〔〕buf1newbyte〔6MB〕;byte〔〕buf2newbyte〔6MB〕;10MB的eden区剩余4MB,空间不足,触发minorGCjavaverbose:gcXms20mXmx20mXmn10mXX:PrintGCDetailsXX:UseSerialGCcom。ch03。Allocationminorgc后新生代内存从6M降到0。2M,存活对象移到了老年区,总的堆内存用量依旧是6MB〔GC(AllocationFailure)〔DefNew:6823K286K(9216K),0。002secs〕6823K6430K(19456K),0。002secs〕〔Times:user0。00sys0。00,real0。00secs〕Heapdefnewgenerationtotal9216K,used6513Kedenspace8192K,76usedbuf2fromspace1024K,28usedtospace1024K,0usedtenuredgenerationtotal10240K,used6144Kthespace10240K,60usedbuf12。大对象直接进入老年区 对于Serial,ParNew,可配置超过阈值XX:PretenureSizeThreshold的大对象(连续内存),直接在老年代中分配,避免触发minorgc,导致Eden和Survivor产生大量的内存复制操作byte〔〕buf1newbyte〔4MB〕;javaverbose:gcXms20mXmx20mXmn10mXX:PrintGCDetailsXX:UseSerialGCXX:PretenureSizeThreshold3145728com。ch03。Allocation3145728即3MBHeapdefnewgenerationtotal9216K,used843Kedenspace8192K,10usedfromspace1024K,0usedtospace1024K,0usedtenuredgenerationtotal10240K,used4096Kthespace10240K,40usedbuf13。长期存活的对象进入老年代 对象头中4bit的age字段存储了对象当前GC分代年龄,当超过阈值XX:MaxTenuringThreshold(默认15,也即age字段最大值)后,将晋升到老年代,可搭配XX:PrintTenuringDistribution观察分代分布byte〔〕buf1newbyte〔MB16〕;byte〔〕buf2newbyte〔4MB〕;byte〔〕buf3newbyte〔4MB〕;触发minorgcbuf3null;buf3newbyte〔4MB〕;javaverbose:gcXms20mXmx20mXmn10mXX:PrintGCDetailsXX:UseSerialGCXX:MaxTenuringThreshold1XX:PrintTenuringDistributioncom。ch03。Allocation〔GC(AllocationFailure)〔DefNewDesiredsurvivorsize524288bytes,newthreshold1(max1)age1:359280bytes,359280total:4839K350K(9216K)〕4839K4446K(19456K),0。0017247secs〕至此,buf1熬过了第一次收集,age1〔GC(AllocationFailure)〔DefNewDesiredsurvivorsize524288bytes,newthreshold1(max1):4446K0K(9216K)〕8542K4438K(19456K)〕Heapdefnewgenerationtotal9216K,used4178Kedenspace8192K,51usedfromspace1024K,0usedbuf1在第二轮收集中被提前晋升tospace1024K,0usedtenuredgenerationtotal10240K,used4438Kthespace10240K,43used4。分代年龄动态判定 XX:MaxTenuringThreshold并非晋升的最低硬性门槛,当Survivor中同龄对象超50后,大于等于该年龄的对象会被自动晋升,哪怕还没到阈值5。空间分配担保 老年代作为ToSurvivor区的担保区域,当EdenFromSurvivor中存活对象的总大小超出ToSurvivor时,将尝试存入老年代。JDK6之后,只要老年代的连续空间大于新生代对象的总大小,或之前晋升的平均大小,则只会进行MinorGC,否则进行FullGCch06。类文件结构 Class文件实现语言无关性,JVM实现平台无关性,参考《Java虚拟机规范》 一个Class文件描述了一个类或接口的明确定义,文件内容是一组以8字节为单位的二进制流,各数据项间没有分隔符,超过8字节的数据项按BigEndian切分后存储。数据项分两种:无符号数:描述基本类型;用u1,u2,u4,u8分别表示1,2,4,8字节长度的无符号数;存储数字值、索引序号、UTF8编码值等表:由无符号数、其他表嵌套构成的复合类型;约定info后缀;存储字段类型、方法签名等6。1结构定义语法参考文档:TheclassFileFormatClassFile{u4magic;魔数u2minorversion;版本号u2majorversion;u2constantpoolcount;常量池cpinfoconstantpool〔constantpoolcount1〕;u2accessflags;类访问标记u2thisclass;本类全限定名u2superclass;单一父类u2interfacescount;多个接口u2interfaces〔interfacescount〕;u2fieldscount;字段表fieldinfofields〔fieldscount〕;u2methodscount;方法表methodinfomethods〔methodscount〕;u2attributescount;类属性attributeinfoattributes〔attributescount〕;}magic:魔数,简单识别。class文件,值固定为0xCAFEBABEminorversion,majorversion:Class文件的次、主版本号constantpoolcount:常量池大小1constantpool:常量池,索引从1开始,0值被预留表示不引用任何常量池中的任何常量;常量分两类字面量:如UTF8字符串、int、float、long、double等数字常量符号引用:类、接口的全限定名、字段名与描述符、方法类型与描述符等现有常量共计17种,常量间除了都使用u1tag前缀标识常量类型外,结构互不相同,常见的有:CONSTANTUtf8info:保存由UTF8编码的字符串CONSTANTUtf8info{u1tag;值为1u2length;bytes数组长度,u2最大值65535,即单个字符串字面量不超过64KBu1bytes〔length〕;长度不定的字节数组}CONSTANTClassinfo:表示类或接口的符号引用CONSTANTClassinfo{u1tag;值为7u2nameindex;指向全限定类名的Utf8info常量间存在层级组合关系}CONSTANTFieldrefinfo,CONSTANTMethodrefinfo,CONSTANTNameAndTypeinfo:成员变量、成员方法及其类型描述符CONSTANTFieldrefinfo{u1tag;值为9u2classindex;所属类u2nameandtypeindex;字段的名称、类型描述符}CONSTANTMethodrefinfo{u1tag;值为10u2classindex;所属类u2nameandtypeindex;方法的名称、签名描述符}CONSTANTNameAndTypeinfo{u1tag;值为12u2nameindex;字段或方法的名称u2descriptorindex;类型描述符} 如上只列举了其中5种常量的结构,可见常量间通过组合的方式,来描述层级关系accessflags:类的访问标记,有16bit,每个标记对应一个位,比如ACCPUBLIC对应0x0001,表示类被public修饰;其他8个标记参考Opcodes。ACCXXXthisclass,superclass:指向本类、唯一父类的Classinfo符号常量interfacecount,interfaces:描述此类实现的多个接口信息fieldscount,fields:字段表;描述类字段、成员变量的个数及详细信息fieldinfo{u2accessflags;作用域、static,final,volatile等访问标记u2nameindex;字段名u2descriptorindex;类型描述符u2attributescount;字段的属性表attributeinfoattributes〔attributescount〕;} 类型描述符简化描述了字段的数据类型、方法的参数列表及返回值,与Java中的类型对于关系如下:基本类型:Z:boolean,B:byte,C:char,S:short,I:int,F:float,D:double,J:longvoid及引用类型:V:void引用类型:L:,类名中的。替换为,添加;分隔符,如Object类描述为LjavalangObject;数组类型:每一维用一个前置〔表示示例:booleanregionMatch(int,String,int,int)对应描述符为(ILjavalangString;II)Zmethodscount,methods:方法表;完整描述各成员方法的修饰符、参数列表、返回值等签名信息methodinfo{u2accessflags;访问标记u2nameindex;方法名u2descriptorindex;方法描述符u2attributescount;方法属性表attributeinfoattributes〔attributescount〕;} 字段表、方法表都可以带多个属性表,如常量字段表、方法字节码指令表、方法异常表等。属性模板:attributeinfo{u2attributenameindex;属性名u4attributelength;属性数据长度u1info〔attributelength〕;其他字段,各属性的结构不同} 属性有20种,此处只记录常见的三种Code属性:存储方法编译后的字节码指令Codeattribute{u2attributenameindex;属性名,指向的Utf8info值固定为Codeu4attributelength;剩下字节长度u2maxstack;操作数栈最大深度,对于此方法的栈帧中操作数栈的深度u2maxlocals;以slot变量槽为单位的局部变量表大小,存储隐藏参数this,实参列表,catch参数,局部变量等u4codelength;字节码指令总长度u1code〔codelength〕;JVM指令集大小200,单个指令的编号用u1描述u2exceptiontablelength;异常表,描述方法内各指令区间产生的异常及其handler地址{u2startpc;catchtype类型的异常,会在〔startpc,endpc)指令范围内抛出u2endpc;u2handlerpc;若抛出此异常,则goto到handlerpc处执行u2catchtype;}exceptiontable〔exceptiontablelength〕;u2attributescount;Code属性自己的属性attributeinfoattributes〔attributescount〕;}LineNumberTable属性:记录Java源码行号与字节码行号的对应关系,用于抛异常时显示堆栈对应的行号等信息。可作为Code属性的子属性LineNumberTableattribute{u2attributenameindex;u4attributelength;u2linenumbertablelength;{u2startpc;字节码指令区间开始位置u2linenumber;对应的源码行号}linenumbertable〔linenumbertablelength〕;}LocalVariableTable属性:记录Java方法中局部变量的变量名,与栈帧局部变量表中的变量的对应关系,用于保留各方法有意义的变量名称LocalVariableTableattribute{u2attributenameindex;u4attributelength;u2localvariabletablelength;{u2startpc;局部变量生命周期开始的字节码偏移量u2length;向后生命周期覆盖的字节码长度u2nameindex;变量名u2descriptorindex;类型描述符u2index;对应的局部变量表中的slot索引}localvariabletable〔localvariabletablelength〕;} 其他属性直接参考JVM文档示例源码:comclsStructure。javapackagecom。cls;publicclassStructure{publicstaticvoidmain(String〔〕args){System。out。println(helloworld);}} javacg:linescomclsStructure。java编译后,参考javap反编译得到的正确结果,odxendianbigStructure。class得出class文件内容的十六进制表示,解读如下:cafebabe1。u4魔数,标识class文件类型000000342。u2,u2版本号,52JDK83。常量池1001fu2constantpoolcount,31项(从1开始计数,0预留)0au1tag,10,Methoddefinfo,成员方法结构0006u2index,6,所属类的Classinfo在常量池中的编号javalangObject0011u2index,17,此方法NameAndType编号init:()V2099,Fileddefinfo,成员变量结构0012u2index,18,所属类Classinfo编号javalangSystem0013u2index,19,此字段NameAndType编号out:LjavaioPrintStream3088,Stringinfo,字符串0014u2index,20,字面量编号helloworld40a001521javaioPrintStream001622println:(LjavalangString;)V507Classinfo,全限定类名0017u2index,23,字面量编号comclsStructure6077,Classinfo,类引用001824javalangObject701Utf8info,UTF8编码的字符串0006u2length,6,字符串长度3c696e69743e字面量值init816010003282956()V010004436f6465Code01000f4c696e654e756d6265725461626c65LineNumberTable0100046d61696emain010016285b4c6a6176612f6c616e672f537472696e673b2956(〔LjavalangString;)V0100104d6574686f64506172616d6574657273MethodParameters01000461726773args01000a536f7572636546696c65SourceFile01000e5374727563747572652e6a617661Structure。java170c12,NameAndType,名字及类型描述符0007u2index,7,字段或方法名字面量编号init0008u2index,8,字段或方法结构编号()V1807001925javalangSystem190c001a001b26:27out:LjavaioPrintStream;2001000b68656c6c6f20776f726c64helloworld2107001c28javaioPrintStream220c001d001e29:30println:(LjavalangString;)V2331010011636f6d2f636c732f537472756374757265comclsStructure0100106a6176612f6c616e672f4f626a656374javalangObject0100106a6176612f6c616e672f53797374656djavalangSystem0100036f7574out0100154c6a6176612f696f2f5072696e7453747265616d3bLjavaioPrintStream;0100136a6176612f696f2f5072696e7453747265616djavaioPrintStream0100077072696e746c6eprintln010015284c6a6176612f6c616e672f537472696e673b2956(LjavalangString;)V00214。u2,accessflagsACCPUBLICACCSUPER00055。u2,thisclass,55。ClassinfocomclsStructure00066。u2,superclass,66。ClassinfojavalangObject00007。u2,interfacecount,000008。u2,fieldscount,000029。methodscount,2方法一0001u2,accessflags,ACCPUBLIC0007u2,nameindex,7init0008u2,descriptorindex,8()V0001u2,attributecount,10009u2,attributenameindex,9Code属性0000001du4,attributelength,300001u2,maxstack,10001u2,maxlocals,100000005u4,codearraylength,52au1,aload0将第0个slot中的变量this入栈b70001u1,invokespecial执行从Object继承的initb1u1,return返回void0000u2,exceptiontablelength,0exceptiontable为空,无异常0001u2,attributescount,1Code属性本身的子属性000a10LineNumberTable属性0000000660001u2,linenumbertablelength,10000u2,startpc,00003u2,linenumber,3方法二0009accessflagsACCPIBLICACCSTATIC000bnameindex,11main000cdescriptorindex,12(〔LjavalangString;)V0002attributecount,20009attributenameindex,9Code00000025attributelength,370002maxstack,20001maxlocals,100000009codearraylength,9b20002getstatic,2Field:javalangSystem。out:LjavaioPrintStream;加载静态对象变量1203ldc,3String:helloworld将常量参数入栈b60004invokevirtual,4Method:javaioPrintStream。println:(LjavalangString;)V执行方法b1return0000exceptiontablelength,00001attributescount,1000a10LineNumberTable0000000a100002linenumbertablelength,2000000050500080006866。2字节码指令 JVM面向操作数栈(operandstack)设计了指令集,每个指令由1字节的操作码(opcode)表示,其后跟随0个或多个操作数(operand),指令集列表参考Javabytecodeinstructionlistings大部分与数据类型相关的指令,其操作码符号都会带类型前缀,如i前缀表示操作int,剩余对应关系为b:byte,c:char,s:short,f:float,d:double,l:long,a:reference由于指令集大小有限(256个),故boolean,byte,char,short会被转为int运算 字节码可大致分为六类:加载和存储指令:将变量从局部变量表slot加载到操作数栈的栈顶,反向则是存储将slot0,1,2,3,N加载到栈顶,T表示类型简记前缀,可取i,l,f,d,aTload0,Tload1,Tload2,Tload3,Tloadn将栈顶数据写回指定的slotTstore0,Tstore1,Tstore2,Tstore3,Tstoren将不同范围的常量值加载到栈顶,由于05常量过于常用,有单独对应的指令,ldc则加载普通常量bipush,sipush,Tconst〔0,1,2,3,4,5〕,aconstnull,ldc运算指令Tadd,Tsub,Tmul,Tp,Trem算术运算:加减乘除,取余Tneg,Tor,Tand,Txor位运算:取反、或、与、异或dcmpg,dcmpl,fcmpg,fcmpl,lcmp比较运算:后缀g即greater,l即lessthaniinc局部自增运算,与iload搭配使用强制类型转换指令:窄化转换为T类型(长度为N)时,会直接丢弃除了低N位外的其他位,可能会导致数据溢出、正负号不确定,浮点数转整型则会丢失精度i2bintbytei2c,i2s;l2i,f2i,d2i;d2l,f2l;d2f对象创建与访问指令:类实例、数组都是对象,存储结构不同,创建和访问指令有所区别new创建类实例newarray,annewarray,multianewarry创建基本类型数组、引用类型数组、多维引用类型数组getfield,putfield;getstatic,putstatic读写类实例字段;读写类静态字段Taload,Tastore;arraylength读写数组元素;计算数组长度instanceof;checkcast校验对象是否为类实例;执行强制转换操作数栈管理指令pop,pop2弹出栈顶1,2元素dup,dup2;swap复制栈顶1,2个元素并重新入栈;交换栈顶两个元素控制转移指令:判断条件成立,则跳转到指定的指令行(修改PC指向)ificmpeq,icmpne;icmplt,icmple;icmpgt,icmpge;acmpe,acmpne整型比较,引用相等性判断ifeq,lt,le,gt,ge,null,nonnull搭配其他类型的比较运算指令使用方法调用与返回指令invokevirtual根据对象的实际类型进行分派,调用对应的方法(比如继承后方法重写)invokespecial调用特殊方法,如cint()V,init()V等初始化方法、私有方法、父类方法invokestatic调用类的静态方法invokeinterface调用接口方法(实现接口的类对象,但被声明为接口类型,调用方法)invokedynamicTODOTreturn,return返回指定类型,返回void异常处理指令:athrow抛出异常,异常处理则由exceptiontable描述同步指令:synchronized对象锁由monitorenter,monitorexit搭配对象的monitor锁共同实现ch07。类加载7。1类加载过程 1。加载 原理:委托ClassLoader读取Class二进制字节流,载入到方法区内存,并在堆内存中生成对应的java。lang。Class对象相互引用 2。验证 校验字节流确保符合Class文件格式,执行语义分析确保符合Java语法,校验字节码指令合法性3。准备 在堆中分配类变量(static)内存并初始化为零值,主义还没到执行putstatic指令赋值的初始化阶段,但静态常量属性除外:publicclassClassX{finalstaticintn2;常量的值在编译期就已知,准备阶段完成赋值,值存储在ConstantValuefinalstaticStringstrstr;字符串静态常量同理}staticfinaljava。lang。Stringstr;descriptor:LjavalangString;flags:ACCSTATIC,ACCFINALConstantValue:Stringstr4。解析 将常量池中的符号引用(Classinfo,Fieldrefinfo,Methodrefinfo)替换为直接引用(内存地址)5。初始化 javac会从上到下合并类中static变量赋值、static语句块,生成类构造器()V,在初始化阶段执行,此方法的执行由JVM保证线程安全;注意JVM规定有且仅有的,会立即触发对类初始化的六种casepublicclassClassX{static{println(mainclassClassXinit);1。main()所在的主类,总是先被初始化}publicstaticvoidmain(String〔〕args)throwsException{首次会触发类的初始化SubXbnewSubX();new对象2。new,getsatic,putstatic,invokestatic指令println(SuperX。a);读写类的static变量,或调用static方法println(SubX。c);3。子类初始化,会触发父类初始化println(SubX。a);子类访问父类的静态变量,只会触发父类初始化不会触发类的初始化println(SubX。b);1。访问类的静态常量(基本类型、字符串字面量)println(SubX。class);2。访问类对象println(newSubX〔2〕);3。创建类的数组}}classSuperX{staticinta0;static{println(classSuperXinitiated);}}classSubXextendsSuperX{finalstaticdoubleb0。1;staticbooleancfalse;static{println(classSubXinitiated);}}7。2类加载器 层级关系 双亲委派机制原理:一个类加载器收到加载某个类的请求时,会先委派上层的父类加载器去加载,逐层向上,当父类加载器逐层向下反馈都无法加载此类后,该类加载器才会尝试自己加载;此模型保证了,诸如rt。jar中的java。lang。Object类,不论在底层哪种类加载器中都一定是被Bootstrap类加载器加载,JVM中仅此一份,保证了一致性实现javalangClassLoaderprotectedClasslt;?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){1。先检查自己的加载器是否已加载此类Classlt;?cfindLoadedClass(name);if(cnull){longt0System。nanoTime();try{if(parent!null){2。还有上层则委派给上层去加载cparent。loadClass(name,false);}else{3。如果没有上级,则委派给Bootstrap加载cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){类不存在}if(cnull){4。到自己的classpath中查找类,用户自定义ClassLoader自定义了查找规则longt1System。nanoTime();cfindClass(name);}}if(resolve){resolveClass(c);}returnc;}}ch08。字节码执行引擎8。1运行时栈帧结构publicstaticvoidmain(String〔〕args){inta1008611;intba;} 对应运行时栈帧结构: 局部变量表:大小在编译期确定,用于存放实参和局部变量,以大小为32bit的变量槽为最小单位long,double类型被切分为2个slot同时读写(单线程操作,无线程安全问题)类对象调用方法时,slot0固定为当前对象的引用,即this隐式实参变量槽有重用优化,当pc指令超出某个槽中的变量的作用域时,该槽会被其他变量重用publicstaticvoidmain(String〔〕args){{byte〔〕bufnewbyte〔1010241024〕;}System。gc();buf还在局部变量表的slot0中,作为GCRoot无法被回收intv0;变量v重用slot0,gc生效System。gc();操作数栈:最大深度在编译期确定,与局部变量表配合入栈、执行指令、出栈来执行字节码指令返回地址:遇到return族指令则正常调用完成,发生异常但异常表中没有对应的handler则异常调用完成;正常退出到上层方法后,若有返回值则压入栈,并将程序计数器恢复到方法调用指令的下一条指令8。2方法调用 1。虚方法、非虚方法非虚方法:编译期可知(程序运行前就唯一确定)、且运行期不可变的方法,在类加载阶段就会将方法的符号引用解析为直接引用。有5种:静态方法(与类唯一关联):invokestatic调用私有方法(外部不可访问)、构造器方法、父类方法:invokespecial调用final方法(无法被继承):由invokevirtual调用(历史原因)publicclassStaticResolution{publicstaticvoiddoFunc(){System。out。println(dofunc。。。);}publicstaticvoidmain(String〔〕args){StaticResolution。doFunc();}}stack0,locals1,argssize1静态方法的调用版本,在编译时就以常量的形式,存入字节码的参数0:invokestatic5MethoddoFunc:()V3:return 虚方法:需在运行时动态确定直接引用的方法,由invokevirtual,invokeinterface调用 2。静态分派、动态分派背景:方法可被重载(参数类型不同,或数量不同)、可被重写(子类继承后覆盖) 分派:对象可声明为类、父类、实现的接口等类型,当对象作为实参或调用方法时,需根据其静态类型或实际类型,才能确定要调用的方法的版本,进而确定其直接引用。此过程即方法的分派 reference变量的2种类型静态类型:变量被声明的类型,不会改变,编译期可知实际类型:变量指向的对象可被替换,运行时随时可能修改静态分派原理:方法重载时,依赖参数的静态类型,来确定要使用哪个重载版本的方法特点:发生在编译阶段,由javac确定类型匹配度最高的重载版本,来作为invokevirtual的参数publicclassStaticDispatch{staticabstractclassHuman{}staticclassManextendsHuman{}staticclassWomanextendsHuman{}publicvoidf(Humanhuman){System。out。println(f(Human));}publicvoidf(Manman){System。out。println(f(Man));}publicvoidf(Womanwoman){System。out。println(f(Woman));}publicstaticvoidmain(String〔〕args){HumanmannewMan();静态类型都是HumanHumanwomannewWoman();实际类型分别为Man,WomanStaticDispatchsdnewStaticDispatch();sd。f(man);f(Human)invokevirtual13Methodf:(Lcomch08StaticDispatchHuman;)Vsd。f(woman);f(Human)编译期就已确定重载版本,写入字节码中}}动态分派原理:方法重写时,依赖Receiver对象的实际类型,来确定要使用哪个类版本的方法特点:发生在运行时,依赖invokevirtual指令来确定调用方法的版本,进而实现多态,解析流程为 注:类的方法查找是高频操作,JVM会在方法区中为类建一张虚方法表vtable,以实现方法的快速查找publicclassDynamicDispatch{staticabstractclassHuman{protectedabstractvoidf();}staticclassManextendsHuman{Overrideprotectedvoidf(){System。out。println(Manf());}}staticclassWomanextendsHuman{Overrideprotectedvoidf(){System。out。println(Womanf());}}publicstaticvoidmain(String〔〕args){HumanmannewMan();虽然静态类型都是HumanHumanwomannewWoman();man。f();Manf()invokevirtual6Methodcomch08DynamicDispatchHuman。f:()Vwoman。f();Womanf()虽然字节码指令的参数,都是静态类型方法的符号引用mannewWoman();man。f();Womanf()但invokevirtual会根据Receiver实际类型,在运行时解析到实际类的直接引用}} 注意,类的字段读写指令getfield,putfield没有invokevirtual的动态分派机制,即子类的同名字段会直接覆盖父类的字段。示例:publicclassFieldHasNoPolymorphic{staticclassFather{publicintmoney1;publicFather(){money2;showMoney();}publicvoidshowMoney(){System。out。println(Father,moneymoney);}}staticclassSonextendsFather{publicintmoney3;子类字段在类加载的准备阶段被赋零值publicSon(){子类构造器第一行默认隐藏调用super()money4;showMoney();}publicvoidshowMoney(){System。out。println(Son,moneymoney);}}publicstaticvoidmain(String〔〕args){FatherguynewSon();System。out。println(guy,moneyguy。money);}}Son,money0Father类构造器执行,动态分派执行了Son::showMoney()Son,money4Son类构造器中访问最新的、自己的money字段guy,money2字段的读写没有动态分派,静态类型是谁,就访问谁的字段 3。单分派、多分派方法的Receiver,与方法的参数,都是方法的宗量,根据一个宗量来选择目标方法称为单分派,需要多个宗量才能确定方法的叫多分派静态分派机制会让编译器在编译阶段,对重载的多个方法,会选出参数匹配度最高的作为目标方法动态分派机制在运行时,依赖Receiver实际类型,配合invokevirtual定位唯一的实例方法作为目标方法 综上两点,Java是静态多分派、动态单分派的语言 注明:第10,11章讲Java的前后端编译,学习了自动装箱等常见语法糖的字节码实现,其余部分待有空搭配龙书一起学;第12,13章内容与《JavaConcurrencyInPractice》等书重合度较高,此处不再赘述 记得点赞,关注转发!!!