当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到 JVM。
package com.shendu;
public class JvmTest01 {
public static final int initData = 666;
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
new JvmTest01().compute();
}
}
如上面的代码:当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到 JVM。
通过Java命令执行代码的大体流程如下:
以上就是整个从jvm到java中main程序的执行全部过程,main是所有程序的入口。
整个类加载如图所示
加载
加载,是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。
类加载阶段:
(1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。
(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。
(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。
验证
验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:
文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范要求
字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
准备
准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:pirvate static int size = 12;。那么在这个阶段,size的值为0,而不是12。但final修饰的类变量将会赋值成真实的值。
解析
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
初始化
初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
类加载器和双亲委派机制
上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那 些类 自定义加载器:负责加载用户自定义路径下的类包
双亲委派机制
一个类只有被第一次主动使用时,才会被java虚拟机加载
主动使用的情况(6种)
1、创建类的实例
2、访问类的静态变量
3、调用类的静态方法
4、反射加载
5、初始化一个类的子类
6、java虚拟机启动时被标记为启动类的类
定义了几个类加载器。
-
AppClassLoader 系统类加载器 负责的目录如下:
- %JAVA_HOME%/jre/lib
- -Xbootclasspath 参数指定的目录
- 系统属性sun.boot.class.path
-
ExtClassLoader 扩展类加载器 负责的目录如下:
- %JAVA_HOME%/jre/lib/ext
- 系统属性java.ext.dirs指定的类库
-
Bootstrap classLoader 启动类加载器 负责的目录如下:
- 环境变量 classpath
- -cp
- 系统属性java.class.path
各个加载器的加载路径的验证
AppClassLoader 加载路径地址
String appProperty = System.getProperty("java.class.path");
for (String s : appProperty.split(";")) {
System.out.println(s);
}
打印为:
C:\Program Files\Java\jdk1.8.0_281\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_281\jre\lib\rt.jar
D:\tcl_ouyang\demo_coding\jvmclassload\target\classes
D:\tcl_ouyang\dev_soft\IntelliJ IDEA 2019.1.4\lib\idea_rt.jar
虽然打印了这么多的路径,但是其他的jar已经被它的父加载器给加载过了,根据双亲委托机制,其实AppClassLoader只是加载项目中的classpath路径下的类。
ExtClassLoader 加载路径
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
打印
C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
Bootstrap classLoader加载路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
System.out.println(url.toExternalForm());
}
打印
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/classes
我们在来验证下,各个加载器之间的关系
System.out.println(JvmTest02.class.getClassLoader());
System.out.println(JvmTest02.class.getClassLoader().getParent());
System.out.println(JvmTest02.class.getClassLoader().getParent().getParent());
打印:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null
我们可以得出一个结论:AppClassLoader的父加载器是ExtClassLoader,然后我们的引导类加载器是不对外开放的,因为它是C++编写的,它不是一个类
JVM类加载器是有亲子层级结构的,如下图
这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的 类加载路径中查找并载入目标类。 比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载 器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天 没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的 类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器, 应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。。 双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载
为什么要设计双亲委派机制?
-
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改 避免类的重复加载:
-
当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性