本文对类加载系统做系统性介绍关于类加载过程在CSDN文章有介绍。
1.类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识;
2.把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
3.ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
4.如果调用构造器实例化对象,则该对象存放在堆区;
1.class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
2.class file 加载到JVM中,被称为DNA元数据模板。
3.在 .class文件 --> JVM --> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建 一个java.lang.Class对象,用来封装类在方法区内的数据结构。
注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
jvm支持两种类型的加载器,分别是引导类加载器和自定义加载器 。引导类加载器是由c/c++实现的,自定义加载器是由java实现的。 jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)。
注意:上图中的加载器划分为包含关系并非继承关系。
这个类加载器使用c/c++实现,嵌套再jvm内部 。它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、 resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。并不继承自 java.lang.ClassLoader,没有父加载器;
java语言编写,由sun.misc.Launcher$ExtClassLoader实现。从java.ext.dirs系统属性所指定的目录中加载类库, 或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动扩展类加载器加载。派生于 ClassLoader。父类加载器为启动类加载器。
java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现。该类加载是程序中默认的类加载器,一般来说, Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类 库;派生于 ClassLoader 。父类加载器为扩展类加载器。通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。
在日常的Java开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式。(文章后面会有自定义加载器的详细介绍)
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时 (即 ClassNotFoundException ),子加载器才会尝试自己去加载。
假设没有双亲委派模型,试想一个场景:
黑客自定义一个 java.lang.String 类,该 String 类具有系统的 String 类一样的功能,只是在某个函数稍作修改。比如 equals 函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到 JVM 中。此时,如果没有双亲委派模型,那么 JVM 就可能误以为黑客自定义的java.lang.String 类是系统的 String 类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的 java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。
或许你会想,我在自定义的类加载器里面强制加载自定义的 java.lang.String 类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在 JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
举个简单例子:
ClassLoader1 、 ClassLoader2 都加载 java.lang.String 类,对应Class1、Class2对象。那么 Class1 对象不属于 ClassLoad2 对象加载的 java.lang.String 类型。
双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了,我们无需去写。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
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;
}
}
从上面代码可以明显看出, loadClass(String, boolean) 函数即实现了双亲委派模型!整个大致过程如下:
1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用 parent.loadClass(name, false); ),或者是调用 bootstrap 类加载器来加载。
3. 如果父加载器及 bootstrap 类加载器都没有找到指定的类,那么调用当前类加载器的 findClass 方 法来完成类加载。换句话说,如果自定义类加载器,就必须重写 findClass 方法!
findClass默认实现:
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看出,抽象类 ClassLoader 的 findClass 函数默认是抛出异常的。而前面我们知道, loadClass 在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的 findeClass 函数,因此我们必须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象。
如果是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为 Class 对象呢?很简单, Java 提供了 defineClass 方法,通过这个方法,就可以把一个字节数组转为Class对象。
defineClass 主要的功能是:
将一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节数组。如,假设 class 文件是加密过的,则需要解密后作为形参传入 defineClass 函数。
defineClass 默认实现如下:
protected final Class> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {
return defineClass(name, b, off, len, null);
}
模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载 器,用于隔离web应用服务器上的不同应用程序。
除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。
比如还可以从数据库、网络、或其他终端上加载。
java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。
实现方式: 所有用户自定义类加载器都应该继承ClassLoader类 在自定义ClassLoader的子类是,我们通常有两种做法:
1) 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
2)重写findClass方法 (推荐)
public class MyClassLoader extends ClassLoader{
// 指定的要加载 class 文件的目录
private File classPathFile;
public MyClassLoader(String absolutePath) {
this.classPathFile = new File(absolutePath);
}
@Override
// 根据类名将指定类加载进 JVM
// 注:该 class 必修在指定的 absolutePath 下
protected Class> findClass(String name) throws ClassNotFoundException {
// 拼接全类名
String className = MyClassLoader.class.getPackage().getName() + "." + name;
if(classPathFile != null){
// 根据绝对路径,以及 class 文件名,拿到 class 文件
// 注意全类名的情况
File classFile = new File(classPathFile,name.replaceAll("\\.","/") + ".class");
// 如果 class 文件存在
if(classFile.exists()){
// 将 class 文件读入内存,暂存到一个字节数组中
FileInputStream in = null;
ByteArrayOutputStream out = null;
try{
in = new FileInputStream(classFile);
out = new ByteArrayOutputStream();
byte [] buff = new byte[1024];
int len;
while ((len = in.read(buff)) != -1){
out.write(buff,0,len);
}
// 构造类的 Class 对象!!!
// 注:defineClass() 是一个 native 方法
return defineClass(className, out.toByteArray(), 0, out.size());
}catch (Exception e){
e.printStackTrace();
}
}
}
return null;
}
}