5.Java虚拟机运行原理

JVM(Java 虚拟机)在运行Java程序的时候,有点类似于即时编译系统。每一个Java程序都是从main主函数开始运行的,JVM则负责将它从代码编译运行成为一个程序。同时,JVM是JRE(Java Runtime Environment)的一个组成部分。

Java程序最大的一个特性便是“一次编写,随处运行”,这意味着你可以将自己编写的Java代码无需经过任何调整,就可以在任何支持Java的平台上运行。而这样的机制则离不开JVM的特性。

当我们将一个.java的文件进行编译,编译程序会生成一个相同名字而后缀为.class的文件。当我们试图运行这个.class文件的时候,JVM需要经过一系列的步骤才能将它运行起来,这些步骤基本上包含了JVM的所有特性。

raw picture
在整个过程中,JVM主要负责三件事

  • 加载
  • 链接
  • 初始化

1.加载:

首先由类加载器(Class Loader)读取.class文件中的内容,然后生成相应的二进制数据并且将其保存在方法区(Method Area)之中。对于每一个.class文件来说,JVM将以下些信息进入方法区:

  • 被加载类的全名以及它的直接父类
  • 该.class类是否与其他的类、接口或者枚举相关联
  • 反射器、变量以及方法信息等

将.class文件加载完成后,JVM将会创建一个Class类型的类对象,然后将它放置进入堆内存(Heap)中。值得注意的是,这个对象跟我们在代码中的对象是没有关系的,它是由Java虚拟机自动生成的,它的类型定义在java.lang这个包中。我们在编写代码时可以使用getClass()方法来获取这个对象,然后通过这个类对象来获取类级别的信息,比如说类的名称、类的父类,方法和变量等等。例如:

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {

    // 主函数,这里是程序的入口
    public static void main(String[] args) {
        // 创建一个Student对象,它的名称是s1
        Student s1 = new Student();
        // 获取s1这个对象的类对象
        Class c1 = s1.getClass();
        // 将c1的名称打印出来
        System.out.println(c1.getName());
        // 获取c1这个类对象中所有的方法,存入数组中
        Method m[] = c1.getDeclaredMethods();
        for (Method method : m)
            System.out.println(method.getName());
        // 获取c1这个类对象中的所有成员变量,存入数组中
        Field f[] = c1.getDeclaredFields();
        for (Field field : f)
            System.out.println(field.getName());
    }
}

// Student类的定义
public class Student {
    private String name;
    private int roll_No;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getRoll_no() { return roll_No; }
    public void setRoll_no(int roll_no) { this.roll_No = roll_no; }
}

输出为:

Student
getName
setName
setRoll_no
getRoll_no
name
roll_No

提示:对于每一个类来说,不过你new出多少个对象,都只有一个Class类型的类对象。每一个由该类创建出来的对象都指向同一个类对象的引用。例如:

Student s1 = new Student();
Class c1 = s1.getClass();

Student s2 = new Student();
Class c2 = s2.getClass();

System.out.println(c1==c2); // 结果为true

2.链接:

这一步主要进行执行验证、执行准备、以及(可能的)解决问题

  • 验证:这一步主要是验证.class文件的正确性。比如说验证这个文件是否被编译器正确地编译了,因为编译器也可能会出错的。如果这一步失败,将会抛出一个java.lang.VerifyError类型的运行时(run-time)异常。
  • 执行准备: JVM为变量分配存储空间以及对变量进行初始化。
  • 解决问题: 这一步主要是将引用的变量转换为它的实际类型,这个实际类型是存储在方法区(Method Area)中的。

3.初始化

在这一阶段,所有静态成员变量都将会被赋值(直接赋值或者通过static代码块赋值)。在多个类中,这一步是父类到子类依次执行;在一个类中,这一步从上至下执行。
一般来说,总共会有3个类加载器:

  • 引导类加载器(Bootstrap class loader):正因为JVM具有引导类加载器,才能够正确地加载所要运行的类。它会将%JAVA_HOME%/jre/lib这个文件夹中的类先加载进来。这个文件夹也可以为引导文件夹,其中的内容是以C或者C++来实现的。
  • 拓展类加载器(Extension class loader):是引导类加载器的一个子类,它主要负责加载%JAVA_HOME%/jre/lib/ext这个文件夹中的内容。它由sun.misc.Launcher$ExtClassLoader这个类实现。
  • 系统/应用类加载器(System/Application class loader):是拓展类加载器的子类,负责加载程序员所编写的类。本质上它需要使用映射到java.class.path中的环境变量。它也是由sun.misc.Launcher$ExtClassLoader这个类实现。
public class Main {
    // 主函数,这里是程序的入口
    public static void main(String[] args) {
        // String类由引导类加载器来加载, 而引导类加载器不是Java对象,
        // 因此结果是null
        System.out.println(String.class.getClassLoader());

        // Main类由应用类加载器加载
        System.out.println(Main.class.getClassLoader());
    }
}

输出为:

null
sun.misc.Launcher$AppClassLoader@18b4aac2

提示:JVM的设计遵从的是层级代理原则。应用类加载器负责加载扩展类加载器所委托的内容,而拓展类加载器负责加载引导类加载器所委托的内容。如果某一个类在%JAVA_HOME%/jre/lib这个文件夹可以找到,那么就加载它,否则会再次传输到拓展类加载器委托它来加载,如果拓展类加载器找不到就委托应用类加载器来加载。如若连应用加载器都找不到的话,就会抛出一个java.lang.ClassNotFoundException的运行时异常。
raw picture

JVM内存区

raw picture

  • 方法区(Method area):与类有关的内容比如说类的名称、类的父类的名称、变量和方法等待都会存储在JVM内存的方法区中。JVM内存中只有一个方法区,所以里面的内容是可以共享的。
  • 堆内存(Heap area):关于对象的内容都会存储在堆内存中,JVM内存区也是只有一个堆内存,所以它也是一个共享区。
  • 栈内存(Stack area):对于每一个线程来说,JVM都会在内存区中开辟一个栈内存。每一个栈内存块中存储着方法的调用,该方法的所有的局部变量都存储在相应的帧中。当线程终止后,它所属的栈内存也会被JVM所销毁。由于每一个栈都是独立的,所以这里面的资源无法被共享。
  • 程序计数器寄存器:全称为Program Counter Registers,也叫PC Registers。里面存储着线程当前执行指令的地址,每一个线程都会有对于的程序计数器寄存器。
  • 本地方法栈内存区(Native method stacks):每一个线程都会独立创建一个本地方法堆栈,里面存储的是本地库(C、C++)方法程序信息。

执行引擎

执行引擎负责执行.class文件(也就是中间码“byte-code”文件)。它通过逐行扫描读取二进制数据,然后结合上面介绍的JVM内存区中的数据执行指令。执行引擎可以分成三个部分。

  • 解析器(Interpreter):逐行扫描二进制数据然后执行。解析器比较愣,如果有某一个方面被多次调用,它每次都会被重新解析。
  • 即时编译器(Just-In-Time Compiler):即时编译器也叫做JIT,解析器解析好的字节码更改为本机的机器代码。当解析器发现有方法被重复调用时,JIT直接为该段提供本地机器代码,因此不用再重新解析,从而提高了解析器的效率。
  • 垃圾回收器(Garbage Collector):负责销毁不再被引用的对象。

Java Native Interface (JNI) :

Java中native接口是执行引擎所需要的本地库(C、C++)的集合。

本文原作者:Gaurav Miglani
链接:https://www.geeksforgeeks.org/jvm-works-jvm-architecture/

posted @ 2018-11-15 20:32  stdio_0  阅读(1490)  评论(0编辑  收藏  举报