jvm笔记

来源:互联网 发布:淘宝虚拟自动发货软件 编辑:程序博客网 时间:2024/03/29 23:03

第2章 Java内存区域与内存溢出异常

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

2.2运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区

域。这些区域有各自的用途,以及创建和销毁的时间。

程序计数器(Program Counter Register)当前线程所执行的字节码的行号指示器,“线程私有”的内存。如果正在执行的是Native方法,这个计数器值则为空。

Java虚拟机栈(Java Virtual Machine Stacks)线程私有。描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。如果线程请求的栈深度大于虚拟机所允许的深度,StackOverflowError;如果虚拟机栈可以动态扩展,OutOfMemoryError。

本地方法栈(Native Method Stack)为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,具体的虚拟机可以自由实现它。

Java堆(Java Heap)是内存中最大的一块,被所有线程共享,唯一目的是存放对象实例。

方法区(Method Area)线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。可以不实现垃圾收集器。

运行时常量池(Runtime Constant Pool)是方法区的一部分,Java虚拟机没有做任何细节的要求。

直接内存(Direct Memory)不是Java虚拟机规范中定义的内存区域,NIO类,可以使用Native函数直接分配堆外内存。配置虚拟机参数需要考虑直接内存。

 

2.3 HotSpot虚拟机对象探秘

遇到new指令时,首先检查这个参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有必须先加载。接下来为新生对象分配内存。指针碰撞(Bump the Pointer);空闲列表(Free List)。分配内存并发情况下非线程安全。对分配内存空间的动作同步处理;按线程划分在不同的空间中进行。初始化为零值。接下来,虚拟机要对对象进行必要的设置,存放在对象头(Object Header)中。对象在内存中存储的布局分为三块区域:对象头、实例数据(Instance Data)和对齐填充(Padding)。

Java程序需要通过栈上的reference数据来操作堆上的具体对象。句柄和直接指针两种访问方式。句柄的好处是对象被移动时只会改变句柄中的实例数据指针,直接指针的好处是速度快。

出现OOM,一般的手段是先通过内存映像分析工具(如EclipseMemory-Analyzer)对Dump出来的堆转储快找进行分析。如果内存泄漏,可进一步通过工具查看泄露对象到GC Root的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们。

由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,虽然-Xoss(设置本地方法栈大小)存在,但实际上是无效的,栈容量只有-Xss参数设定。实验表明:单个线程下抛出的都是StackOverflowError。如果建立过多线程导致内存溢出,只能通过减少最大堆和减少栈容量来换取更多的内存,若无经验这种通过“减少内存”的手段来解决内存溢出的方式比较难想到。

String.intern()是一个Nativ方法,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象。可以通过-XX:PerSize和-XX:MaxPermSize限制方法区域,从而间接限制其中常量池的容量。

方法去用于存放Class的相关信息,OOM:PerGen-space方法区溢出。

DirectMemory容量可通过-XX:MaxDirectMemorySize指定。如果OOM后Dump文件很小,而程序中又直接或间接使用了NIO,可以考虑这个原因。

 

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

当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,需要对GC的技术是是必要的监控和调节。

 

3.2 对象已死吗

引用计数法(Reference Counting)。当有一个地方引用它时,计数器加一,引用失效时,计数器减一。Java不采用,很难解决对象之间循环引用的问题。

可达性分析(Reachability Analysis),通过一系列称为“GCRoots”的对象作为起始点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象GC Roots没有任何引用链相连时,则证明此对象是不可用的。GC Roots的对象包括:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。

强引用:类似Object obj = new Object()

软引用:有用但并非必须的对象。在系统将要发生内存溢出异常之前,会把这些对象列入回收范围二次回收。

弱引用:非必需对象,只能生存到下一次垃圾收集发生之前。

虚引用:完全不对对象生存时间构成印象,也无法通过虚引用来取得一个对象实例。设置虚引用唯一目的是能在这个对象被回收器回收时收到一个系统通知。

真正宣告一个对象死亡,至少经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,条件是此对象是否有必要执行finalize()方法。当没有覆盖finalize()方法,或finalize()方法已经被调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要finalize(),这个对象将会放置在一个F-Queue的队列中,并在稍后一个由虚拟机自动建立的、低优先级的Finalizer线程去执行。“执行”指虚拟机会出发这个方法,但不承诺会等待它运行结束。原因是:某个对象finalize()执行缓慢或死循环导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运最后一次机会,只要重新与引用链上的任何一个关系建立关联。

笔者建议避免finalize(),是Java为了C程序员作出的妥协。运行代价高昂,不确定性大。try-finally可以做得更好。

在方法去进行垃圾收集“性价比”较低。永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。判断“无用的类”的3个条件:该类所有实例都已经被回收;加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有在任何地方被引用。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁定义的ClassLoader的场景都需要虚拟机具备卸载的功能。

 

3.3垃圾收集算法

标记-清除(Mark-Sweep)算法,不足:效率;空间,标记清除后会产生大量不连续的内存碎片。

复制(Copying),可用内存按容量分为大小相等的两块,每次只用一块。当这一块用完了,就将还存活者的对象复制到另外一块上面。新生代中的对象98%朝生夕死,将内存分为一块较大的Eden空间和两块较小的Survivor空间。当回收时,Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上。默认8:1

标记-整理算法,让所有存活的对象都向一端移动。

分代收集算法,根据对象存活周期的不同将内存划分为几块。

 

3.4 HotSpot的算法实现

枚举根节点。可作为GC Roots的节点主要在全局性的引用与执行上下文中,消耗很多资源;GC进行时必须停顿所有Java执行线程。准确式GC,虚拟机有办法直接得知那些地方存放着对象引用。在HotSpot的实现中,使用OopMap的数据结构达到这个目的。在类加载完成后,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来。

可能导致引用关系变化,或OopMap内容变化的指令非常多,如果为每一条指令都声称对应的OopMap需要大量的额外空间。HotSpot只是在安全点(Safepoint)记录信息。Safepoint的选定不能太少以至于让GC等待时间太长,也不能过于频繁以至于增大负荷。以“是否具有让程序长时间执行的特征”为标准选定安全点,最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等。

如何在GC发生时让所有线程都跑到最近的安全点上再停顿下来。抢先式中断(Preemptive Suspension)GC时如果有线程中断地方不在安全点上,就恢复线程让它跑到安全点上(几乎不用)。主动式中断(Voluntary Suspension)当GC需要中断线程时,简单地设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。

安全区域(Safe Region)解决Sleep状态线程无法走到安全地方终端挂起。安全区域是指一段代码片段之中引用关系不会变化,任意地方GC安全。线程执行到Safe Region中的代码时首先标志,这样当GC时就不用管该进程。离开Safe Region时要检察系统是否已经完成了根节点枚举(或GC),如果完成了线程继续执行,否则必须等待直到可以安全离开Safe Region的信号。

 

3.5垃圾收集器

Serial收集器。一条收集线程,必须暂停其他线程。无线程交互开销,高效,最多一百毫秒可以接受。

ParNew收集器。Serial的多线程版本。只有它能与CMS收集器配合工作。

ParallelScavenge收集器。目标是达到一个可控制的吞吐量(Throughput)=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。两个参数:控制最大垃圾收集停顿时间、直接设置吞吐量大小。可以开启GC自适应的调节策略(GC Ergonomics),自动设置新生代大小、Eden与Survivor区的比例等(与ParNew的重要区别)。

Serial Old收集器。主要意义在于给Client模式下的虚拟机使用。

Parallel Old收集器。注重吞吐量以及CPU资源敏感的场合:ParallelScavenge加Parallel Old

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。初始标记,并发标记,重新标记,并发清除。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以CMS收集器的内存回收过程是与用户线程一起并发执行的。但远达不到完美的程度。

3个缺点:对CPU资源非常敏感;无法处理浮动垃圾(FloatingGarbage),需要预留一部分空间提供并发收集时的程序运作使用。如果预留内存无法满足程序需要,会出现“Concurrent Mode Failure”失败,这时将启动后备预案:Serial Old;空间碎片,提前触发FullGC。CMS提供一个开关参数用于要进行Full GC时开启内存碎片的合并整理过程。

G1(Garbage-First)收集器特点:并行与并发、分代收集、空间整合、可预测的停顿。G1将整个Java堆划分为多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。G1跟踪每个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间优先回收价值最大的Region。

使用Remembered Set避免全堆扫描。每个Region对有一个对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中。如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当内存回收时,在GC根节点的枚举范围中加入RememberedSet即可保证不对全堆扫描。

 

3.6内存分配与回收策略

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

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。新生代Minor GC非常频繁,速度快。老年代Major GC/Full GC经常伴随至少一次Minor GC,速度一般比Minor GC慢10倍以上。

需要大量连续内存空间的Java对象直接进入老年代。

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区每“熬过”一次Minor GC,Age++。当年龄增加到一定程度(默认15)就会晋升到老年代中。如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立那么Minor GC是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,改为进行一次FullGC。

 

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

4.1概述

工具永远是知识技能的一层包装,没有什么工具是“秘密武器”,不可能学会了就能包治百病。

 

4.2 JDK的命令行工具

命令行工具大多数是jdk/lib/tools.jar类库的一层薄包装。选择采用Java代码实现这些监控工具有特别用意:当应用程序部署到生产环境后,谁论是直接接触物理服务器还是远程Telnet到服务器上都可能会受到限制。借助tools.jar类库里面的接口,可以直接在应用程序中实现功能强大的监控分析功能。(tools.jar中的类库不属于Java的标准API,如果引入这个类库意味着用户程序只能运行于SunHotspot上面,或者在部署程序时需要一起部署tools.jar)。

主要命令行监控工具:

jps   JVM Process Status Tool,显示指定系统内所有的Hotspot虚拟机进程。

jstat  JVM Statistics Monitoring Tool,用于收集Hotspot虚拟机各方面的运行数据。

jinfo  Configuration Info for Java,显示虚拟机配置信息。

jmap  Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)。

jhat   JVM Heap Dump Browser,用于分析heapdump文件。

jstack  Stack Trace for Java,显示虚拟机的线程快照。

在Java虚拟机规范中,详细描述了虚拟机指令集中每条指令的执行过程、执行前后对操作数栈、局部变量表的影响等细节。但随着技术的发展,高性能虚拟机真正的细节实现方式已经渐渐与虚拟机规范所描述的内容产生了越来越大的差距,虚拟机规范中的描述逐渐成了虚拟机实现的“概念模型”。

HSDIS是一个Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件,它包含在HotSpot虚拟机的源码之中,但没有提供编译后的程序。

 

4.3 JDK的可视化工具

JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具。内存监控,线程监控。

VisualVM(All-in-One Java Troubleshooting Tool)是目前位置随JDK发布的功能最强大的运行监事和故障处理程序。而且不需要被监视的程序基于特殊Agent运行,因此它对应用程序的实际性能的影响很小。生成、浏览堆转储快照,分析程序性能,BTrace动态日志跟踪。

 

第5章 调优案例分析与实战

 

第6章 类文件结构

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

6.1概述

越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

 

6.2无关性基石

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式----字节码(ByteCode)是构成平台无关性的基石。语言无关性正越来越被开发者所重视。有一些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,这也为其他语言实现一些有别于Java的语言特性提供了基础。

 

6.3 Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,只有两种数据类型:无符号数和表。整个Class文件本质上就是一张表。

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。第5、6字节是子版本号,7、8字节是主版本号。

紧接着主版本号之后的是常量池入口。与其他项目关联最多,占空间最大,第一个出现的项目。入口需要放置一项u2类型的常量池容量计数器(constant_pool_count,计数从1开始,0表示“不引用任何一个常量池项目”的含义)。常量池中主要存放两大类常量:字面量(Literal)和符号引用(SymbolicReferences)。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。javap用于分析Class文件字节码的工具。

常量池结束后,紧接着的两个字节代表访问标志(access_flag),用于识别一些类或者接口层次的访问信息。

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后。

字段表用于描述接口或者类中声明的变量。不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,如内部类自动添加指向外部类实例的字段。另外,Java语言中字段无法重载,但对于字节码,如果两个字段的描述符不一致,那字段崇明就是合法的。

对于方法的描述与对字段的描述几乎完全一致。如果父类方法在子类中没有被重写,方法表集合中不会出现来自父类的方法信息。但有可能出现编译器自动添加的方法。若两个方法仅有返回值不同也可以合法共存与同一个Class文件中。

属性表不再要求具有严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

1.      Code属性存储方法体中的代码。虽然code_length是一个u4类型的长度值,但虚拟机规范中明确限制了一个方法不允许超过65535条字节指令,即只用了u2长度。javap中输出的Args_size的值至少为1,因为至少存在一个指向当前对象实例的局部变量this(如果方法为static则为可能0)。异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。

2.      Exception属性列举出方法中可能抛出的受查异常。

3.      LineNumberTable属性描述Java源码行号与字节码行号之间的对应关系。不是运行必须属性,但会默认生成到Class文件之中,便于调试。

4.      LocalVariableTable属性描述栈帧中局部变量与Java源码中定义的变量之间的关系,便于调试。

5.      SourceFile属性记录生成这个Class文件的源码文件名称。

6.      ConstantValue属性通知虚拟机自动为静态变量赋值。对于非static类型的变量的赋值是在实例构造器<init>方法中进行的;而对于类变量有两种方式:在累构造器<clinit>方法中或者使用ConstantValue属性。目前Sun Java编译器的选择时如果这个变量没有被final修饰或并非基本类型及字符串,则在<clinit>方法中进行初始化。

7.      InnerClass属性用于记录内部类与宿主类之间的关联。

8.      Deprecated及Synthetic属性都属于标志类型的布尔属性。

9.      StackMapTable是一个复杂的变长属性,会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

10.  Signature可选定长属性记录泛型签名信息。

11.  BootStrapMethod复杂的变长属性用于保存invokedynamic指令引用的引导方法限定符。

 

6.4字节码指令简介

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字以及跟随其后的零至多个代表此操作所需参数而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数。

字节码指令集是一种具有鲜明特点、优劣势都很突出的的指令架构。缺点:总数不超过256,;放弃了编译后代码的操作数长度对其,意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时重建出具体数据的结构,但也可以省略很多填充和间隔符号。

大多数指令都包含了操作数所对应的数据类型信息。Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。大部分的指令都没有支持类型byte、short、char和boolean,编译器会在编译期或运行期将byte和short类型的数据带符号扩展为相应的int类型数据,处理时也会转换为使用对应的int类型的字节码指令来处理。

9类指令:加载和存储指令,运算指令,类型转换指令,对象创建与访问指令,操作数栈管理逻辑,控制转移指令,方法调用和返回指令,异常处理指令,同步指令。

 

6.5公有设计和私有实现

一个优秀的虚拟机实现,在满足虚拟机规范的约束下对具体实现做出修改和优化是完全可行的。

 

第7章 虚拟机类加载机制

7.1概述

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

 

7.2类加载的时机

解析阶段可以在初始化后再开始,这是为了支持Java语言的运行时绑定。

Java虚拟机规范没有强制何时加载。严格规定了有且仅有5种情况必须立即对类进行“初始化”。

 

7.3类加载的过程

1.加载阶段完成3件事:获取类的二进制字节流;将静态存储结构转化为方法区的运行时数据结构;生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2.验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求。4个阶段:文件格式验证,元数据验证,字节码验证(停机问题;StackMapTable被篡改的可能),符号引用验证。

3.准备阶段是正是为类变量分配内存并设置类变量初始值(“0”)的阶段。

4.解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。虚拟机实现可以根据需要来判断是在类被加载器加载时就对常量池中的符号引用进行解析,还是等一个符号引用将要被使用前才去解析他。除invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存。

5.初始化阶段才真正开始执行类中定义的Java程序代码。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句和并产生的,编译器收集顺序由源文件顺序决定。静态语句块只能访问到定义在静态语句块之前的变量,之后的变量只能赋值不能访问。不需要显示调用父类构造器。

 

7.4类加载器

“通过一个类的全限定名来获取描述此类的二进制字节流”在Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。即使两个类来源于同一个Class文件,只要加载他们的类加载器不同,这两个类就不相等。

从Java虚拟机的角度,只存在两种不同的类加载器:一种是启动类加载器,用C++实现,是虚拟机的一部分;另一种是所有其他的类加载器,这些类加载器都用Java语言实现,并且全都继承自抽象类java.lang.ClassLoader。

从开发人员角度,会使用到以下3种:启动类加载器,扩展类加载器,应用程序类加载器。

类加载器的双亲委派模型:如果一个类加载器收到了类加载的请求,它首先会把这个请求委派给父类加载器,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

破坏双亲委派模型。

 

第8章 虚拟机字节码执行引擎

8.1概述

     

8.2运行时栈帧结构

栈帧(Stack Frame)适用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。在活动线程中,只有位于栈顶的帧栈才是有效的,称为当前帧栈(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。方法的Code属性的max_locals。容量以变量槽为最小单位(Variable Slot)。

局部变量中的Slot是可以重用的。这样的设计除了节省战阵空间以外,还伴随一些额外的副作用,例如某些情况下,Slot的复用会直接影响到系统的垃圾收集行为。代码示例说明了赋null值的操作在某些情况下确实是有用的,但不应当对其有过多的依赖(JIT)。局部变量不存在“准备阶段”。

操作数栈(Operand Stack)。

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用时为了支持方法调用过程中的动态连接(Dynamic Linking)。

两种方式退出方法:正常完成出口(Normal Method Invocation Completion),异常完成出口(Abrupt Method Invocation Completion)。

 

8.3方法调用

     方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本。所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期不可改变。符合“编译器可知,运行期不可变”这个要求的方法主要包括静态方法和私有方法,它们都适合在类加载阶段进行解析。

与之对应,Java虚拟机提供了5条方法调用字节码指令。只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,它们在类加载的时候就会把符号引用解析为该方法的直接调用,称为非虚方法(还包括final修饰的方法)。

解析调用一定是个静态的过程,再编译期间就完全确定,在类装载的解析阶段就会把设计的符号引用全部转变为可确定的直接引用。

静态类型,实际类型。虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。静态类型是编译期可知的。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分配的典型应用是方法重载。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是唯一的。往往只能确定一个更加适合的版本。invokevirtual指令。

运行期根据实际类型确定方法执行版本的分派过程称为动态分配。invokevirtual指令的多态查找过程:找到操作数栈顶第一个元素所指向的对象的实际类型,按继承关系从下往上搜索与常量中的描述符和简单名称都相符的方法。这个过程就是Java语言中重写的本质。

方法的接收者与方法的参数称为方法的宗量。单分派是根据一个宗量对目标方法进行选择。Java语言的静态分配属于多分派类型,动态分配属于单分派类型。

动态分配是非常频繁的动作,需要为类在方法区建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的。

动态语言的关键特征是它的类型检查的注意过程是运行期而不是编译期。

方法getPrintlnMH()中模拟了invokevirtual指令的执行过程。MethodHandle的使用方法和效果与Reflection有众多相似之处,不过还是有以下区别:1.Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。2.Reflection中的java.lang.reflect.Method对象包含的信息更多,是Java一端的全面映像,重量级。3.由于MethodHandle是对字节码的方法指令调用的模拟,所以理论上虚拟机在这方面做的各种优化,在MethodHandle上也应该可以采用类似的思路去支持。某种程度上,invokedynamic指令与MethodHandle机制的作用是一样的,一个采用上层Java代码和API实现,另一个用字节码和Class中其他属性、常量来完成。

 

8.4基于栈的字节码解释执行引擎

只有确定了谈论对象是某种具体的Java实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。

Java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture, ISA),指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。与之相对的另一套常用的指令集架构是基于寄存器的指令集。基于栈的指令集主要优点是可移植,寄存器由硬件直接提供,程序依赖这些硬件寄存器则不可避免地要受到硬件的约束。栈指令的主要缺点是执行速度相对来说慢一些。

 

第9章 类加载及执行子系统的案例与实战

9.1概述

在Class文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多。能通过程序操作的,主要是字节码生成与类加载器这两部分的功能。

 

9.2案例分析

9.2.1 Tomcat:正统的类加载器架构

一个功能健全的Web服务器,要解决如下几个问题:保证两个应用程序的类库可以互相独立使用。部署在同一服务器上的两个Web应用程序所使用的Java类库可以互相共享。保证自身安全不受部署的Wed应用程序影响。支持HotSwap功能。

各种Web服务器提供了好几个ClassPath路径供用户存放的第三方类库,被放置到不同路径中的类库,具备不同的访问范围和服务对象。为了支持这套目录结构,并对目录里的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现。spring如何访问并不在其加载范围内的用户程序?线程上下文类加载器。

9.2.2 OSGi:灵活的类加载器架构

OSGi(Open Service Gateway Initiative)是OSGi联盟制定的一个基于Java语言的动态模块化规范。目的是使服务提供商通过住宅网关为各种家用智能设备提供各种服务,已经成为Java世界“事实上”的模块化标准。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖,并且类库的可见性能得到非常精确的控制,一个模块里只有被Export过的Package才可能由外界访问。OSGi 的Bundle类加载器之间只有规则,没有固定的委派关系。

9.2.3字节码生成技术与动态代理的实现

动态代理中的“动态”是针对使用Java代码实际编写了代理类的“静态”代理而言的,它的优势不在于省去了编写代理类的工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以灵活地重用于不同的应用场景之中。

9.2.4Retrotranslator:跨越JDK版本

逆向移植工具:Retrotranslator的作用是将JDK1.5编译出来的Class文件转变为可以在JDK1.4或1.3上部署的版本。

 

9.3实战:自己手动实现远程执行功能

0 0