一、前言
当我们完成了Java源代码编写并通过编译和打包流程生成了一个包含主类(其中含有main函数)的JAR文件后,执行java -jar XXX.jar命令时,JVM加载和执行操作以启动应用程序。在不关心JVM内部执行过程的前提下,那么这个过程主要流程是这个样子的:
上图展示了一个主要流程,其实这个主要流程就是jvm的类加载机制,所谓的类加载机制,就是将class文件加载到内存中,形成可以被JVM可以直接使用的java类型。由这个图我们衍生出来个问题:
- 加载这个class文件都产生了什么操作?
我们带着问题继续往下看。
二、类的加载过程:
类的加载过程,主要有以下几步,加载->验证->准备->解析->初始化。
加载:就是从我们的磁盘文件或者其他存储介质中(如数据库),读取.class文件,获取二进制字节流,二进制字节流转化为方法区的运行时数据结构,并在内存中生成一个class对象,作为访问方法区这个类的各种数据访问入口。 (备注:方法区主要存放类型信息,常量、静态变量等信息),下面用一段简单的demo来解释下这句话什么意思。
arduino
public class Demo {
private String address ;
private int age;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
//加载类Demo.class文件,并在内存中生成一个Class对象,clazz就是这个对象的引用,代表的是Demo类
Class<?> clazz = Class.forName("com.leetcode.jvm.Demo");
//通过clazz对象访问方法区中的属性、创建对象,间接访问方法区中的类数据
Field[] declaredFields = clazz.getDeclaredFields();
for (int i = 0; i < declaredFields.length; i++) {
System.out.println(declaredFields[i].getName());
}
Demo instance = (Demo) clazz.newInstance();
System.out.println(instance);
}
}
验证:验证这个class文件是否符合JVM的数据格式规范(包括文件格式等)
准备 :给类静态变量分配内存空间,并给类的静态变量赋零值(基本数据类型为零值,这个零值不一定代表的是0,基本数据类型的默认值和引用类型的默认值),但是如果是final修饰的静态常量,则会赋给定的值,然后放在常量池中。
解析 :将类的符号引用替换成直接引用。这个东西说的比较抽象,这里稍微解释下。符号引用是出现在编译阶段的,符号引用包括全限定类名、字段的名称、方法的名称等。比如在我这个类DemoClass中,有个静态方法fly(),那么在DemoClassB中如果引用了这个方法,那么在DemoClassB的字节码文件中,不会存储这个方法的内存地址,只是存储这个方法的引用,即全限定类名+方法描述(类似这种形式com.jvm.classload.DemoClass#fly())。直接引用是出现在解析阶段的,可以直接指向类、字段或者方法在内存中的物理位置的一种引用方式。类加载 器会根据DemoClassB这个符号引用(com.jvm.classload.DemoClass#fly(),类似这种)找到类DemoClass,并确定这个符号引用对应的内存物理地址。以后每当访问fly方法的时候,可以直接拿着这个内存地址进行访问,而不是拿着符号引用进行查找,这就是符号引用就被替换成了直接引用。
初始化 :执行静态代码块和给静态变量赋指定初始值。
以上就是类加载的主要过程的一个阐述,但是不过要注意的是,类加载过程,是一个懒加载的过程,只有这个类被真正用到的时候,才会被加载,比如访问类的静态方法,new一个对象的时候等,下面用一个简单的demo来演示下这个问题:
csharp
public class DemoClass {
static {
System.out.println("DemoClass的静态代码块执行了");
}
public DemoClass() {
System.out.println("DemoClass的构造方法");
}
}
csharp
public class DemoClassOther {
static {
System.out.println("DemoClassOther的静态代码块执行了");
}
public DemoClassOther() {
System.out.println("DemoClassOther的构造方法");
}
}
java
public class ClassLoadDeep {
public static String name = "lisi";
static {
System.out.println("主类静态代码块的执行");
}
public static void main(String[] args) throws Exception {
DemoClass demoClass = new DemoClass();
DemoClassOther other = null;
}
}
代码的运行结果如下:
三、类加载器
上面那个类的加载过程,是由什么去执行的呢?这个时候,我们就要引出一个比较重要的东西,类加载器。
在jvm中,除了引导类加载器、扩展类加载器、应用类加载器,还可以定义类加载器。引导类加载器是JVM自身实现中的一部分,由C++语言实现,扩展类加载器和应用类加载器是由java语言实现,java语言实现的类加载器有一个公共的父类ClassLoader,这个看一下类的关系图实现就清楚了。那么这几种类加载器的作用肯定是加载类的,但是他们的区别是什么,我们先看一下他们的定位或,或者说是加载的内容。
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar。
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下,ext扩展目录中的JAR 类包。
- 应用类加载器:看到应用这两个词,就是加载我们自己应用的类文件的,即我们打包过后classpath路径的类文件。
jvm实例创建的时候,会去实例化引导类加载器,然后这个引导类加载器去加载rt.jar中com.misc.Launcher这个类,在加载Launcher这个类的时候,会去创建扩展类加载器和应用类加载器的实例,流程大概是下面这个样子的:
我们从源码中细看一下:
从图中我们可以看到,Launcher在被加载的时候,由于是静态变量,即类变量,然后在类加载过程中的初始化阶段的时候,就已经赋初始值了,调用自己的构造函数,在构造函数中,实例化了扩展类加载器和应用类加载器 ,是不是清晰点了呢。
我们再去验证下,这三种类加载器加载的内容具体是什么,写一个简单的demo如下:
csharp
public class DemoClassLoader {
public static void main(String[] args) {
//引导类加载器加载的类文件 Integer位于rt包中
System.out.println(Integer.class.getClassLoader());
//扩展类加载器加载的类文件
System.out.println(com.sun.crypto.provider.AESKeyGenerator.class.getClassLoader().getClass().getName());
//应用类加载器加载的类文件-我们自己写的类
System.out.println(DemoClassLoader.class.getClassLoader().getClass().getName());
}
}
三、结束语
如上,如有不对的地方,还请各位大佬批评指正。毕竟还是一只成长中的小菜鸟。下一篇,浅谈JVM类加载机制-类加载器的双亲委派模型。