每一个Java 开发者都知道字节码是通过JRE(Java 运行时环境)执行的,JRE实现了JVM ,JVM可以解析执行字节码文件。
什么是JVM?
虚拟机是一个软件,它是物理机的实现。Java 开发是建立在一次编译到处运行(Write Once Run Anywhere)的理念上的。Java编译器首先编译Java代码为.class文件。Class文件加载进入JVM,进行解析执行。
JVM体系结构图:
JVM 如何工作(本文主要针对主流HotSpot虚拟机)?如上图所示 JVM 主要分割成为了3个主要的系统:
- 类加载系统
- 运行时数据区
- 执行引擎
1.类加载系统( Class Loader System)
类加载器把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。与那些在编译时期需要进行连接工作的语言不同,Java语言中类的加载在程序运行时完成的。以下为类加载的主要过程:
1.1 加载,类加载器主要有以下三种:
启动类加载器: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)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
当加载class 文件时,以上类加载器会遵循双亲委派模型.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载.参见classLoader加载类的loadClass方法
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 首先委托给父类去加载 c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // z自行加载 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
1.2 连接
验证 - 验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
准备 - 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
解析 - 解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程;
1.3 初始化
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源。
2.运行时数据区,运行时数据区主要分为下面5个区域:
- 方法区(Method Area) – 方法区是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即编译器编译后的代码等数据.这个区域会抛出 OutOfMemory异常;运行时常量池是方法区的一部分。Class 文件除了类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用。
- 堆(Heap Area) – 堆也是各个线程共享得到内存区域,非线程安全。在虚拟机启动时建立,主要用于存储对象实例。由于现在的垃圾收集器都采用分代收集算法,堆可以分为新生代和老年代;
- 栈(Stack Area) – 虚拟机栈是线程私有的, 它的生命周期与线程相同。 每个方法执行时,虚拟机都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接以及方法出口等信息。局部变量表存放了编译期可知的各种基本数据类型和对象引用(reference类型,它是一个指向对象起始位置的引用指针,或者指向代表对象句柄的位置。这个区域会抛出 StackOverflowError和 OutOfMemory异常)
- 程序计数器(Program Counter Registers) – 当前线程所执行的字节码行号的行号指示器,每个线程私有。Java 虚拟机的多线程是通过线程轮流切换并分配处理器的执行时间的方式实现的。任何一个确定的时刻,一个处理器只会执行一个线程中的指令。为了线程切换后能恢复到正确的执行位置,每个线程都需要一个线程计数器记录虚拟机字节码指令地址。
- 本地方法区(Native Method stacks) – 本地方法区与栈类似,只不过本地方法区执行的是虚拟机Native方法。
3. 执行引擎
字节码加载进运行时数据区(Runtime Data Area )后,会被执行引擎执行。执行引擎主要分为以下几个组件:
- 解析器(Interpreter) – 解析器解析字节码很快,但是执行很慢。它的劣势是,当一个方法被调用多次时,每次都需要创建一个新的解析器。
- JIT 编译器– 当遇到相同的代码时, JIT 编译器会把相同的字节码为本地方法。
- 垃圾收集器(Garbage Collector)