Java虚拟机

目录

自动内存管理机制

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

一、运行时数据区域

二、虚拟机中对象的创建

三、内存泄漏和内存溢出

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

一、概念

二、如何判断对象“已死”

三、垃圾收集算法

四、垃圾收集器

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

一、JDK的命令行工具

二、JDK的可视化工具

虚拟机执行子系统

类文件结构

一、相关

二、Class类文件的结构

虚拟机类加载机制

一、相关

二、类加载的时机

三、类加载的过程

四、类加载器

虚拟机字节码执行引擎

一、相关

二、运行时栈帧结构

三、方法调用

类加载及执行子系统的案例

一、Tomcat的类加载器架构

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

程序编译与代码优化

早期(编译期)优化

一、相关

二、Javac编译器

三、Java语法糖

晚期(运行期)优化

一、相关

二、HotSpot虚拟机内的即时编译器

三、 编译优化技术

四、Java与C/C++的编译器对比


自动内存管理机制

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

一、运行时数据区域

Java虚拟机_第1张图片

1. 程序计数器

  • 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器;
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令;
  • 线程私有;
  • 唯一一个没有规定任何OutOfMemoryError情况的区域;

2. 虚拟机栈

  • 线程私有,生命周期与线程相同;
  • 描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程;
  • 局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型;
  • 栈容量只由-Xss参数设定;
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

3. 本地方法栈

  • 类似虚拟机栈,区别在于本地方法栈为虚拟机使用到的Native方法服务;

4. 堆

  • 线程共享;
  • 在虚拟机启动时创建,存放对象实例,包括数组的分配;
  • 从内存回收的角度来看(分代收集算法),可细分为:新生代和老年代;
  • 可通过-Xmx和-Xms扩展,设置为一样可避免堆自动扩展;
  • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常;

5. 方法区

  • 线程共享;
  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码;
  • 也可成为“永久代”;
  • 此区域的内存回收目标主要是针对常量池的回收和对类的卸载;
  • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常;

6. 运行时常量池

  • 是方法区的一部分;
  • 用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放;
  • 运行期间也可能将新的常量放入池中,如String类的intern()方法;
  • JDK1.7开始逐步“去永久代”;
  • 通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量;
  • 当常量池无法再申请到内存时会抛出OutOfMemoryError异常;

7. 直接内存

  • JDK1.4中加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作;
  • 如果忽略直接内存,使得各个内存区域和大于物理内存限制,会导致动态扩展时出现OutOfMemoryError异常;
  • 通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值一样(-Xmx);

二、虚拟机中对象的创建

1. 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

2. 在类加载检查通过后,接下来虚拟机将为新生对象分配内存

3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4. 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头之中。

5. 执行new指令之后会接着执行方法,把程序按照程序员的意愿进行初始化,最终产生一个真正可用的对象。

三、内存溢出和内存泄漏

1. 概念

  • 内存溢出:内存中的对象是必须的,但是内存不够;
  • 内存泄漏:内存中的对象不是必须的,但是垃圾收集器无法自动回收;

2. Java堆溢出

  • Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。
public class HeapOOM {
    
    static class OOMObject {
    
    }
    
    public static void main(String[] args) {
        List list = new ArrayList();

        while(true) {
            list.add(new OOMObject());
        }
    }    
}

//结果:java.lang.OutOfMemoryError: Java heap space

3. 虚拟机栈和本地方法栈溢出

public class JavaVMStackSOF {

    private int stackLength = 1;
    
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

//结果:java.lang.StackOverflowError
public class JavaVMStackOOM {

    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }    
}

//结果:java.lang.OutOfMemoryError: unable to create new native thread

4. 方法区和运行时常量池溢出

  • String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
public class RuntimeConstantPoolOOM {

    List list = new ArrayList();

    int i = 0;
    while (true) {
        list.add(String.valueOf(i++).intern());
    }

}

//结果:OutOfMemoryError: PermGen space

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

一、概念

1. 为什么需要了解GC和内存分配?

  • 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,需要对这些“自动化”的技术实施必要的监控和调节。

2. Java堆和方法区这部分内存的分配和回收是动态的,垃圾收集器所关注的是这部分内存。

二、如何判断对象“已死”

1. 引用计数算法

  • 给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的;
  • 引用计数算法实现简单,判定效率也很高,但是主流Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题;
public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;
    
    //占内存
    private byte[] bigSize = new byte[2 * _1MB];
    
    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        
        objA = null;
        objB = null;

        System.gc();
    }    
}

2. 可达性分析算法

  • 通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即不可达时,则证明此对象是不可用的;
  • GC Roots对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象;

Java虚拟机_第2张图片

3. 引用

  • 强引用:类似 Objcet obj = new Object(); 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象;
  • 软引用:描述一些还有用但并非必需的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之内进行第二次回收;
  • 弱引用:也是描述一些还有用但并非必需的对象,但是,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象;
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知;

4. 对象死亡过程

  • 要真正宣告一个对象死亡,至少要经历两次标记过程;
  • 1)如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”;
  • 2)如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()方法中成功拯救自己(重新与引用链上的任何一个对象建立关联),那在第二次标记时它将被移除出“即将回收”的集合,如果对象没有拯救自己,那就被回收了;
  • 任何一个对象的finalize()方法都只会被系统自动调用一次,不建议使用该方法,因为它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,“关闭外部资源”之类的工作可以使用try-finally来替代;

5. 回收方法区

  • 在方法区中进行垃圾收集的性价比一般比较低,在堆中,尤其是新生代中,常规应用一次垃圾收集一般可以回收75%~95%的空间,而永久代的垃圾收集效率远低于此;
  • 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类;
  • 回收废弃常量与回收Java堆中的对象非常类似;
  • 判定一个类是否是无用的类需要同时满足3个条件:1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例  2)加载该类的ClassLoader已经被回收  3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

三、垃圾收集算法

1. 标记-清除算法

  • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象;
  • 不足:一个是效率问题,另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片大多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作;

2. 复制算法

  • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉;
  • 优点:实现简单,运行高效;
  • 不足:将内存缩小为原来的一半;
  • 现状:用于回收新生代,不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间,HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保;

3. 标记-整理算法

  • 老年代使用该算法;
  • 标记过程仍然与“标记-清理算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存;

4. 分代收集算法

  • 即在新生代中使用复制算法,在老年代中使用“标记-清理”或者“标记-整理”算法;

四、垃圾收集器

1. Serial收集器

  • 单线程收集器,必须暂停其他所有的工作线程,直到它收集结束;
  • 采用复制算法;
  • 是虚拟机运行在Client模式下的默认新生代收集器;

2. ParNew收集器

  • 是Serial收集器的多线程版本;
  • 采用复制算法;
  • 是虚拟机运行在Server模式下的首选的新生代收集器;

3. Parallel Scavenge收集器

  • 新生代收集器;
  • 采用复制算法;
  • 多线程收集器;
  • 关注点在于达到一个可控制的吞吐量,而其他收集器关注点在于尽可能地缩短垃圾收集时用户线程的停顿时间;

4. Serial Old收集器

  • 老年代收集器;
  • 单线程收集器;
  • 采用标记-整理算法;
  • 给Client模式下的虚拟机使用;

5. Parallel Old收集器

  • 老年代收集器;
  • 多线程收集器;
  • 采用“标记-整理”算法;

6. CMS收集器

  • 是一种以获取最短回收停顿时间为目标的收集器;
  • 基于标记-清除算法实现;

7. G1收集器

  • 并行与并发;
  • 分代收集;
  • 空间整合;
  • 可预测的停顿;

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

一、JDK的命令行工具

1. 当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程Talnet到服务器上都可能会收到限制,借助tools.jar类库里面的接口,可以直接在应用程序中实现功能强大的监控分析功能。

2. jps

  • 显示指定系统内所有的HotSpot虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID(LVMID);
  • 对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID)是一致的;
  • 命令格式:jps [options] [hostid];
  • jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名;
  • 选项,-q:只输出LVMID,-m:输出虚拟机进程启动时传递给主类main()函数的参数,-l:输出主类的全名,如果进程执行的是jar包,输出jar路径,-v:输出虚拟机进程启动时jvm参数;

3. jstat

  • 用于收集HotSpot虚拟机各方面的运行数据;
  • 可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据;

4. jinfo

  • 实时地查看和调整虚拟机各项参数;

5. jamp

  • 生成虚拟机的内存转储快照(heapdump文件);

6. jhat

  • 用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果;

7. jstack

  • 显示虚拟机的线程快照;

8. HSDIS

  • JIT生成代码反汇编;

二、JDK的可视化工具

1. JConsole

  • java监视与管理控制台;
  • 启动JConsole:通过JDK/bin目录下的jconsole.exe启动,将自动搜索出本机运行的所有虚拟机进程,选择一个进程即可开始监控;

2. VisualVM

  • 多合一故障处理工具;
  • 除了运行监视、故障处理外,还有性能分析;
  • 不需要被监视的程序基于特殊Agent运行,因此它对应用程序的实际性能的影响很小,使得它可以直接应用在生产环境中;

虚拟机执行子系统

类文件结构

一、相关

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

2. 与平台无关的理想最终实现在操作系统的应用层上:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的一次编写,到处运行。

3. 可以运行在Java虚拟机上的语言:Clojure、Groovy、JRuby、Jython、Scala等。

4. 实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

Java虚拟机_第3张图片

二、Class类文件的结构

1. 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来,类或接口并不一定都得定义在文件里,因为也可以通过类加载器直接生成。

2. Class文件是一组以8位字节为基础单位的二进制流;

3. Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值;
  • 表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表;

虚拟机类加载机制

一、相关

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

2. Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。

二、类加载的时机

1. 类的生命周期

Java虚拟机_第4张图片

2. 其中1-5阶段的顺序是确定的,而解析阶段则不一定,解析阶段也可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定。

3. 5种情况必须对立即对类进行初始化

  • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化,具体场景:使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法的时候;
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化;
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
  • 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

注:这5种场景的行为称为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,成为被动引用。

4. 被动引用举例

  • 通过子类引用父类的静态字段,不会导致子类初始化;
public class SuperClass {

    static {
        System.out.println("SuperClass init")
    }

    public static int value = 123;
}

public class SubClass extends SuperClass{

    static {
        System.out.println("SubClass init")
    }
}

public class Main {
    
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

//输出结果:
//SuperClass init
//123

注:对于静态字段,只有直接定义这个字段的类才会被初始化。

  • 通过数组定义来引用类,不会触发此类的初始化;
public class SuperClass {

    static {
        System.out.println("SuperClass init")
    }
}

public class Main {
    
    public static void main(String[] args) {
        SuperClass[] sc = new SuperClass[10];
    }
}

//输出结果:无

注:该代码触发了另外一个名为“[L包名.SuperClass”的类的初始化阶段,这个类代表了一个元素类型为包名.SuperClass的一维数组。

  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;
public class SomeClass {

    static {
        System.out.println("SomeClass init");
    }

    public static final String HELLO_WORLD = "hello world";
}

public class Main {
    
    public static void main(String[] args) {
        System.out.println(SomeClass.HELLO_WORLD);
    }
}

注:如上面第1种情况所描述:被final修饰、已在编译期把结果放入常量池的静态字段不会触发该类的初始化。

5. 接口的初始化过程与类的初始化过程有细微差别,在第3种情况:接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化,如引用接口中定义的常量。

三、类加载的过程

1. 加载

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 通过这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

注1:其中第1条包括这样一些方式:

  • 从ZIP包中获取(JAR、EAR、WAR);

  • 从网络中获取(Applet);

  • 运行时计算生成(动态代理技术);

  • 由其他文件生成(JSP);

  • 从数据库中获取;

注2:数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,一个数组类的创建过程遵循一下规则:

  • 如果数组的组件类型,即数组去掉一个维度的类型是引用类型,那就递归采用上面的加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识;
  • 如果数组的组件类型不是引用类型,如int[]数组,Java虚拟机将会把数组标记为与引导类加载器关联;
  • 数组类的可见性与它的组建类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public;

2. 验证

  • 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;

3. 准备

  • 准备阶段是正式为类变量分配内存并设置初始值的阶段,这些阶段所使用的内存都将在方法区中进行分配;
  • 准备阶段进行内存分配的仅包括类变量(被static修饰的变量),实例变量将会在对象初始化时随着对象一起分配在Java堆中;
  • 准备阶段的初始值通常是指数据类型的零值,例如“public static int value = 123;”,准备阶段过后的初始值是0,在初始化阶段才会赋值为123,特殊情况是final关键字修饰的变量,会在准备阶段就赋值;

4. 解析

  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程;
  • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;
  • 直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄;

5. 初始化

  • 初始化阶段是执行类构造器()方法的过程;
  • ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量可以赋值,但不能访问;
  • ()方法与类的构造函数()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕;
  • 由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作;
public class Test {

    static class Parent {

        public static int A = 1;
        static {
            System.out.println(A);      //1
            A = 2;
            //System.out.println(B);    //编译失败
            B = 2;                      //能访问到,但是赋值没成功
        }
        public static int B = 1;
    }

    static class Sub extends Parent {
        public static int C = A;
        public static int D = 1;
    }

    public static void main(String[] args) {
        System.out.println(Sub.A);      //2
        System.out.println(Sub.B);      //1
        System.out.println(Sub.C);      //2
        System.out.println(Sub.D);      //1
    }
}

Java虚拟机_第5张图片

  • ()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法;
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法,但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量使用时,父接口才初始化,另外,接口的实现类在初始化时同样也不会执行接口的()方法;
  • 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕;
static class DeadLoopClass {

    static {
        try {
            if (true) {
                System.out.println(Thread.currentThread() + " init DeadLoopClass " + System.currentTimeMillis());
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + " start " + System.currentTimeMillis());
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " stop " + System.currentTimeMillis());
            }
        };
        new Thread(runnable, "A").start();
        new Thread(runnable, "B").start();
    }
}

Java虚拟机_第6张图片

四、类加载器

1. 将类加载阶段中的“通过一个类的全限定名来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获得所需要的类,这个动作的代码模块成为“类加载器”。

2. 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,相等包括:代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果和使用instanceof关键字做对象所属关系判定的情况。

public class ClassLoaderTest {

    public static void main(String[] args) {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (Exception e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = null;
        try {
            obj = myLoader.loadClass("ClassLoaderTest").newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //obj由自定义类加载器加载,ClassLoaderTest由系统应用程序类加载器加载
        System.out.println(obj instanceof ClassLoaderTest);    //false
    }
}

3. 类加载器的双亲委派模型

Java虚拟机_第7张图片

  • 启动类加载器:负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载),开发者无法直接使用;
  • 扩展类加载器:负责加载\lib\ext目录中的,或被java.ext.dirs系统变量所指定的路径中的类库,开发者可以直接使用;
  • 应用程序类加载器:这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,负责加载用户类路径(ClassPath)上的所制定的类库,开发者可以直接使用,也是程序中默认的类加载器;
  • 双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载;

虚拟机字节码执行引擎

一、相关

1. 虚拟机的执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

2. 在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地码执行)两种选择,也可能两者兼备,甚至还可能会包括几个不同级别的编译器执行引擎,但从外观上看来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

二、运行时栈帧结构

1. 栈帧

  • 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素;
  • 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
  • 在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现;

2. 局部变量表

  • 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量;
  • 局部变量表的容量以变量槽Slot为最小单位,每个Slot都应该能存访一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据;
  • 虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量最大的Slot数量。

3. 操作数栈

  • 操作数栈是一个先入先出栈,每一个元素可以是任意的Java数据类型,32位数据类型所占栈容量为1,64位数据类型所占栈容量为2;
  • 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作;

4. 动态连接

  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接;
  • Class文件的常量池中存有大量的符号引用,一部分会在类加载阶段或者第一次使用的时候转化为直接引用,成为静态解析,另一部分将在每一次运行期间转化为直接引用,成为动态连接;

5. 方法返回地址

  • 方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值;
  • 方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一班不会保存这部分信息;

三、方法调用

1. 方法调用阶段唯一的任务就是确定被调用方法的版本。

2. 静态解析:所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。这些方法包括:静态方法、私有方法、实例构造器(()方法)、父类方法、final方法。这些方法也称为非虚方法

public class Main {
    
    public static void sayHello() {
        System.out.println("hello");
    }

    public static void main(Stringp[] args) {
        Main.sayHello();
    }
}

3. 静态分派

public class Overload {

    static class Human {
    }

    static class Man extends Human {
    }

    static class Woman extends Human {
    }

    public void sayHello(Human human) {
        System.out.println("human");
    }

    public void sayHello(Man man) {
        System.out.println("man");
    }

    public void sayHello(Woman woman) {
        System.out.println("woman");
    }

    public static void main(String []args) {
        Human human = new Human();
        Human man = new Man();
        Human woman = new Woman();
        Overload o = new Overload ();
        o.sayHello(human);
        o.sayHello(man);
        o.sayHello(woman);
    }
}

  • 将Human称为变量的静态类型,将Man或Woman称为变量的实际类型;
  • 重载时是通过参数的静态类型而不是实际类型作为判定依据的,并且静态类型是编译器可知的;
  • 重载中的自动转型按照char->int->long->float->double->包装类型->包装类型实现的接口->包装类型的父类->可变长参数;

4. 动态分派

public class Override {

    static class Human {
        public void sayHello() {
            System.out.println("human");
        }
    }
    static class Man extends Human {
        public void sayHello() {
            System.out.println("man");
        }
    }
    static class Woman extends Human {
        public void sayHello() {
            System.out.println("woman");
        }
    }
    public static void main(String []args) {
        Human human = new Human();
        Human man = new Man();
        Human woman = new Woman();
        human.sayHello();
        man.sayHello();
        woman.sayHello();
    }
}

5. 单分派和多分派

  • 方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种;
  • 单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择;
public class Dispatch {

    static class QQ {}

    static class _360 {}

    public static class Father {
        public void hardChoice(QQ arg) {
            System.out.println("father choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("father choose 360");
        }
    }

    public static class Son extends Father {
        public void hardChoice(QQ arg) {
            System.out.println("son choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        father.hardChoice(new _360());
        father.hardChoice(new QQ());

        Father son = new Son();
        son.hardChoice(new _360());
        son.hardChoice(new QQ());

        Son son1 = new Son();
        son.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}

Java虚拟机_第8张图片

  • 在编译阶段,也就是静态分派的过程,这时选择目标方法的依据有两点:一是静态类型是Father还是Son,而是方法参数是QQ还是360,因此是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型;
  • 在运行阶段,也就是动态分派的过程,因为在编译期已经确定目标方法的参数,所以唯一影响虚拟机选择的因素只有此方法的接收者的实际类型是Father还是Son,因此只有一个宗量作为选择依据,所有Java语言的动态分派属于单分派类型;

6. 动态类型语言支持

  • 动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期,相对的,在编译器就进行类型检查过程的语言就是静态类型语言;
  • JDK1.7以前的字节码指令集种,4条方法调用指令(invokevirtual、invokespecial、invokestatic、invokeinterface)的第一个参数都是被调用的方法的符号引用,方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定接收者类型;
  • JDK1.7实现了JSR-292,包括invokedynamic指令以及java.lang,invoke包,目的是提出一种新的动态确定目标方法的机制,称为MethodHandle;
  • Reflection和MethodHandle机制都是在模拟方法的调用,区别在于:1)Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用,2)Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息多,前者包含了方法的签名、描述以及方法属性表中各种属性的Java端表示方式,还包括执行权限等运行期信息,而后者仅仅包含与执行该方法相关的信息,3)MethodHandle是对字节码的方法指令调用的模拟,所以理论上虚拟机在这方面做的各种优化,也可以在MethodHandle上提供支持;
  • 例:JDK1.7以前,可以通过super关键字调用父类中的方法,但是无法访问祖类的方法,JDK1.7中,可以解决这个问题;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

class Test {

    class GrandFather {
        void thinking() {
            System.out.println("i am grandfather");
        }
    }

    class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father");
        }
    }

    class Son extends Father {
        void thinking() {
            try {
                MethodType mt = MethodType.methodType(void.class);

                //JDK1.7
                //MethodHandle mh = MethodHandles.lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());

                //JDK1.8
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                MethodHandle mh = ((MethodHandles.Lookup) IMPL_LOOKUP.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);

                mh.invoke(this);
            } catch (Throwable e) {
            }
        }
    }

    public static void main(String[] args) {
        (new Test().new Son()).thinking();
    }
}

类加载及执行子系统的案例

一、Tomcat的类加载器架构

1. 一个功能健全的Web服务器,要解决如下问题:

  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离;
  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互共享;
  • 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响;
  • 支持JSP应用的Web服务器,大多数都需要支持HotSwap功能;

2. Tomcat目录结构中,有4组目录可以存放Java类库:

  • /common:类库可被Tomcat和所有的Web应用程序共同使用;
  • /server:类库可被Tomcat使用,对所有的Web应用程序都不可见;
  • /shared:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见;
  • /WebApp/WEB-INF:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见;

Java虚拟机_第9张图片

注:其中WebApp类加载器和JSP类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器。

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

1. java.lang.reflect.Proxy或实现java.lang.reflect.InvocationHandler接口。

2. Spring中如果Bean是面向接口编程,那么在Spring内部都是通过动态代理的方式来对Bean进行增强的。

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

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DynamicProxyTest {

    interface IHello {
        void sayHello();
    }

    static class Hello implements IHello {
        @Override
        public void sayHello() {
            System.out.println("hello");
        }
    }

    static class DynamicProxy implements InvocationHandler {
        Object originalObj;

        Object bind(Object originalObj) {
            this.originalObj = originalObj;
            return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("welcome");
            return method.invoke(originalObj, args);
        }
    }

    public static void main(String[] args) {
        IHello hello = (IHello) new DynamicProxy().bind(new Hello());
        hello.sayHello();
    }
}

程序编译与代码优化

早期(编译期)优化

一、相关

1. Java的编译期

  • 可能是一个前端编译器把*.java文件转变成*.class文件的过程,如Sun的Javac;
  • 也可能是指虚拟机的后端运行期编译器(JIT)把字节码转变成机器码的过程,如HotSpot VM的C1、C2编译器;
  • 还可能是指使用静态提前编译器(AOT)直接把*.java编译成本地机器码的过程;

2. 虚拟机设计团队把性能的优化集中到了后端的即时编译器中,这样可以让那些不是由javac产生的Class文件也同样能享受到编译器优化所带来的好处。

3. javac编译器做了许多针对Java语言编码过程的优化措施来改善程序员的编码风格和提高编码效率,如语法糖。

二、Javac编译器

1. javac编译器是由Java语言编写的程序。

2. 编译过程大致有3个步骤:

  • 解析与填充符号表过程;
  • 插入式注解处理器的注解处理过程;
  • 语义分析与字节码生成过程;

三、Java语法糖

1. 泛型与类型擦除

  • 泛型是JDK1.5的一项新增特性,它的本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法;
  • Java中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList与ArrayList就是同一个类,Java中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型;
public class Main {
    
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("hello", "你好");
        System.out.println(map.get("hello"));
    }
}

//编译为class文件后反编译

public class Main {
    
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("hello", "你好");
        System.out.println((String) map.get("hello"));
    }
}

2. 自动装箱、拆箱与遍历循环

public class Main {

    public static void main(String[] args) {
        List list = Arrays.asList(1, 2, 3, 4);
        //List list = [1, 2, 3, 4];        //JDK1.8
        int sum = 0;
        for (int i : list) {
            sum += i;
        }
        System.out.println(sum);
    }
}

//编译为class文件后反编译

public class Main {

    public static void main(String[] args) {
        List list = Arrays.asList(new Integer[] {
            Integer.valueOf(1),
            Integer.valueOf(2),
            Integer.valueOf(3),
            Integer.valueOf(4)
        });
        int sum = 0;
        for (Iterator localIterator = list.iterator(); localIterator.hasNext();) {
            int i = ((Integer) localIterator.next()).intValue();
            sum += i;
        }
        System.out.println(sum);
    }
}

注:加减乘除运算的时候,引用类型会自动拆箱为基本类型;“==”运算的时候,如果是两个引用类型,则比较的是地址,是否同一个对象引用,如果是一个引用类型一个基本类型,则引用类型会自动拆箱为基本类型,比较的是值是否相等,如果是两个基本类型,则比较的是值是否相等;“equals()”判断的时候,如果是一个引用类型一个基本类型,基本类型会自动装箱为引用类型。

3. 条件编译

  • 许多程序设计语言都提供了条件编译的途径,如C、C++中使用预处理器指示符#ifdef来完成条件编译;
  • Java进行条件编译的方法就是使用条件为常量的if语句;
public class Main {

    public static void main(String[] args) {
        if (true) {        //必须使用条件为常量的if语句才行
            System.out.pritnln("true");
        } else {
            System.out.println("false");
        }

        while (false) {        //编译失败
            System.out.println("while");     
        }
    }
}

//编译为class文件后反编译

public class Main {

    public static void main(String[] args) {
        System.out.pritnln("true");
    }
}

4. 其他语法糖

  • 内部类、枚举类、断言语句、对枚举和字符串的switch支持、try语句中定义和关闭资源等。

晚期(运行期)优化

一、相关

1. 当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为热点代码,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT)。

2. 即时编译器并不是虚拟机必须的部分,但是即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。

二、HotSpot虚拟机内的即时编译器

1. 解释器与编译器

  • 解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行,在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率;
  • 当程序运行环境中内存资源限制较大(嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率;
  • 解释器还可以作为编译器激进优化时的一个逃生门;
  • HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,简称为C1编译器和C2编译器,主流的HotSpot虚拟机中默认采用解释器与其中一个编译器直接配合的方式工作,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用-client和-server参数去强制指定;
  • 无论采用的编译器是什么模式,解释器与编译器搭配使用的方式在虚拟机中称为混合模式,可以通过-Xint设置为解释模式(Interpreted Mode),通过-Xcomp设置为编译模式(Compiled Mode)(已废弃),可以通过java -version查看这三种模式;

2. 编译对象与触发条件

  • 在运行过程中会被即时编译器编译的热点代码有两类:1)被多次调用的方法,2)被多次执行的循环体;
  • 目前主要的热点探测判定方式有两种:1)基于采样的热点探测,虚拟机周期性地检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是热点方法,2)基于计数器的热点探测,虚拟机会为每个方法建立计数器,统计方法的执行次数,如果超过一定的阈值就认为它是热点方法;
  • HotSpot虚拟机中采用的是第二种方式,为每个方法准备了两类计数器:1)方法调用计数器,2)回边计数器;
  • 方法调用计数器是用于统计被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次,阈值可以通过-XX:CompileThreshold设定,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数,当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,是在虚拟机进行垃圾收集时顺便进行的;
  • 回边计数器的作用是统计一个方法中循环体代码执行的次数,没有计数器热度衰减过程;

三、 编译优化技术

1. 示例

  • 原始代码
static class B {

    int value;
    final int get() {
        return value;
    }
}

public void foo() {
    y = b.get();
    //
    z = b.get();
    sum = y + z;
}
  • 第一步进行方法内联
static class B {

    int value;
    final int get() {
        return value;
    }
}

public void foo() {
    y = b.value;
    //
    z = b.value;
    sum = y + z;
}
  • 第二步进行冗余访问消除
static class B {

    int value;
    final int get() {
        return value;
    }
}

public void foo() {
    y = b.value;
    //
    z = y;
    sum = y + z;
}
  • 第三步进行复写传播
static class B {

    int value;
    final int get() {
        return value;
    }
}

public void foo() {
    y = b.value;
    //
    y = y;
    sum = y + y;
}
  • 第四步进行无用代码消除
static class B {

    int value;
    final int get() {
        return value;
    }
}

public void foo() {
    y = b.value;
    //
    sum = y + y;
}

2. 公共子表达式消除

  • 如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式,对于这种表达式,没有必要花时间再对它进行计算,只需要直接使用前面计算过的表达式结果代替E就可以了;
  • 例如“int d = (c * b) * 12 + a + (a + b * c);”,在即时编译时,会进行如下优化:将表达式视为“int d = E * 12 + a + (a + E);”,然后还可以进行代数化简”int d = E * 13 + a * 2“;

3. 数组边界检查消除

  • 在Java语言中访问数组元素的时候系统将自动进行上下界的范围检查,不满足将抛出一个运行时异常java.lang.ArrayIndexOutOfBoundsException;
  • 如果编译器只要通过数据流分析就可以判定循环变量的取值范围,那在运行期间就可以把数组的上下界检查消除,这样可以节省很多次的条件判断操作;
  • 除了数组边界检查优化这种尽可能把运行期检查提到编译器完成的思路之外,另外还有一种避免思路:隐式异常处理,Java中空指针检查和算术运算中的除数为0检查都采用了这种思路;

4. 方法内联

  • 编译器在进行内联时,如果是非虚方法,那么直接进行内联就可以了,这时候的内联是有稳定前提保障的;
  • 如果遇到虚方法,则会向CHA(类型继承关系分析技术)查询此方法在当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,那也可以进行内联,不过需要预留一个逃生门;
  • 如果查询出来的结果是有多个版本的目标方法可供选择,则编译器使用内联缓存来完成方法内联;

5. 逃逸分析

  • 逃逸分析的基本行为就是分析对象动态作用域;
  • 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸;
  • 甚至还有可能被外部线程访问到,譬如复制给类变量或可以在其他线程中访问的实例变量,称为线程逃逸;

四、Java与C/C++的编译器对比

1. Java虚拟机的即时编译器与C/C++的静态优化编译器相比,可能会由于下列这些原因而导致输出的本地代码有一些劣势:

  • 即时编译器运行占用的是用户程序的运行时间;
  • Java是动态的类型安全语言,虚拟机要确保程序不会违反语言语义或访问非结构化内存;
  • Java中使用虚方法的频率远远大于C/C++,意味着方法内联的难度大;
  • Java可以动态扩展,运行时加载新的类可能改变程序类型的继承关系,使得很多全局的优化都难以进行;
  • Java中对象的内存分配都是在堆上进行的,只有方法中的局部变量才能在栈上分配,而C/C++有多种内存分配方式;
  • Java由垃圾收集器回收内存,存在无用对象的筛选过程,而C/C++主要由用户程序代码来回收;

你可能感兴趣的:(Java,虚拟机)