1.什么是类加载
类加载指的是将class文件读入内存,并为之创建一个java.lang.Class对象,即程序中使用任何类是,系统都会为之创建一个java.lang.Class对象,系统中所有的累都是java.lang.Class的实例。实际上类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构
1.1.类的加载由类加载器完成,jvm提供了三种类加载器下面的会讲述到,此外还可以通过继承ClassLoader基类来自定义类加载器。
1.2.通常可以用一下几种方式加载类的.class文件:
1.从本地文件系统加载class文件;
2从jar包中加载class文件,如JAR包的数据库驱动类;
3.通过网络加载class文件
4.把一个Java源文件动态编译并执行加载。
2.类的加载过程:
从类加载到虚拟机的内存开始,到卸载出内存为止,类的一整个生命周期包括类加载、验证、准备、解析、初始化、使用和卸载七个阶段,具体的过程如图2所示。类的加载过程包括加载、验证、准备、解析和初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化的顺序是确定,而解析则不一定,解析的可能发生在初始化阶段之后。这四个阶段是按顺序开始,并不意味着顺序执行或者顺序完成,有可能是交叉混合地进行。通常在一个阶段执行过程调用激活另外一个阶段。
2.1接下来一一解析类加载的过程:
2.1.1加载:加载指的是将类的class文件读入到内存中,并为之创建一个Java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之创建一个java.lang.Class对象。总结起来加载阶段,虚拟机需要完成三件事:
- 通过类的全限定类名来获取其定义的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为运行时方法区运行时的数据结构;
- 在堆中生成一个代表Class的对象,作为方法区中这些数据访问的入口。
这个阶段阶段的可控性最强,程序员可以使用系统提供的类加载器加载,也可以使用自定义的类加载器加载。
2.1.2验证:验证阶段的作用主要是保证类的正确性,即我们所加载的类不能违反虚拟机的规则对虚拟机造成危害。
- 文件格式的验证:验证.class字节文件是否符合java虚拟机class文件的规范,并且能被当前虚拟机版本所处理。这里面主要对魔数、主版本号、常量池等等的校验。
- 元数据的验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等
- 字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威胁虚拟机安全的事件。
- 符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成
2.1.3准备:准备阶段主要是为类的静态变量分配内存并设置默认初始值
- 类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中。
- 这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。
2.1.4解析:解析阶段主要的作用是将符号引用替换为直接引用。
- 符合引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
- 直接引用:是指向目标的指针,偏移量或者能够定位的句柄。和虚拟机实现的内存相关,不同的虚拟机直接引用一般不同。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
2.1.5初始化:初始化是类加载的最后一个阶段,初始化就是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似矛盾,准备阶段时根据类变量的数据类型赋予默认的初始值,比如private static int i=1,首先字节码文件被加载到内存中,再进行验证,通过验证后进入到准备阶段,在准备阶段jvm或根据类变量的数据类型给i赋予i=0,然后到了解析阶段,完成解析阶段后,到初始化这个步骤,这个时候才把真正的值初始值1赋值给i,此时的i=1.java为类变量赋予初始值有两种方式:
- 声明类变量时指定初始值
- 使用静态代码块为类静态变量赋序初始值。
jvm的初始化步骤:
- 假如这个类还没被加载和链接,首先对这个类进行加载和链接;
- 假如该类的直接父类还没有被初始化,则初始化其直接父类;
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
2.2类加载的时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
- 创建类的实例,即new一个对象时候;
- 访问某一个类或接口的静态变量,或者对该类静态变量赋值;
- 调用类的静态方法;
- 反射(比如Class.forName(“com.lyj.load”))
- 初始化一个类的子类(会首先初始化子类的父类)
- jvm启动时标明的启动类,即文件名和类名相同的那个类。
3.类加载器
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。
3.1Java提供了三种类加载器,当一个jvm启动时候,Java开始使用以下三种类加载器
- Bootstrap ClassLoader:根类加载器,是最顶级的类加载器,加载Java的核心类,是用c++语言实现的,并不是继承自java.lang.ClassLoader(我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等)
- Extension ClassLoader:拓展类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
- Application ClassLoader:系统类加载器,也称为SystemAppClass。 加载当前应用的classpath的所有类。
我们看到java为我们提供了三个类加载器,应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。这三种类加载器的加载顺序是什么呢?
三种加载器的执行顺序是:
Bootstrap ClassLoader > Extention ClassLoader > Applocation ClassLoader
3.2类加载的方式:认识了三种类加载器,再来看看类加载的三种方式:
- 通过命令行启动应用时由JVM初始化加载含有main()方法的主类;
- 通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块通过ClassLoader.loadClass()方法动态加载,不会执行初始化块;
3.3类的记载机制:JVM的类加载机制主要有三种:
- 全盘负责:所谓的全盘负责,就是当一个类加载器负责加载某个class时,该class所依赖和引用的其他class也将由该类加载器负责加载,除非显示使用另一个类的加载器来载入;
- 双亲委派:所谓的双亲委派就是,则是先让父类加载器试图加载该类的Class ,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的来说,就是某个特定的类加载器在接到加载器的请求后,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去完成;
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜索该Class,只有当缓存区不存在该类的Class对象时,系统才会读取该类的对应的字节码文件,并将其转换成Class对象,存入缓冲区。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效。
3.4双亲委派类加载机制:上面解释不同的类加载机制已经解释的了很清楚了,我们再来谈谈它的优点:
- 采用双亲委派机制的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过层级关系可以避免类的重复加载,当父类已经加载了该类时,就没有必要子类再loader一次。
- 第二点就是处于安全因素,Java核心API中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派机制传递到启动类加载器,而启动类加载器在核心Java API中发现这个名字的类。发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Integer,而是直接返回已加载过的Integer.class,这样子防止核心API库被随意篡改。
4.自定义类加载器
谈完Java给我们提供的三个类加载器,我们来谈谈自定义类加载器,那么如何自定义类加载器,主要有两种方法:
- 遵循双亲委派机制:继承ClassLoader,重写findClass()方法;
- 破坏双亲委派机制:继承ClassLoader,重写loadClass()方法;
通常情况我们建议使用第一种方法来实现自定义类加载器,最大程度的保留双亲委派机制,保留双亲委派机制的优点。
什么情况下需要自定义类加载器:
- 隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如,某些框架通过,自定义类加载器的确保应用中依赖的jar包不会影响到中间件运行时使用的jar包;
- 修改类加载方式:类的加载模型并非强制,除了bootstrap外,其他的加载并非一定要引用,或者根据实际情况在某个时间点进行按需动态加载;
- 扩展加载源:比如从数据库、网络,甚至是电视机机顶盒进行加载;
- 防止源码泄露:Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,对加密的字节码进行解密。
下面我们来时实现一个自定义类加载器:
1.首先写一个Beans.java类,我的电脑是Mac,将这个文件放在desktop,在terminal上 javac Beans.java编译成字节码文件Beans.class
public class Beans {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Beans( ) {
}
@Override
public String toString() {
return "Person [name=" + name + "]";
}
}
2.写一个MyClassLoader类继承ClassLoader抽象类,重写findClass方法:
public class MyClassLoader extends ClassLoader {
private String libPath;
public MyClassLoader(String path) {
libPath = path;
}
@Override protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(libPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0; try { while ((len = is.read()) != -1) { bos.write(len); }
}catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close(); bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
e.printStackTrace();
} return super.findClass(name); }
//获取要加载 的class文件名
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
3.写一个测试类LoaderTest.java:
public class LoaderTest {
public static void main(String[] args) {
MyClassLoader diskLoader = new MyClassLoader("/Users/luoluo/Desktop");
//加载class文件,注意是
try {
Class<?> c = diskLoader.loadClass("Beans");
Object c1Object=c.newInstance();
System.out.println(c1Object);
System.out.println(c1Object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行,看具体的实现: