JVM基础整理(大多数基于JDK7)

Posted by keys961 on January 30, 2018

JVM整理

Chapter 2

  1. JVM运行时数据区域:PC,JVM栈,Native栈,堆,方法区

    • PC Register:当执行本地方法时该值为空,该区域不会报OOM错误(每个线程都有一个PC)

    • JVM栈:线程私有,生命周期与线程相同。double,long占2个局部变量空间(32bits)其余占1个(使用-Xss设置每个线程栈大小)。若栈爆了,则报StackOverflowError;若扩展栈申请不到内存会报OutOfMemoryError

    • Native栈:用于Native方法服务,也会有StackOverflowErrorOutOfMemoryError

    • 堆:存放对象实例。分为新生代(Eden, From Survivor, To Survivor)和老年代(Old/Tenured Gen)。堆内存逻辑连续,可以配置成固定大小,也可扩展(使用-Xmx设置最大值,-Xms设置初始值,-XMn设置年轻代大小,-XX:进行其他配置).堆没法创建实例或无法扩展时报OutOfMemoryError

    • 方法区:线程共享.存储类信息(对象类型数据),常量,静态变量,JIT编译后的代码等数据.逻辑上属于堆的一部分但是还是被区分开来,称为”非堆”(1.7中还有永生代概念但在1.8中被移除,取而代之的是Metaspace,元空间不在JVM中而是直接使用本地内存).也会报OutOfMemoryError

    • 运行时常量池:1.6及以前作为方法区的一个部分(永生代),1.7以后被移出方法区并移入堆中(但是静态变量还是在方法区),1.8后永久代被移除.也会报OutOfMemoryError.

    • 直接内存:不是JVM一部分,但被频繁使用,会报OOM错误.如NIO类的使用(基于通道和缓冲区).有如新加入的Metaspace.它也会导致OOM.

  2. Hotspot

    • 对象创建:
      1. 首先进行类加载检查

      2. 然后分配内存(可用空闲列表,指针碰撞等方式,当多线程环境下可用CAS或者本地线程分配缓冲进行.对于TLAB,哪个线程要分配内存就在哪个TLAB上分配,用完后才需要同步锁定),

      3. 接着初始化空间,然后进行对象头设置(元数据设置)

      4. 最后执行<init>方法按照程序员意愿初始化

    • 对象内存布局:包含对象头,实例数据,对齐填充。对象头:一部分村运行时自身数据(hashcode,GC分代年龄,锁状态,持有锁,偏向线程ID,偏向时间戳等等,称为Mark Word),另一部分为类型指针(指向它类元数据的指针),若为数组还要有长度数据;实例数据通常相同宽度分配到一起;对齐填充并不必要,只是对象大小必须是8字节的整数倍。

    • 对象访问定位:主流实现有句柄和直接指针.前者会在堆中划分出句柄池,reference指向句柄池,再由句柄池中的指针指向实例/类型数据;后者reference直接指向堆中的对象数据,里面包含了对象类型数据的指针(指向方法区).Hotspot使用了直接指针实现.

Chapter 3

  1. 通用GC算法:
  • 引用计数:给对象引用计数,若为0则对象不可再被使用.效率高,实现简单但是难解决循环引用问题

  • 可达性分析算法:以GC Root为起点遍历引用(图遍历),若对象无法被引用到,则说明对象不可达(即不可用).GC Root可以是:JVM栈中引用的对象,方法区中类静态引用对象,方法区中常量引用对象,本地栈中Native方法引用的对象.

当可达性算法找到不可达对象时,不会立即回收,至少标记过2次后才会回收.首先第一次标记判断是否需要执行finalize(),若需要则将对象置入F-Queue队列中并等待出发这个方法.然后进行第二次标记,若对象在finalize()中重新声明了引用,则不被回收,否则就会被GC回收(应避免使用这个方法释放资源)

  1. 不同的引用
  • StrongReference:普通存在的引用,若其存在则永远不被GC

  • SoftReference:有用但非必需对象.在OOM前这些对象会被列入回收范围进行第二次回收,若还是不够才会抛出OOM

  • WeakReference:非必需对象,比软引用还要弱,其只能生存到下一次GC发生之前

  • PhantomReference:最弱的引用关系,不会对其生存时间造成影响

  1. 方法区的回收

(1.7及以前)的永久代回收分为2部分:废弃常量和无用的类.前者当系统中没有该常量的引用时可能被清除;后者需要满足:该类实例全被回收,加载该类的ClassLoader被回收,该类对应的Class对象没有被其他地方引用,可能会被回收.

  1. JVM中的GC算法

JVM中主要使用分代收集,即把内存按存活周期分块,每块用不同算法.

  • Mark-Sweep.先标记要清除的对象,然后清除(2个问题:效率不高,会有大量内存碎片)

  • Copying(用于新生代GC).将内存分为一定比例2块,其中一块用完则将存活对象复制到另一块上,然后清除原来整个半片区域.解决碎片问题,但是可用空间减少了一半

  • Mark-Compact(用于老年代GC).先标记,然后将存活对象向一端移动,并清理可回收对象,在4.1.算法基础上解决了外部碎片问题

  1. HotSpot实现
  • 枚举根节点.通常需要停顿.主流JVM使用准确式GC,从外部记录下对象的引用(类加载完成后),存成映射表(OopMap),而并不需要不遗漏地检查所有引用

  • 安全点.只在特定位置(即安全点)暂停下来GC,这些点记录了OopMap.所有线程都要到安全点停下来,可以使用抢先式中断和主动式中断,前者直接中断所有线程,若线程没到安全点就恢复让其跑到安全点;后者只对线程作一个标志(与安全点重合)让线程轮询检查这个标志,检查到就中断.

  • 安全区域.即一块代码区域里引用关系不发生变化,开启GC都是安全的,可用于无法响应中断请求的线程GC.进入该区域则先标记,要GC时就直接处理即可;离开时检查是否完成根节点枚举/GC过程,若没完成则要等待其完成后才能离开.

  1. 垃圾收集器

年轻代:Serial, ParNew, Parallel Seavenge;老年代: CMS, Serial Old(MSC), Parallel Old; 共有/试验:G1.

Minor/Major/Full GC: 清理年轻代/老年代/全堆

  • Serial:单线程GC,用于年轻代,Client模式下默认的年轻代GC.GC时其他线程必须暂停.使用Copying算法.

  • ParNew:多线程GC,用于年轻代,Server模式下默认年轻代GC.GC时其他线程必须暂停.使用Copying算法(若CPU核心不多,性能可能比Serial还要差).只有ParNew和Serial能和CMS协同工作.

  • Parallel Seavenge:多线程GC,用于年轻代.其关注可控的吞吐量(即可控制GC停顿时间),并支持动态调节(-XX:+UseAdaptiveSizePolicy).GC时其他线程必须暂停,算法依旧是Copying.

  • Serial Old:单线程GC,用于老年代,Client模式下默认的老年代GC.GC时其他线程必须暂停.使用Mark-Compact算法.

  • Parallel Old:多线程GC,用于老年代.其关注可控的吞吐量,和Parrallel Seavenge组合.GC时其他线程必须暂停,使用Mark-Compact算法.

  • CMS(Concurrent Mark Sweep):并发GC,即GC时不一定需要停止其他线程工作,用于老年代.使用Mark-Sweep算法(可设置参数来设置FullGC时整理碎片,碎片整理时其他线程暂停).

    其分为4个步骤:

    • 初始标记(标记GC Root直接关联的对象,STW)
    • 并发标记(从Root遍历标记引用)

    前两步都会扫描新生代,因为新生代对象可能引用老年代对象

    新生代扫描可以通过预先的Minor GC加速

    • 并发预清理

    Card Table:将老年代分成512字节的块。

    若某个元素引用变化,则该对象所在的块被标记为Dirty Card,该阶段会重新扫描这个块,将该块上的对象标记为可达的(同时Dirty标志被移除)

    • 重新标记(修正程序运作时修改的标记,STW)
    • 并发清除
    • 重置(清除内部状态,为下次回收准备)

    缺点:

    • 对CPU敏感(吞吐量降低,尤其是CPU数量少的时候,默认线程数=(CPU数量+3)/4)

    停顿时间下降,牺牲CPU吞吐量

    • 无法收集浮动垃圾(即新产生的垃圾只能下次GC处理),
    • 碎片问题(Mark-Sweep算法,可在Full GC时整理碎片但是消耗时间会更加长)
  • G1.用于替换CMS,JDK9默认GC.

    其特点:

    • 并行且并发,分代收集
    • 不产生内存碎片,
    • 可预测停顿.
    • 其不区分年轻代和老年代,而划分若干区域,并维护垃圾堆积大小列表,优先收集垃圾多的区域.每个区域都有维护一个Remembered Set来避免全堆扫描.

    其分为4个步骤:

    • 初始标记(标记GC Root直接关联的对象,STW)
    • 并发标记(遍历引用),
    • 最终标记(修正程序运作时修改的标记,修正Remembered Set,STW)
    • 筛选清除
  1. 对象分配和回收策略
  • 优先在新生代Eden分配,若空间不足则发起Minor GC

  • 大对象直接进入老年代(大于PretenureSizeThreshold)

  • 长期存活对象进入老年代(Minor GC会增加Age计数,即+1.对于Eden的对象,第一次Minor GC熬过后会移到Survivor区,而熬过一定时间后会移入老年代中,默认15岁)

  • 若某个年龄的所有对象大小占用Survivor空间的一半,那么年龄大于等于该年龄的对象可直接进入老年代

  • Minor GC前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总大小,若条件成立则Minor GC安全.否则若允许且老年代最大可用连续空间大于历史晋升到老年代对象平均大小,则再次尝试Minor GC(有风险),否则就行Full GC.

这是为了当Minor GC之后,当Survivor区域容量不足时,老年代有足够的空间容下这些多余的对象(即老年代进行对象的担保)

Chapter 6

  1. Class类文件结构(8位字节存储, Big-Endian)

    • Magic Number & Version:魔数为每个Class文件头4个字节(0xCAFEBABE),确定这个文件是否为JVM可以接受的Class文件.后面4个字节存储版本号(次2个+主2个)

    • 常量池:配有u2类型的计数值,从1开始计数.存2大类常量:字面量(文本字符串,final常量值),符号引用(类/接口全限定名,字段名称和描述符,方法名称和描述符),因此Class文件中不会保存各个方法和字段的最终内存布局信息.

      常量是一个表(1字节标志+数据/引用)

    • 访问标志:占2个字节,识别类和接口层次的访问信息(是类还是接口,是否为public,是否为abstract,是否为final类等)

    • 类,父类索引和接口索引集合:这3者决定这个类的继承关系(集合入口会有计数器,后面接接口索引表)

    • 字段表集合:包含类级变量和实例变量(不含局部变量),信息由访问修饰符,是否静态,是否可变,是否并发可见(volatile),是否可序列化,字段类型,字段名称.另外还有对常量池的引用(字段简单名称,字段和方法的描述符)以及属性表的相关数据信息

    • 方法表集合:和字段表集合几乎相同.对于重写,方法表集合种不会出现来自父类的方法;对于重载,简单名称要相同,还需要有与原方法不同的特征签名,本质上是参数在常量池种的字段符号的引用集合.(Java代码层面:特征签名包含方法名,参数顺序和参数类型;字节码层面:还包含返回值以及受查异常表)

    • 属性表集合:字段表,方法表都可以携带自己的属性表集合以用于描述某些场景专有的信息。

      具体属性有如Code(字节码指令),Exception(列举方法中受查异常),LineNumberTable(描述源码与字节码啊行号的对应关系),LocalVariableTable(栈帧局部变量与源码队医变量的关系),SourceFile(记录Class文件的源码文件名称),ConstantValue(为静态变量赋值),InnerClasses(内部类与外部类的关联)等等

  2. 虚拟机实现方式

    • 输入的Java虚拟机代码翻译成另外一种虚拟机指令

    • 输入的Java虚拟机代码翻译成宿主CPU本地指令集(如JIT技术)

Chapter 7

  1. 类生命周期: {加载->连接(验证->准备->解析)->初始化}->[使用]->卸载,{}为类加载的全过程

  2. 类加载时期:只有5种情况才会立即对类初始化(主动引用时)

    • 实例化一个对象/读取或设置一个类的非final静态字段/调用类的静态方法(new,getstatic, putstatic, invokestatic指令)

    • 反射对类进行调用(若此前没有初始化)

    • 子类初始化时若父类未初始化,先初始化父类(而对于接口,若子接口初始化时父接口没被初始化,只有在真正使用父接口的时候父接口才会先被初始化)

    • JVM启动时用户指定一个含main()方法的执行主类,JVM先初始化这个主类

    • 若使用MethodHandle解析出REF_get/put/invokeStatic句柄时,对于类必须被初始化(若此前没有初始化)

    被动引用:

    • 通过子类引用父类静态字段(父类必被初始化,但是子类不被初始化,而其加载和验证并美欧规定,因为只有直接定义这个字段的类才会被初始化);

    • 数组定义引用一个类(该类没被初始化,因为对应值为null);

    • 调用类的常量池存储的变量(如static final,该常量被编译器存入自己类的常量池中,本质上没有引用到对应的类)

  3. 类加载过程

    • 加载:

      步骤:获取定义该类的二进制字节流->静态存储结构转化为方法区的运行时数据结构(存入方法区)->生成代表该类的Class对象(Hotspot将其存在方法区).其可以和连接阶段交叉进行.

      数组类加载:本身不由加载器创建而由虚拟机直接创建.若元素类型是引用类型,使用加载器加载组件;若为基本类型,则虚拟机把数组标记为与引导类加载器相关联;可见性数组本身和元素是一致的.

    • 验证:验证Class文件是否符合虚拟机要求且安全.包括文件格式验证、元数据验证、字节码验证、符号引用验证.

    • 准备:为类变量正式分配内存并设置类变量初始值(仅仅设置static变量,它们都在方法区分配,并先只赋予0值,而最后的设置的值在类构造器<clinit>()方法中执行).而对于类字段中存在ConstantValue属性的来说,准备阶段中的该变量会被直接赋为对应的值.

    • 解析:将常量池中的符号引用(用符号描述引用目标,和虚拟机内存布局无关)替换成直接引用(直接指向目标的指针/偏移量/句柄,和虚拟机内存布局相关),包括类/接口解析,字段解析(查找&权限检查),类方法解析,接口方法解析.静态解析的结果(即直接引用)会被缓存进运行时常量池.

    • 初始化:执行类构造器<clinit>()方法.(若类中没有静态语句块和静态赋值语句,该方法可能不会被生成)

      • 该方法是编译器收集类变量(static)赋值动作和静态语句块合并产生的,收集顺序和源代码顺序相关(规则:只能访问定义在前面的变量,否则出现“非法向前引用”,而赋值可以赋出现在后面的变量).

      • 第一个被执行的类构造器是Object的.父类的类构造器优先执行.而接口(不允许含有静态语句块)的类构造器只有当里面的变量被使用时才会执行(即不一定执行接口的<clinit>()).

      • 虚拟机保证执行一个类的<clinit>()是线程安全的

  4. 类加载器:“获取定义该类的二进制字节流”在虚拟机外部进行,实现这个动作需要类加载器.而虚拟机中的类唯一性这个类本身和它的类加载器共同决定(即一个Class文件若由不同类加载器加载,它们在虚拟机中代表不同的类,即出现不同的Class类在虚拟机中)

    类加载器种类:Bootstrap类加载器(加载lib/下的类库,委托其加载直接设置父类加载器为null即可),扩展类加载器(加载lib/ext/下或java.ext.dirs定义的的类库,可直接使用),应用程序(系统)类加载器(加载用户Classpath上的类库,可直接使用且默认)

    自定义类加载器建议重写findClass()而不是loadClass()方法.

    双亲委派模型: ​

    • 加载器之间有层次关系,除了顶层的Bootstrap类加载器外,其余的加载器都应有父亲类加载器(但通常以组合关系实现).层次: Bootstrap <- Extension <- Application <- User-defined

    • 工作流程:优先给父类加载器,若父类加载器无法处理这个请求则自己尝试加载

    打破双亲委派模型例子:

    • JNDI,JDBC:使用线程上下文类加载器(可在当前线程中设置)请求子类加载器加载对应的类

    • OSGi:为了追求动态性(热替换和热部署).每个模块(Bundle)都有自己的类加载器,若要替换模块时连同类加载器一起替换以实现热替换.

Chapter 8

  1. JVM栈帧包含:局部变量表,操作数栈,动态链接,返回地址,附加信息.编译代码后,一个栈帧的大小时确定的.

  2. 栈帧结构

    • 局部变量表:以slot为最小单位(通常32位,可复用,因此需要GC时对于不需要的变量应该主动设置为null),boolean,byte,char,short,int,float,reference,returnAddress存一个slot,为对于64位数据(long,double)连续分配2个slot空间。虚拟机通过索引定位使用局部变量表。局部变量表分配结构组成:this,方法参数,方法内部局部变量。此外局部变量表slot不会又初始化,任何未初始化局部变量的调用会造成加载失败.

    • 操作数栈:32位数据占1个容量,64位数据占2个容量.算数运算就是通过操作数栈进行.执行字节码指令时必须与操作数栈的元素类型严格匹配(编译和类校验时会验证).此外不同栈帧的局部变量表和操作数栈可能共享同一片区域,这是一种优化处理.

    • 动态连接:指向运行时常量池中的方法引用,在每一次运行期间转化成直接引用.

    • 返回地址:一种是正常退出方法的返回地址(返回时给调用者返回值),另一种是抛出异常(不返回值给调用者).都要返回到方法被调用的位置.方法退出过程中需要恢复上层方法的局部变量表和操作数栈,将返回值压入调用者的操作数栈,PC指向方法调用指令的下一条指令(即恢复现场).

  3. 方法调用

    • 解析:编译器在编译期就确定好的方法调用,是一个静态过程,类加载解析阶段这一类符号的引用被直接转化成了直接引用.使用解析的方法调用适用于静态方法、私有方法、实例构造器、父类方法.(它们是非虚方法,当然还包括final方法)

    • 分派:

      • 静态分派(方法重载解析):变量分为静态类型和实际类型(A a = new B()A为静态类型,B为实际类型),前者编译期可知,后者运行时可知.静态分派(例如方法重载)是依靠编译器在编译期决定的,即由静态类型决定.(另外静态方法的重载选择也由静态分派完成,也可认为是一种解析)

      • 动态分派(例如重写):在运行时决定调用哪个方法.在重写中,先将对象引用压入栈顶,然后使用invokevirtual指令调用常量池中已经存储的方法.

        invokevirtual指令步骤:找到操作数栈顶第一个元素指向对象的实际类型->在该类型中找到常量池中符合的方法并进行校验,若通过则返回直接引用,否则抛出异常->未找到则按照继承关系从下往上找对应的方法->若未找到则抛异常

      • 单分派/多分派:编译时静态分派,被调用者的静态类型和方法的参数类型将被决定,对2个宗量进行选择,为多分派;运行时,以对象的实际类型为依据,只对一个宗量进行选择,为单分派.(所以Java是静态多分派,动态单分派的语言)

      • 动态分派的实现:在类的方法区建立虚方法表,在类加载的连接阶段初始化.虚方法表中存入各个方法的入口地址,若没被重写则都为父类方法的入口地址;若被重写则被替换成子类版本的入口地址.

    • 动态类型支持:JDK7中的invoke包是其中的实现,提供MethodHandle机制,类似于函数指针的实现.而invokedynamic指令和MethodHandle机制作用类似,参数包含引导方法、方法类型和名称信息.这类分派不是由虚拟机决定的而是由程序员自己决定的.

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

    Java的编译是半独立实现,指令是基于栈的指令集(可移植强,代码紧凑,编译器实现简单,但速度慢).可参考8.4.3小节的执行顺序(如istore将操作数栈顶元素存到局部变量表中,iload将局部变量表的元素压入操作数栈中,其他运算操作都取操作数栈顶的几个元素操作).

Chapter 9

  1. Tomcat:正统类加载器架构

    • 目录结构:/lib存放共用类库(可指定server.loader和share.loader),WebApp/*/WEB-INF存放当前应用可用的类库

    • 类加载器关系:

      • Application<-Common<-Shared<-WebApp<-Jasper

      • Application<-Common<-Catalina

  2. OSGi

    模块称为Bundle,可声明它依赖的Import-Package,也允许这个Bundle被导出和依赖成为Export-Pacakage。

    加载关系不是树状而是图/网状,注意加载的死锁和内存泄漏。

    • java.*开头的类委派给父加载器加载

    • 否则委派列表名单的类委派给父加载器加载

    • Import的类委派给Export这个类的Bundle类加载器加载

    • 不是Import的,查找当前Bundle的Classpath使用自己的类加载器加载

    • 再不行则查找是否有Fragment Bundle,委派给对应Bundle类加载器加载

    • 再不行查找Dynamic Import列表的Bundle给对应Bundle类加载器加载

    • 若还不行,则加载失败

  3. 动态代理

    例:Spring的Bean组织管理(依赖倒置,即若A依赖B,我们不用先得到B再得到A而是B自己会送上门来注入到A).

    ProxyInvocationHandler接口

  4. Retrotranslator(逆向移植工具),将新版本Class文件转变成旧版本.

Chapter 10

  1. Javac编译器

    执行步骤:解析并填充符号表,注解处理,语义分析与字节码的生成

    • 解析,填充符号表:词法分析生成AST,然后将符号地址和符号信息构成K-V表格.

    • 注解处理:注解可以修改AST的任意元素,若修改则编译器重新解析和填充符号表直到所有的插入式注解器都没有对AST修改为止

    • 语义分析和字节码生成:AST构建后会进行语义分析(进行上下文有关性质的检查,如类型检查,标注检查,数据和控制流分析,解析语法糖),然后生成字节码,将上述生成的信息(AST,符号表)转化成字节码写到磁盘(<init>()<clinit>()在这里添加到AST)

  2. 语法糖

    • 泛型与类型擦除:Java的泛型在编译后的字节码文件中类型被擦除(即运行期A<T1>A<T2>都是A),并在相应地方插入强制转型的代码.

      带有泛型的重载,其方法参数也会被擦除->编译期的签名相同而导致不能重载.[但是只要返回值不同就可以重载(但是我的javac 9.0.1不能过编译),因为字节码层面上的签名包含了返回值和抛出的异常.(字节码层面的签名是由Signature等属性引入的)]

      擦除仅仅是Code属性字节码进行擦除,元数据还是得到保留,可用反射得到该类型参数(ParameterizedType类)

    • 自动装/拆箱,遍历循环.编译器使用valueOf()装箱,xxValue()拆箱,遍历(foreach,集合必须实现Iterable接口)时替换成迭代器的实现.(==在遇到算术运算会自动拆箱,equals()不会处理数据类型转换)

    • 条件编译:对于不可到达的代码,只有使用if条件才能过编译,编译器可以将if块中不能到达的代码消除掉达到条件编译的目的(即只能实现基本语句块的条件编译).

Chapter 11

  1. JVM中通常使用解释器解释执行的.而通常JVM中还会有一个编译器JIT在运行时将字节码编译成本地机器码进行优化,让机器直接执行.

  2. Hotspot中2个JIT:Client(C1) & Server(C2) Compiler,默认采用解释器和一种JIT(取决于运行模式)运行.Hotspot采用分层编译层次,如下:

    • 0:解释执行

    • C1:将字节码编译成本地代码,简单优化,可加入性能监控逻辑

    • C2:将字节码编译成本地代码,进行较长时间优化,可能根据性能监控信息进行不可靠的激进优化

  3. 编译触发

    • 编译的热点代码:多次调用/执行的方法体(编译整个方法)和循环体(依旧编译整个方法,使用栈上替换,即方法栈帧还在,但方法被替换了).

    • 探测方法:采样、计数器(Hotspot使用,使用方法调用计数器和回边计数器)

  4. 编译过程

    • 编译通常在后台执行,即未完成编译时依旧解释执行

    • C1:简单的三段式编译(静态单分配的高级中间代码&优化->转换成低级中间代码&优化->线性扫描&在LIT分配寄存器并生成机器代码)

    • C2:能达到GNU C++ -O2优化强度,执行所有经典优化动作,寄存器分配使用全局图作色分配器

  5. 经典编译优化技术

    • 公共子表达式消除:若表达式E已被计算且在某段时间内E的值没变化,那么在这段时间内若再次出现E,则E为公共子表达式,只要用前面计算的结果代替即可,不需要再次运算.

    • 数组范围检查消除:通过编译器数据流分析判断索引变量没有越界,不必要的运行时范围检查判断就可以去掉(即可以在编译器去除不必要的数组范围检查);另一种方法就是隐式异常优化,即不检查范围,但若出现错误,则直接进入异常中断处理器(虚拟机会注册).

    • 方法内联:将简单的方法直接展开,使程序调用该方法时不必要调用栈的操作(类似C++ inline方法).由于大多数都是虚方法,JIT使用类型继承关系分析技术实现,但需要有一个后门,当内联代码由于继承关系改变而不能使用/产生变化时要能退回解释执行状态.这是一种激进的优化.

    • 逃逸分析:不是直接优化代码,而是为其他代码优化手段提供依据.其分析对象的动态作用域(如对象被外部方法调用->方法逃逸,对象被外部线程访问到->线程逃逸),若该对象不会逃逸,则肯为这个变量进行优化,如栈上分配、同步消除、标量替换(标量不可再分解)等。不过逃逸分析开销较大,不是很成熟,默认不开启.