1-类编译加载执行过程
先了解下Java从编译到运行的整个过程
类编译:在编写好代码之后,我们需要将 .java文件编译成 .class文件,才能在虚拟机上正常运行代码。文件的编译通常是由JDK中自带的Javac工具完成,一个简单的 .java文件,我们可以通过javac命令来生成 .class文件。我们可以通过javap反编译来看看一个class文件结构中主要包含了哪些信息。编译后的字节码文件主要包括常量池和方法表集合。
常量池主要记录的是类文件中出现的字面量以及符号引用。字面常量包括字符串常量(例如String str=“abc”,其中"abc"就是常量),声明为final的属性以及一些基本类型(例如,范围在-127-128之间的整型)的属性。符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用(例如String str=“abc”,其中str就是成员变量引用)等。
方法表集合中主要包含一些方法的字节码、方法访问权限(public、protect、prviate等)、方法名索引(与常量池中的方法引用对应)、描述符索引、JVM执行指令以及属性集合等。
类加载:当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。不同的实现类由不同的类加载器加载,JDK中的本地方法类一般由根加载器(Bootstrp loader)加载进来,JDK中内部实现的扩展类一般由扩展加载器(ExtClassLoader)实现加载,而程序中的类文件则由系统加载器(AppClassLoader)实现加载。在类加载后,class类文件中的常量池信息以及其它数据会被保存到JVM内存的方法区中。
类连接:类在加载进来之后,会进行连接、初始化,最后才会被使用。在连接过程中,又包括验证、准备和解析三个部分。
验证:验证类符合Java规范和JVM规范,在保证符合规范的前提下,避免危害虚拟机安全。
准备:为类的静态变量分配内存,初始化为系统的初始值。对于final static修饰的变量,直接赋值为用户的定义值。例如,private final static int value=123,会在准备阶段分配内存,并初始化值为123,而如果是 private static int value=123,这个阶段value的值仍然为0。
解析:将符号引用转为直接引用的过程。在编译时,Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法,就需要把它们转化为JVM可以直接获取的内存地址或指针,即直接引用。
类初始化阶段是类加载过程的最后阶段,在这个阶段中,JVM首先将执行构造器<clinit>方法,编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>() 方法。
初始化类的静态变量和静态代码块为用户自定义的值,初始化的顺序和Java源码从上到下的顺序一致。
JVM 会保证 <clinit>() 方法的线程安全,保证同一时间只有一个线程执行。
2-即时编译
在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。
在HotSpot虚拟机中,内置了两个JIT,分别为C1编译器和C2编译器,这两个编译器的编译过程是不一样的。
C1编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,例如,GUI应用对界面启动速度就有一定要求。
C2编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。根据各自的适配性,这两种即时编译也被称为Client Compiler和Server Compiler。
在Java8中,默认开启分层编译,-client和-server的设置已经是无效的了。如果只想开启C2,可以关闭分层编译(-XX:-TieredCompilation),如果只想用C1,可以在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1。
除了这种默认的混合编译模式,我们还可以使用“-Xint”参数强制虚拟机运行于只有解释器的编译模式下,这时JIT完全不介入工作;我们还可以使用参数“-Xcomp”强制虚拟机运行于只有JIT的编译模式下。
在HotSpot虚拟机中的热点探测是JIT优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法” 。
经典的编译优化技术:方法内联-栈上分配。
方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。
经常执行的方法,默认情况下,方法体大小小于325字节的都会进行内联,我们可以通过
-XX:MaxFreqInlineSize=N来设置大小值;
不是经常执行的方法,默认情况下,方法大小小于35字节才会进行内联,我们也可以通过
-XX:MaxInlineSize=N来重置大小值。
栈上分配:
在Java中默认创建一个对象是在堆中分配内存的,而当堆内存中的对象不再使用时,则需要通过垃圾回收机制回收,这个过程相对分配在栈中的对象的创建和销毁来说,更消耗时间和性能。这个时候,逃逸分析如果发现一个对象只在方法中使用,就会将对象分配在栈上。
还有一些其他的优化技术,比如标量替换和锁消除等。