什么是java虚拟机?

虚拟机是一种抽象化的计算机,通过在实际上的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令体系。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需要生成在java虚拟机上运行的目标代码,就可以在多种平台上不加修改地运行。做到了一次编译到处运行。
在JVM启动之后,它实际上就是一个操作系统中的进程。

JVM的体系结构

1SuGSU.png

类装载器

什么是类加载机制?

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

类加载机制的意义是什么?

在java语言里,类型的加载、连接和初始化都是在运行时通过类加载机制完成的,这种机制虽然会增加性能的开销,但是也成就了java的灵活性。
例子:

  1. 用户可以编写一个面向接口的应用程序,可以等到运行时再指定实际的实现类。
  2. 用户可以实现自己的类加载器,让一个本地运行的程序可以通过网络等方式从其它地方加载二进制流作为程序代码的一部分。

一个类的生命周期

一个类从被加载到JVM到卸载出内存需要经历如下过程。

  • 加载(Loading)
    通过一个类的全限定名,来获取此类的二进制流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据区。在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。
  • 验证 (Verification)
    验证主要确保Class文件二进制流中所包含的信息是否符合虚拟机的要求,是否会危害虚拟机的运行。验证主要包括,文件格式校验,元数据验证,字节码验证等工作。
  • 准备 (Preparation)
    准备阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。这里所说的初始值通常来值数据的零值。
    public static int value=123;这还代码在准备阶段后,value的值为0,而不是123.
  • 解析 (Resolution)
    解析过程是将虚拟机常量池中的符号引用替换为直接引用的过程。
  • 符号引用:符号引用以一组符号来描述所引用的符号。
  • 直接引用:是可以直接指向目标的指针,相对偏移量或者是一个间接定位到目标的句柄。
  • 初始化 (Initialization)
  • 使用 (Using)
  • 卸载 (Unloading)
    其中验证,准备和解析可以统称为连接(Linking)。

需要立即对类进行初始化的5种情况:

  • 遇到new,getstatic,putstatic或invokestatic这四条指令的时候,如果类没有进行初始化,则需要进行初始化。简单来讲就是我们在java代码中,使用new关键字实例化对象的时候,读取或设置一个类的静态字段的时候(被final修饰,已经在编译期把结果放入常量池的静态字段除外),已经调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,那么需要先触发其初始化。
  • 如果初始化一个类的时候,发现它的父类还没有进行过初始化的时候,则需要触发器父类进行初始化。
  • 当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果java.lang.invoke.MethodHeanle实例最后的结果为REF_getStatic,REF_putStaic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上五种情况为对类的主动引用。还存在对类的被动引用:

  • 通过子类去引用父类的静态字段,不会导致子类初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class ClassLoaderStudy {
public static void main(String[] args) {
//通过子类去引用父类的静态字段
System.out.println(SubClass.val);
}
}
class SuperClass{
static {
System.out.println("superClass init");
}
public static int val=123;
}


class SubClass extends SuperClass{
static {
System.out.println("subclass init");
}
}

输出:

1
2
superClass init
123
  • 通过数组定义来引用类,不会触发此类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public class ClassLoaderStudy {
public static void main(String[] args) {
//通过数组引用的类不会对类进行初始化。
SuperClass[] arr=new SuperClass[10];
}
}


class SuperClass{
static {
System.out.println("superClass init");
}
public static int val=123;
}


class SubClass extends SuperClass{
static {
System.out.println("subclass init");
}
}

没有任何的输出,说明没有对SuperClass类进行初始化。

  • 常量在编译时会存入调用类的常量池中,因此不会触发定义常量的类的初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ClassLoaderStudy {
public static void main(String[] args) {
//通过数组引用的类不会对类进行初始化。
System.out.println(SuperClass.val);
}
}


class SuperClass{
static {
System.out.println("superClass init");
}
public final static int val=123;
}


class SubClass extends SuperClass{
static {
System.out.println("subclass init");
}
}

没有任何的输出。
可见并没有初始化该常量的定义类。

类加载器

类加载器的工作就是:“通过一个类的全限定名来获取此类的二进制字节流”
比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下从才有意义

一共有四种类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++编写的,这个类主要负责将存放在/lib目录中,或者-Xbootclasspath参数所指定的路径中的。并且是由虚拟机识别的类库加载到虚拟机内存中。
  • 拓展类加载器(Extension ClassLoader):这个类加载器负责加载/lib/ext目录中的,或者被jave.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用类加载器。
  • 应用类加载器(application Classloader),这个类加载器负责加载用户路径(ClassPath)上所指定的类库。
  • 自定义类加载器,我们可以继承ClassLoader实现自己的类加载器。

双亲委派机制

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此只有当父加载器反馈无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派机制使得类随着它的加载器具备了一种带有优先级的层次关系。实现了沙箱安全。

执行引擎

什么是JVM执行引擎

java编写的代码(.java文件)Java编译器(java compiler)编译为Java字节码(.class)文件。类加载器负责转载编译后的字节码,并加载到运行时数据区中,然后执行引擎执行这些字节码。
执行引擎按照字节码指令来执行相应的任务。因为字节码计算机是无法直接运行的,所以需要将其转化为能够被计算机执行的及其语言。这个过程JVM执行引擎有两种方案。一种是解释执行:即一条条读取,解释并执行字节码指令。这种方案加载快,但执行较慢。另一种是即时编译(Just-In-Time):在合适的时候,即时编译器会将整段的字节码编译为本地代码(编译后的本地代码缓存在cache中),然后自己执行本地代码。编译后的代码,比解释执行更加的快。但编译的过程比较耗时。

因此内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。

JVM规范并没有定义执行引擎到底要如何区执行。因此JVM的提供者通过使用不同的技术以及不同的类型的JIT编译器来提高执行引擎的效率,

大部分的JIT编译器会将字节码转化为一个中间层表达式,然后进行优化,最后再把这种表示转化为本地代码。

Oracle Hotspot VM使用了一种叫做热点编译器的JIT编译器。它会通过分析代码的调用频数,寻找代码热点,然后会把热点代码编译为本地代码。如果代码不再是热点了,那么就会将本地代码从cache中移除,继续使用解释执行的方案。

运行时数据区

JVM在执行java程序的时候会它管理的内存分为若干个不同的数据区域,这些区域有着不同的用途。
运行时数据区可以分为方法区,堆区,虚拟机栈,本地方法栈,程序计数器。

程序计数器

程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。通过修改程序计数器的值,可以实现跳转执行等功能。它是线程私有的。
如果线程正在执行一个java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,那么这个计数器则为null。

此块区域是唯一一个在JVM规范中没有规定的任何OutOfMemoryError情况的区域。

Java虚拟机栈

线程私有的,生命周期和线程相同,虚拟机栈描述的是Java方法执行的内存模型。我们知道函数的调用是通过栈来完成的,每个方法在执行的时候都会开辟一个栈帧,方法中的局部变量其实就是存放在栈帧中。
在JVM规范中,对此区域规定了两种异常情况:如果线程请求的栈深度超过了虚拟机允许的最大深度,那么就会抛出StackOverflowError异常;如果允许虚拟机栈可以动态拓展时,无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈所发挥的作用其实和虚拟机栈的作用是非常相似的。只不过本地方法栈描述的是native方法执行的内存模型。在虚拟即规范中没有对本地方法栈中方法使用的语言与数据结构做出强制规定,因此具体的虚拟机可以自由的实现本地方法栈。本地方法栈也会抛出StatkcOverflowError和OutOfMemoryError异常。它同样也是线程私有的。

方法区

方法区是线程共享的,它主要存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池也是方法区的一部分,它主要存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

堆是线程共享的。它是JVM中最大的一块内存区域,在虚拟机启动的时候就已经创建了。这块区域的目的就是存放对象实例和数组,几乎所有的对象实例都在这里分配。堆区是垃圾收集管理最主要的区域。