首页 > 代码库 > JVM 基础知识
JVM 基础知识
- JVM 概念
- 类加载器子系统
- 装载
- 链接
- 初始化
- JVM 内存模型
- 方法区Method Area
- 堆区Heap Area
- 栈区Stack Area
- 程序计数器Program Counter Register
- 本地方法栈Native Method Stacks
- 执行引擎
- Java Native Interface JNI
- 本地方法库
JVM 概念
JVM(Java Virtual Mechine,Java虚拟机),是 Java 应用程序的运行时引擎。JVM 是 JRE(Java Runtime,Java运行环境)的一部分,它实际上就是去执行 Java 程序的 main 方法。
为什么说 Java 是一个平台无关的语言? Java 号称是 Write Once,Run Anywhere,能实现这个的核心,就是 JVM 。当我们通过编译器编译 .java
文件时,会生成一个同名的 .class
文件(包含字节码),当我们运行 Java 程序时,流程如图所示:
类加载器子系统
类加载器子系统主要负责以下三部分:
- 装载
- 链接
- 初始化
装载
类加载器读取 .class
文件,生成相应的二进制数据并将其保存在方法区域中。对于每个 .class
文件,JVM 在方法区域中存储以下信息:
- 已加载类及其直接父类的全名(含包名)
.class
文件是否与类或接口或 Enum 相关- 修饰符,变量和方法信息等
加载完 .class
文件后,JVM 会创建一个类型为 Class
的对象,以便在堆内存中表示此文件。这里需要注意的是,该 Class
对象的类型是在java.lang包中预定义的类。这个Class对象可以被程序员用于获取类名,类名,方法和变量信息等类级别信息。我们可以通过 Object#getClass
方法来获取该对象。这也是 Java 反射的基础,关于反射可参见这篇文章。
/**
* java获取Class对象的三种方式
*/
public static Class<?> getClassObj() {
// 根据类名获取Class对象
Class<?> clazz1 = People.class;
// 根据对象获取Class对象
People people = new People();
Class<?> clazz2 = people.getClass();
// 根据完整类名获取Class对象
try {
Class<?> clazz3 = Class.forName("com.yuyh.reflection.java.People");
} catch (ClassNotFoundException e) {
Log.e(TAG, e.toString());
}
Log.i(TAG, "clazz1 = " + clazz1);
return clazz1; // clazz2 clazz3
}
链接
链接过程负责对二进制字节码的格式进行: 校验、初始化装载类中的静态变量、解析类中调用的接口、类。在完成了校验后,JVM初始化类中的静态变量,并将其值赋为默认值。
- 校验: 确保
.class
文件的正确性,检查此文件是否正确格式化并由有效编译器生成。如果验证失败,将会抛出运行时异常:java.lang.VerifyError
; - 初始化: JVM为类变量分配内存并将内存初始化为默认值。例如
public static int CODE = 1;
实际上,是将CODE初始化为0; - 解析: 通过搜索方法区域来定位被引用的实体。对类中的所有属性、方法进行解析,以确保其需要调用的属性、方法存在,以及具备应的权限,若方法或属性不存在,将会抛出
NoSuchMethodError
、NoSuchFieldError
等异常。
初始化
在这个阶段,所有静态变量都会分配他们在代码和静态块(如果有)中定义的值,也就是赋值。执行顺序是在类中从上到下执行,在类层次中从父类到子类执行。 通常有三个类加载器:
- 引导类加载器: 每个 JVM 实现都必须有一个引导类加载器,用于加载加载 JAVA_HOME/jre/lib 目录中存在的核心 Java API 类。这个路径通常称为引导路径。该加载器通过 C,C ++ 等语言实现。
- 扩展类加载器:用于加载扩展目录 JAVA_HOME/jre/lib/ext(扩展路径)或 java.ext.dirs 系统属性指定的任何其他目录中存在的类。该加载器通过
sun.misc.Launcher$ExtClassLoader
类实现。 - 系统/应用程序类加载器:是扩展类加载器的子类,用来加载启动参数中指定的Classpath中的jar包以及目录。它也通过
sun.misc.Launcher$ExtClassLoader
类实现,对应的类名为AppClassLoader
。
举个栗子:
public class Test{
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(Test.class.getClassLoader());
}
}
输出:
null
sun.misc.Launcher$AppClassLoader@73d16e03
因为 String 类是通过引导类加载器加载的,而引导类加载器是通过 C/C++ 实现的,并非 Java 对象,所以为null;而我们自定义的类是通过应用程序类加载器加载的。
注:JVM遵循委托 - 层次原理来加载类。
1. 系统类加载器 委托加载请求到 扩展类加载器
2. 扩展类加载器 委托加载请求到 引导类加载器
3. 如果类在引导路径中找到,类被加载,否则请求再次传递到扩展类加载器,然后到系统类加载器。最后如果系统类加载器无法加载类,那么我们得到运行时异常java.lang.ClassNotFoundException。如图所示:
JVM 内存模型
方法区(Method Area)
在方法区域中,存储类名称、直接父类名称、方法和变量信息等所有类级信息,包括静态变量。每个 JVM 只有一个方法区,它是一个线程共享资源。
对于每一个加载的类,会在方法区中保存以下信息:
类型信息
类及其直接父类的全限定名(java.lang.Object没有父类)
类的类型
类的访问修饰符
实现的接口的全限定名的列表
字段与方法信息
常量池
除常量外的静态变量
类的Class及ClassLoader引用
字段信息
字段名
字段类型
字段的修饰符(public, private , protected, static, final, volatile, transient)
方法信息
方法名
方法返回类型
方法参数的数量和类型(按照顺序)
方法的修饰符(public, private, protected , static, final, synchronized, native, abstract)
堆区(Heap Area)
所有对象(包括数组)的信息存储在堆区域中。每个 JVM 也有一个堆区域,并且它也是一个线程共享资源。堆区可能会抛出 OutOfMemoryError 异常。
栈区(Stack Area)
对于每个线程,JVM 会创建一个存储在此处的运行时堆栈。每调用一个方法,就会生成一个栈帧(Stack Frame)用于存储方法的本地变量表、操作栈、方法出口等信息,当这个方法执行完后,就会弹出相应的栈帧。如果请求的栈的深度过大,虚拟机可能会抛出 StackOverflowError
异常,如果虚拟机的实现中允许虚拟机栈动态扩展,当内存不足以扩展栈的时候,会抛出 OutOfMemoryError
异常。在线程终止后,它的运行时栈将被 JVM 销毁。它不是共享资源。
程序计数器(Program Counter Register)
存储线程当前执行指令的地址。由于多线程间切换时要恢复每一个线程的当前执行位置,显然每个线程都会有独立的PC寄存器。
本地方法栈(Native Method Stacks)
对于每个线程,会创建单独的本地堆栈。它存储本地方法信息。与 Stack Area 类似。
执行引擎
执行引擎执行 .class(字节码)。它逐行读取字节代码,读取各类存储区中存在的数据和信息并执行指令。它可以分为三个部分:
- 解释: 它逐行解释字节码,然后执行。主要缺点是当一个方法被多次调用时,每次都需要解释。(第一代JVM)
- 即时编译(Just-In-Time Compiler,JIT): 用于提高解释器的效率。它编译整个字节码并将其更改为本机代码,因此每当解释器查看重复的方法调用时,JIT为该部分提供直接本地代码,因此不需要重新解释,从而提高效率。(第二代JVM)
- 垃圾收集器:它会销毁未引用的对象。关于垃圾收集,暂不叙述。
Java Native Interface (JNI)
它是与本地方法库交互并提供执行所需的本机库(C,C ++)的接口,使JVM 可以调用C / C ++库。
本地方法库
它是执行引擎所需的本机库(C,C ++)的集合。
JVM 基础知识