虚拟机类加载机制
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载这七个阶段。其中验证、准备、解析三个部分统称为连接。
加载、验证、准备、初始化和卸载这五个阶段对的顺序是确定的,类型的加载过程必须按照这种顺序来进行加载。
类的生命周期:
一、类加载过程:
1、加载
在硬盘中的文件(如class文件)通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各个数据的访问入口
2、验证
为了确保Class文件的字节流中包含的信息是符合《Java虚拟机规范》的全部约束条件的,而且还得保证这些信息被正常运行后不会危害虚拟机自身的安全。
1)文件格式验证:验证字节流是否符合Class文件格式的规范,并能被当前版本的虚拟机进行处理。
2)元数据验证:对字节码描述的信息进行语义分析,以保证相关信息符合《Java虚拟机规范》的要求。如该类是否有父类,该类的父类是否允许被继承等
3)字节码验证:最复杂的一个阶段,是通过数据流分析和控制流分析,确定程序语义是否合法,是否符合逻辑。
4)符号引用验证:确保解析行为能够正常执行。该阶段是在虚拟机将符号引用转化为直接引用的时候进行进行校验的(解析时)。判断该类是否缺少或禁止访问他依赖的某些外部类,方法,字段等资源。
3、准备
正式为类中定义的变量分配内存并设置类变量初始值的阶段。
public static int a = 1;
在准备阶段过后,此时的a值还是为0,而不是1,因为这个时候还没有开始执行任何的Java方法,当类初始化阶段后a的值才是为1。
基本数据类型在准备阶段后,基本都是相应的零值。
public static final int b = 2;
在准备阶段过后,b的值则为2。为什么呢?
因为编译时javac将会为b生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将b赋值为2。
4、解析
是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能够在使用时通过该符号准确的定位到需要的目标即可。
直接引用:是可以直接指向目标的指针、相对偏移量、能够间接定位到目标的句柄。(句柄:Java堆中划分一块区域内存来作为句柄池,reference中存储的就是这个对象的句柄地址,而句柄中包含了对象的实例数据与类型数据的具体地址信息。)
1)类或接口解析:当程序执行到某一个从未解析过的地方时,需要把一个从未解析过的符号引用解析为一个类或者接口的直接引用。
2)字段解析:解析一个未被解析过的字段符号引用,也就是字段所属的类或接口的符号引用。
3)方法解析:解析一个未被解析过的方法所属的类或接口的符号引用。
4)接口方法解析:解析接口方法所属的类或接口的符号引用。
5、初始化
类加载过程的最后一个步骤,虚拟机真正开始执行类中编写的程序代码。
在准备阶段,我们的变量其实已经赋值过一次了,只不过还是初始的零值,在初始化阶段,则会根据我们代码中定义的变量来进行赋值。
初始化阶段也就是执行类构造器<clinit>()方法的过程。
二、类加载器
1、类与类加载器
对于一个类来说,都必须由他的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即便是这两个类时同一个Class文件,被同一个虚拟机加载,只要加载他的类加载器不同,那么这两个类必定不同。
代码示例:
/**
* 不同的类加载器对instanceof关键字运算所带来的影响
* @Author chenxuan
*/
public class ClassLoadTest {
/**
* 我们使用了类加载器加载了该类,结果在进行instanceof运算时返回的时false
* 是因为虚拟机中同时存中了两个ClassLoadTest类,一个是由虚拟机的应用程序类加载的,另一个是由我们自定义的类加载器所加载的。
* 虽然他们是来自同一个class文件,但是在虚拟机中却是两个相互独立的类。
* @Author chenxuan
**/
public static void main(String[] args) {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream input = getClass().getResourceAsStream(fileName);
if(input == null){
return super.loadClass(name);
}
try {
byte[] bytes = new byte[input.available()];
input.read(bytes);
return defineClass(name,bytes,0,bytes.length);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.loadClass(name);
}
};
try {
Class clazz = classLoader.loadClass("com.example.demo.jvm.classload.ClassLoadTest");
Object o = clazz.newInstance();
//用的是自己的类加载器
System.out.println(clazz.getClassLoader().getClass().getName());
System.out.println(o.getClass());
System.out.println(o instanceof com.example.demo.jvm.classload.ClassLoadTest);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
com.example.demo.jvm.classload.ClassLoadTest$1
class com.example.demo.jvm.classload.ClassLoadTest
false
2、双亲委派模型
从Java虚拟机的角度来看,由两种不同的类加载器,一种是启动类加载器,这个类加载器是通过C++来实现的;另一种就是其他的类加载器,这些类加载器都是通过Java来实现的,独立存中与虚拟机外,全部都是继承自抽象类java.lang.ClassLoader
1)启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、tools.jar等
2)扩展类加载器:在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
3)应用程序类加载器:在类sun.misc.Launcher$AppClassLoader来实现的。负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
4)自定义类加载器:负责加载用户自定义路径下的类包
代码示例:
/**
* 类加载器示例
* @Author chenxuan
*/
public class ClassLoadF {
public static void main(String[] args) {
//启动类加载器,因为是C++来实现的,所以是null
System.out.println(String.class.getClassLoader());
//扩展类加载器
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
//应用程序类加载器
System.out.println(ClassLoadF.class.getClassLoader().getClass().getName());
//应用程序类加载器
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}
输出结果 :
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader
双亲委派模型工作过程:如果一个类加载器收到了类加载的请求,他不会立马去加载这个类,而是把这个加载的请求委派给父类加载器去完成,然后每一层往上请求,因此所有的加载请求最终都会到最顶层的启动类加载器中,只有当父加载器无法对该类进行加载时,才会告知子加载器,使子加载器自己去完成类的加载。
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
3、破坏双亲委派模型
既然我们定义了双亲委派模型,那么我们为什么还要去打破它呢?
以Tomcat类加载为例,Tomcat是个web容器,一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
这样看来,我们tomacat再使用双亲委派类模型加载的话是不可取的。
因为使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
tomcat的几个主要类加载器:
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
如何去打破它呢?
代码简单示例:
/**
* 打破双亲委派模型示例
* @Auther chenxuan
*/
public class ChenClassLoaderTest extends ClassLoader {
private String classPath;
public ChenClassLoaderTest(String classPath){
this.classPath= classPath;
}
private byte[] loadByte(String name) throws Exception{
name = name.replaceAll("\\.","/");
FileInputStream input = new FileInputStream(classPath+"/"+name+".class");
int len = input.available();
byte[] date = new byte[len];
input.read(date);
input.close();
return date;
}
/**
* 重写loadClass,用来打破双亲委派模型
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//先校验该类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
//当传入的类路径不符合条件时,则还是使用双亲委派模型规则
if (!name.startsWith("com.xuan.springboot")) {
c = this.getParent().loadClass(name);
}else{
long t1 = System.nanoTime();
//找到class文件并加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
try {
byte[] date = loadByte(name);
return defineClass(name,date,0,date.length);
}catch (Exception e){
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
//此时的User类是通过应用程序类加载器所加载
System.out.println(User.class.getClassLoader().getClass().getName());
//编译后的class目录
ChenClassLoaderTest classLoader = new ChenClassLoaderTest("d:/myProject/demo/target/classes");
Class clazz = classLoader.loadClass("com.example.demo.jvm.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("myClassLoader",null);
method.invoke(obj);
//自定义的类加载器加载
System.out.println(clazz.getClassLoader().getClass().getName());
System.out.println(obj instanceof User);
}
}
结果输出:
sun.misc.Launcher$AppClassLoader
User 无参的构造函数
=====chenxuan ClassLoader method=====
com.example.demo.jvm.ChenClassLoaderTest
false
由此可见,当我们通过自定义的类加载器加载User.class时,并没有按照双亲委派模型规则,正常User是由应用程序类加载器所加载,而此时是通过我们的自定义类加载所加载,故而打破了双亲委派模型。
User类:
public class User { private String name; public User(){ System.out.println("User 无参的构造函数"); } public void myClassLoader(){ System.out.println("=====chenxuan ClassLoader method====="); } }
双亲委派模型部分源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//加锁,防止一个类同时被加载
synchronized (getClassLoadingLock(name)) {
//先校验该类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//若该类加载器还有父类加载器,则先调用父类加载中的loadClass,一直往上递归
//如当前的类加载器是AppClassLoader,那么此时的parent就是ExtClassLoader
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//一直递归到最上面没有类加载器时,那么就用启动类加载器来加载
//如果没找到或者没能加载,则返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
long t1 = System.nanoTime();
//如果父类的加载器没能够加载该类,则调用findClass来查找该类
//由不同的子类加载器自己实现
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 调用启动类加载器,可以看到调的是native本地方法,证明了上面我们说过的是由C++实现
* 如果没能找到相应的类,则返回null
*/
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// return null if not found
private native Class<?> findBootstrapClass(String name);
由源码可知,想要打破双亲委派模型,得重写loadClass方法,将不想调用父类加载器的地方重新自定义加载类。