一个程序需要先编译javac,然后才能够运行java。
代码编译的结果是从本地机器码变为字节码,是存储格式发展的一小步,却是编程语言的一大步。
java语言中,类型的加载、连接和初始化过程都是在运行期间完成的。
源码(.java文件) -> 编译(命令为javac xx.java,字节码文件,以.class结尾,与源码在同一目录下) -> 运行(命令为java xx,这时候java虚拟机将会启动,执行编译好的文件)

Class文件

将java文件编译为*.class文件,然后虚拟机就可以识别其中的内容,从而完成程序的执行
class文件是以8字节为基础单位的二进制流
组成部分有

  • 每个class文件的前4个字节成为魔数,作用是确定这个文件能否被虚拟所有接受,其值为0xCAFEBABE
  • 常量池
  • 类索引和接口索引
  • 字段表、方法表和属性表

分析class文件的指令javap -verbose classname
class文件是字节有序、大端存储的

大端法和小端法

其实 big endian 是指低地址存放最高有效字节( MSB ),而 little endian 则是低地址存放最低有效字节( LSB )。大弟高,小弟低
所有网络协议也都是采用 big endian 的方式来传输数据的。所以有时我们也会把 big endian 方式称之为网络字节序。当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成为网络字节序后再进行传输。

1
2
3
4
5
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 12 | 34 | 56 | 78 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Little Endian

1
2
3
4
5
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 78 | 56 | 34 | 12 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字节码指令

java虚拟机的指令是由一个字节长度的、操作数和参数而构成,又称作字节码指令。

类的加载

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

类的生命周期
类的生命周期

在java语言中,类型的加载、连接和初始化都是在程序运行期间完成的,虽然这样会稍微增加性能开销,但是却为java程序提供了高度的灵活性。
java可以扩展语言的特性就是依赖运行期动态加载和动态连接完成的。

加载

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

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

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在方法区中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

注意:类加载(class loading)与加载(loading)是完全不同的概念

类的加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
  • 所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  • 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。

双亲委派模型

双亲委派模型
双亲委派模型

这种层次关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类

使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,保证了Object类在程序中的各种类加载器中都是同一个类

比如Tomcat服务器就是的类加载就是双亲委派模型

tomcat服务器的类加载架构
tomcat服务器的类加载架构

WebApp和Jsp加载器通常会对应多个实例,每个一个WebApp对应一个加载器,每个一个Jsp文件对应一个Jsp类加载器
以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

OSGI

加载器的关系不再是双亲委派的树形结构,而是复杂的运行时才能确定的网状结构。

验证

验证的目的是为了确保 Class 文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:

  • 文件格式的验证
  • 元数据的验证
  • 字节码验证
  • 符号引用验证

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如 0、0L、null、false 等),而不是被在 Java 代码中被显式地赋予的值。
  • final修饰的类变量会设置成所需要的值

解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。

1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。

初始化

初始化阶段是根据程序员制定的计划执行的,也就是初始化阶段是执行类构造器<clinit>()方法的过程

何时初始化

顺序

静态代码块(静态变量和静态代码块) -> main方法 -> 构造代码块 -> 构造方法
都是先父后子的顺序

  • 构造块:直接在类中定义且没有加static关键字的代码块称为{}构造代码块。构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类构造函数。
  • 普通代码块:在方法或语句中出现的{}就称为普通代码块。普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定–“先出现先执行”
  • 静态代码块:在java中使用static关键字声明的代码块。静态块用于初始化类,为类的属性初始化。每个静态代码块只会执行一次。由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。如果类中包含多个静态代码块,那么将按照”先定义的代码先执行,后定义的代码后执行”。注意:1 静态代码块不能存在于任何方法体内。2 静态代码块不能直接访问静态实例变量和实例方法,需要通过类的实例对象来访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class B extends Object
{
{
System.out.println("构造块 B");
}
static
{
System.out.println("静态块 B");
}
public B()
{
System.out.println("构造方法 B");
}
public static void main(String[] args) {
System.out.println("main B");
}
}
class A extends B
{
{
System.out.println("构造块 A");
}
static
{
System.out.println("静态块 A");
}
public A()
{
System.out.println("构造方法 A");
}
public static void main(String[] args) {
System.out.println("main A");
}
}
class Testclass
{
public static void main(String[] args)
{
new A();
}
}

结果:
静态块 B
静态块 A
构造块 B
构造方法 B
构造块 A
构造方法 A

http://www.cnblogs.com/sophine/p/3531282.html
http://blog.csdn.net/owenchan1987/article/details/52879774

条件

  • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。
    生成这四条指令最常见的Java代码场景是:
    • 使用new关键字实例化对象时
    • 读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)
    • 调用一个类的静态方法时
  • 使用Java.lang.refect包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。

这几种场景被称为是主动引用,,而其余引用成为被动引用,被动引用是不会出发初始化的

例子

  • 子类引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Father{
public static int m = 33;
static{
System.out.println("父类被初始化");
}
}
class Child extends Father{
static{
System.out.println("子类被初始化");
}
}
public class StaticTest{
public static void main(String[] args){
System.out.println(Child.m);
}
}

结果输出

1
2
父类被初始化
33

对于静态字段,只有直接定义这个字段的类才会被初始化。
因此对于子类引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

  • 通过数组来定义引用类,不会触发此类的初始化
1
2
3
4
5
6
7
8
9
10
11
class MyArray{
static{
System.out.println("初始化MyArray类");
}
}
public class ArrayTest{
public static void main(String[] args){
MyArray[] myArray = new MyArray[5];
}
}

结果没有输出初始化MyArray类
通过数组来定义引用类,不会触发此类的初始化

  • 常量的引用不会触发定义常量的类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
class ConstClass{
public static final String NAME = "我是常量";
static{
System.out.println("初始化ConstClass类");
}
}
public class Test{
public static void main(String[] args){
System.out.println(ConstClass.NAME);
}
}

结果并没有输出初始化ConstClass类
常量在编译阶段会存入常量池中,本质上没有直接引用到定义常量的类,因此常量的引用不会触发定义常量的类的初始化

类构造器<clinit>()

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的 Java 程序代码。
在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是由编译器自动收集所有类变量的赋值动作和静态语句块中的语句合并产生的,收集的顺序是语句在源文件出现的顺序
    即在静态语句块只能访问在块前的变量,而块后的变量只能复制不能访问

    1
    2
    3
    4
    5
    6
    7
    public class Test(){
    static{
    i = 0; // 编译通过
    System.out.println(i); //编译出错,非法向前引用
    }
    static int i = 1;
    }
  • <clinit>()与类的构造函数不同,他不需要显示调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此虚拟机中第一个被执行的<clinit>()方法一定是java.lang.Object

  • 由于父类的<clinit>()会先执行,因此父类的静态语句块会比子类先执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    static class Parent{
    public static int A = 1;
    static{
    A = 2;
    }
    }
    static class Sub extends Parent{
    public static int B = A;
    }
    public static void main(String[] args){
    System.out.println(Sub.B);
    }

结果是2

  • <clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法

  • 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<clinit>()方法。但是接口与类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

jvm启动后创建的线程

  • Attach Listener
  • Finalizer
  • Signal Dispatcher
  • Reference Handler
  • main 程序所在的线程

Attach Listener :线程是负责接收到外部的命令,而对该命令进行执行的并且吧结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。
signal dispather: 前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部jvm命令时,进行初始化工作。
Finalizer: 用来执行所有用户Finalizer 方法的线程
Reference Handler :它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
http://supben.iteye.com/blog/2267828

分派

调用

方法调用不等同于方法执行,方法调用只是用来确定执行方法的哪个版本,不涉及到内部运行。
一切方法调用在class文件中存储的只是符号引用。
在类的解析阶段,会将一部分符号引用转化为直接引用,前提是运行之前就可以知道调用的版本,并且版本在运行期间不可变。
符合这样的方法主要有:静态方法、私有方法、实例构造器、父方法。他们在类加载的过程中会将符号引用转化为直接引用。
其中实例方法构造是<init>方法,
对JVM来说所有实例初始化动作都要收集到“特殊的实例初始化方法”(名为<init>,内容对应所有实例初始化器+构造器)里,所以上面的代码从JVM的角度看会是这样:
这个合成的<init>()V里,先是构造器里隐式或显式的super()调用,然后是按代码顺序把实例初始化动作(包括实例字段初始化与匿名的实例初始化器)收集起来,然后是构造器自身的内容。
https://www.zhihu.com/question/36643366/answer/68519999

方法分派

分派指的是在Java中对方法的调用。
Java中有三大特性:封装、继承和多态。
分派是多态性的体现,Java虚拟机底层提供了我们开发中“重写”和“重载”的底层实现。其中重载属于静态分派,而重写则是动态分派的过程。
除了使用分派的方式对方法进行调用之外,还可以使用解析调用,解析调用是在编译期间就已经确定了,在类装载的解析阶段就会把符号引用转化为直接引用,不会延迟到运行期间再去完成。
而分派调用则可能是静态的也可能是动态的,根据分派依据的宗量数(方法的调用者和方法的参数统称为方法的宗量)又可分为单分派和多分派。两类分派方式两两组合便构成了静态单分派、静态多分派、动态单分派、动态多分派四种分派情况。

Java语言(JDK1.6)是一门静态(编译)多分派(重载)、动态(运行)单分派(重写) 的语言

静态分派

  • 静态分派的最典型应用就是多态性中的方法重载Overload
  • 静态分派发生在编译阶段,因此确定静态分配的动作实际上不是由虚拟机来执行的

因此,在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Human{
}
class Man extends Human{
}
class Woman extends Human{
}
public class StaticPai{
public void say(Human hum){
System.out.println("I am human");
}
public void say(Man hum){
System.out.println("I am man");
}
public void say(Woman hum){
System.out.println("I am woman");
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticPai sp = new StaticPai();
sp.say(man);
sp.say(woman);
}
}

上面代码的执行结果如下

1
2
I am human
I am human

动态分派

  • 动态分派的一个最直接的例子是重写Override
  • 运行期根据实际类型确定方法执行版本的分派过程称为动态分派
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Eat{
}
class Drink{
}
class Father{
public void doSomething(Eat arg){
System.out.println("爸爸在吃饭");
}
public void doSomething(Drink arg){
System.out.println("爸爸在喝水");
}
}
class Child extends Father{
public void doSomething(Eat arg){
System.out.println("儿子在吃饭");
}
public void doSomething(Drink arg){
System.out.println("儿子在喝水");
}
}
public class SingleDoublePai{
public static void main(String[] args){
Father father = new Father();
Father child = new Child();
father.doSomething(new Eat());
child.doSomething(new Drink());
}
}

运行结果应该很容易预测到,如下:

1
2
爸爸在吃饭
儿子在喝水

我们首先来看编译阶段编译器的选择过程,即静态分派过程。这时候选择目标方法的依据有两点:一是方法的接受者(即调用者)的静态类型是Father还是Child,二是方法参数类型是Eat还是Drink。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。
再来看运行阶段虚拟机的选择,即动态分派过程。由于编译期已经了确定了目标方法的参数类型(编译期根据参数的静态类型进行静态分派),因此唯一可以影响到虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Child。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

字节码执行相关

栈帧是虚拟机进行方法调用和方法执行的数据结构,是虚拟机的栈元素。
每一个栈帧都包含了局部变量表、操作数栈、动态连接、方法返回地址和一些额外信息。

对于每一个线程来说都有自己的程序计数器和栈
线程中的每个方法对应着栈中的一个栈桢
一个栈有多个栈桢,每个栈帧里有局部变量表,操作数栈等

局部变量表

用于存放方法参数和方法内部定义的局部变量表。
局部变量表以slot为单位,一个slot可以存放32位的数据,而long、double需要2个slot
但是由于局部变量表是建立在栈中的,是线程私有的,是原子操作,因此不存在数据安全问题

类变量是有2次赋值机会的,一次是准备阶段,为系统值;另一次是初始化阶段,为程序员定义的值
而局部变量就不一样了,如果一个局部变量定义了,而没有赋值,是不能使用的,不能编译通过的。

局部变量:在栈中,每个方法一个,无默认值,必须赋值
成员变量:在堆中,每个实例一个,在init<>中初始化,有默认值
类变量:在方法区,每个类一个,在类中用static修饰,有默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Test {
//类变量
static int i;
//成员变量
int a;
public void fun() {
//局部变量
int x;
// System.out.println(x);
System.out.println("成员变量:a="+a);
}
public static void main(String[] args) {
//局部变量
int ii;
//System.out.println("ii="+ii);
// System.out.println(a);
System.out.println("类变量:i=" + i);
Test t = new Test();
t.fun();
System.out.println("成员变量:"+t.a);
}
}

http://blog.csdn.net/du_minchao/article/details/48881637

操作数栈

为什么局部变量是线程安全的?

每一个线程都有自己的stack,线程中的方法对应着该栈的栈帧,而该方法所以定义的局部变量是保存在该栈帧中的。
栈帧中包含着局部变量表和操作数栈等,这些都是私有的,当当方法执行结束,栈帧就会被销毁pop。
如果这些局部变量是基本类型,那么一定是线程安全的。
如果是对象,该对象存在堆里,那么就不一定了。
http://stackoverflow.com/questions/12825847/why-are-local-variables-thread-safe-in-java
http://stackoverflow.com/questions/4251282/java-threads-variables-local-to-the-thread?rq=1

编译器相关

前端编译器:将java代码转化为字节码的编译器。
主要工作有:词法分析、语法分析、注解器、语义分析(检查、解语法糖)和字节码生成

后端运行期编译器JIT:将字节码转化为机器码的过程

JVM即时编译器JIT

即时编译器(Just In Time Compiler) 简称JIT
JAVA程序最初是通过解释器(Interpreter)进行解释执行的,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。
为了提高热点代码的执行效率,就会将这些“热点代码”编译成与本地机器相关的机器码,进行各个层次的优化。 完成这个任务的编译器就是即时编译器(JIT)。

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

JVM的两款即时编译器JIT

JVM中默认内置了两款即时编译器,称为Client Compiler和Server Compiler。
可以用指定参数的方式,指定采用Client模式和Server模式。默认是mixed模式。
   java –Xint 解析 java –Xcomp 编译
解析器与编译器并存:
1、当程序需要迅速启动和执行的时候,解析器首先发挥作用,省去编译的时间,立即执行。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
2、当机器内存限制比较大,可以用解析方式节约内存,反之可以用编译提升效率。
3、解析器还可以作为编译器的“逃生门”。当例如加载了新类后类型结构发生变化,可以采用逆优化,退回到解析状态继续执行。

优化技术

程序员都有一个共识:以编译方式执行本地代码比解释方式更快
即时编译器产生的本地代码会比javac产生的字节码更优秀
虚拟机团队在即时编译上做了很多优化

  • 公共子式表达式消除:如果一个表达式已经计算过,未发生变化,那么就不需要重复计算,成为公共子式
  • 数组范围检查消除
  • 方法内联:将目标方法的代码复制到发起调用的方法之中,避免真实的调用发生。
  • 逃逸分析:
    逃逸分析的基本行为即使分析对象动态作用域,如果它能被其他方法所引用,则称为方法逃逸;如果被赋值给其他线程的实例变量等,则称为线程逃逸。如果能证明一个对象不会逃逸到方法或者线程之外,就会做一些优化,比如:栈上分配(减小gc)、同步消除(锁消除)、标量替换(不创建对象而创建基本类型标量)等。

Client Compiler和Server Compiler

Client Compiler和Server Compiler会实现分层编译(JDK 1.7默认有)。
第0层 程序解析执行,解析器不开启性能监控,可触发第一层编译。
第1层 编译成本地相关代码,进行简单优化
第2层 除编译成本地相关代码外,还进行成编译耗时较长的优化。
Client Compiler获得更高的编译速度 Server Compiler获得更好的编译质量,无须承担性能监控的任务