Java代码要在JVM中正常运行,首先要先编译成.class文件,由类加载器将其加载到内存中。之后由类编译器来编译执行。
在加载的过程中需要注意什么呢?
1)什么时候需要加载一个类呢?
2)随便任何一个文件都是否可以被加载呢,它的加载过程是什么样的?
3)类加载器需要对内存中的内容做什么工作才可以被JVM直接使用?

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Class类型,这就是虚拟机的类加载机制。—《深入理解Java虚拟机》

1
2
类型的加载、链接和初始化过程都是在程序运行期间完成的。
Java可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

类的加载时机

Java虚拟机规范并没有规定什么时候加载类,但是规定了什么时候初始化一个类。

虚拟机规范严格规定了有且只有5中情况必须立即对类进行“初始化”。
1)遇到new、getstatic、putstatic、invokestatic;
2)初始化类的时候,对父类进行初始化;
3)JVM执行的时候,对主类进行初始化;
4)使用java.lang.reflect包的方法对类进行反射调用时;
5)使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后解析结果是REF_getstatic, REF_putstatic, REF_invokestatic时,对该类进行初始化;
6) jdk1.8中子类实现的接口中定义了default方法时;

类的加载过程

类的加载过程总共分为:Loading(加载)、Linking(连接)、Initializing(初始化)、Using(使用)、Unloading(卸载)。
其中 Linking阶段 分为 Verification(验证)、Preparation(准备)、Resolution(解析)。

ClassLoader

加载

加载阶段的成果是生成Class类对象,并将该对象存储在堆中。Class对象作为访问该对象数据的访问入口。
加载的步骤:
1) 根据类的全限定名读取类的二进制字节流;
2) 对二进制字节流进行解析转换,并将类信息存储在方法区中;
3) 堆中生成Class类对象,作为程序访问方法区中的类型数据的外部接口;

注意:
数组类不通过类加载器创建,由java虚拟机直接在内存中动态构造出来的。
数组的元素类型(去掉所有维度)要靠类加载器加载完成。
数组的组件类型(去掉一个维度)是引用类型,数组将被标识在加载该组建类型的类加载器的类名称空间上。
如果数组的组件类型不是引用类型,Java虚拟机会把数组标记为与引导类加载器关联。

验证

验证加载的类信息是否符合Java虚拟机规范,类的实现和继承、方法的执行、对象和方法的可访问性等等,主要分为四个阶段:

1)文件格式的验证
主要对Class文件中内容进行验证,是否符合Java虚拟机规范。
比如魔数、常量池的类型、指向常量的索引值是否存在或者类型是否正确等等;

这个阶段的验证是基于二进制字节流进行的,只有通过了验证才会被允许进入Java虚拟机的内存方法区存储。

2)元数据验证
对字节码描述信息进行语义分析,保证描述信息符合《Java语言规范》比如:

  • 这个类是否有父类、父类是否允许继承(被final修饰);
  • 这个类是否实现了父类所要求实现的所有方法;
  • 类中的字段、方法是否与父类产生矛盾;

3)字节码验证
对类的方法体进行验证,保证任何跳转指令都不会跳转到方法体以外的字节码指令上;
保证类型转化都是有效的.

4)符号引用验证

虚拟机在解析阶段将符号引用转化为直接引用的时候,对符号引用进行验证;

  • 符号引用中通过字符串描述的全限定名能否找到对应的类;
  • 在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段;
  • 符号引用中的类、字段、方法的可访问性是否可以被当前类访问;

符号引用阶段抛出的异常有:IllegalAccessError、NoSuchFieldError、NoSuchMethodError;

准备

准备阶段是正式为类中定义的变量分配内存并设置类变量初始值的阶段;

对于类变量(静态属性),设置的是“零值”,真正赋值是在初始化阶段;
对于常量(final static),会被设置为ConstantValue属性所指定的初始值。

对于常量,Javac时会为value生成ConstantValue属性。

解析

解析阶段是Java虚拟机将常量池的符号引用替换为直接引用的过程。《Java虚拟机规范》并没有规定何时进行解析。

  • 符号引用:可以无歧义的定位到目标即可;与虚拟机的内存布局无关;引用的目标不一定已经加载到虚拟机中;必须遵循《Java虚拟机规范》;
  • 直接引用:直接可以指向目标的指针、偏移量,或者可以间接定位到目标的句柄;与虚拟机的内存布局直接相关;直接引用的目标一定已经加载到虚拟机内存中;

解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符号引用进行。

类、接口解析:

虚拟机的完整解析过程:
1)如果不是一个数组类型,虚拟机将会把全限定名传给当前类的加载器去加载,在加载的过程中,由于元数据验证、字节码验证的需要,又可能出发其他相关类的加载动作。
2)如果是一个数组类型,并且数组的元素类型为对象,那么会按照第一点的规则加载数组元素类型。如果是基础数据类型包装类,那么由虚拟机生成一个代表该数组唯独和元素的数组对象;
3)前面两部解析OK后进行符号引用验证,确定当前类是否有加载类的访问权限。

如果一个类拥有另外一个类的访问权限,那么至少有一条是成立的:
1)被访问类是public的,并且与访问类处于同一个模块;
2)被访问类是public的,不与访问类处于同一个模块,但是被访问类的模块允许访问类访问;
3)被访问类不是public的,但是与访问类处于同一个包中

字段解析

对字段表内class_index项中索引的CONSTANT_Class_Info符号引用进行解析(即字段所属类)

1)先查找当前类是否包含了简单名称与描述符与目标相匹配
2)从下往上的顺序查找实现的接口类
3)从下往上的顺序查找父类
4)都无法查找到,抛出java.lang.NoSuchFieldError.

如果查找成果返回了引用,将会对这个字段进行权限验证,对于不具备权限的访问会抛出IllegalAccessError;

方法解析

解析出方法表class_index项中索引的方法所属的类或者接口的符号引用。
具体的解析过程与字段解析类似。

接口方法解析

与方法解析类似。

初始化

这个阶段Java虚拟机才真正开始执行类中编写的程序代码。程序在这个阶段初始化类变量和其他资源。

初始化阶段也是执行<clinit>()的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的;
编译器收集顺序是由语句在源文件中出现的顺序决定的;
静态语句块中智能访问到定义在静态语句块之前的变量;
定义在静态语句块后的变量,静态语句块可以赋值,但是不能访问;
Java虚拟机保证父类的<clinit>() 总是优先于子类的<clinit>()执行;

如果一个类中没有静态语句块,也没有对变量的赋值操作,编译器可以不为这个类生成clinit()方法。

类加载器

类加载器主要通过一个类的全限定名来获取描述该类的二进制字节流,它处于Java虚拟机的外部,方便让应用程序自己决定如何获取所需的类。

对于任意一个类,必须由加载它的类加载器与类本身来确定在Java虚拟机中的唯一性。
每个类加载器都拥有一个独立的类名称空间。

类加载器的双亲委派模型

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应有自己的父“类加载器”。
当类加载器需要加载一个类的时候,首先看该类是否已经被加载,如果没有被加载则尝试着让父“类加载器”去加载,如果没有父“类加载器”则让bootstrapClassLoader作为父“类加载器”,如果父“类加载器”加载失败,则使用自己的find_class方法去加载。

  • 启动类加载器(Bootstrap Class Loader)
    启动类加载器负责加载<JAVA_HOME>\lib 目录下的jar包(按名字识别,名字不符合的类库存放在lib目录下也不会被加载),也可以指定 -Xbootclasspath参数指定加载目录。启动类加载器无法被java程序直接引用。
  • 扩展类加载器(Extension Class Loader)
    扩展类加载器负责加载<JAVA_HOME>\lib\ext目录下的jar包,或者被java.ext.dirs指定的路径这种的所有类库。这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码实现的。
  • 应用程序类加载器(Application Class Loader)
    负责加载用户类路径上的所有类库。这个类加载器是在sun.misc.Launcher$AppClassLoader实现。
定制自己的类加载器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CustomClassLoader extends ClassLoader {

public CustomClassLoader(ClassLoader parent) {
super(parent);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

String path = "/classes/";
try {
InputStream ins = new FileInputStream(path + name.replace(".", File.separator) + ".class");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
byte[] classData = baos.toByteArray();
return defineClass(name, classData, 0, classData.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static void main(String[] args) throws Exception {
CustomClassLoader classLoader = new CustomClassLoader(null);
Class clazz = classLoader.loadClass("com.*.ClassLoader");
System.out.println(clazz.getClassLoader());
System.out.println(clazz.getClass());
}