一,类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的Java.lang.Class对象,用来封装类在方法区类的对象。看下面2图
类的加载的最终产品是位于堆区中的Class对象 Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
加载类的方式有以下几种:
- 从本地系统直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件(服务器)
二,加载步骤
JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤,如下图所示:
1. 装载:查找并加载类的二进制数据;
2. 链接:
- 验证:确保被加载类的正确性
那为什么我要有验证这一步骤呢? 首先如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途,就不妙了,因此这个class文件要先过验证这一关,不符合的话不会让它继续执行的,也是为了安全考虑吧
-
准备:为类的静态变量分配内存,并将其初始化为默认值
-
解析:把类中的符号引用转换为直接引用
3. 初始化:为类的静态变量赋予正确的初始值
准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10
对于
1. 静态变量
2. 静态初始化块
3. 变量
4. 初始化块
5. 构造器
-
它们的初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)>构造器
-
静态变量和静态初始化块是依照他们在类中的定义顺序进行初始化的。同样,变量和初始化块也遵循这个规律。
-
并不是父类完全初始化完毕后才进行子类的初始化,实际上子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了
三,加载器
JVM的类加载是通过ClassLoader及其子类来完成的
类的层次关系和加载顺序可以由下图来描述:
验证ClassLoader加载类的原理:
测试1:打印ClassLoader类的层次结构,请看下面这段代码:
ClassLoader loader = ClassLoaderTest.class.getClassLoader(); //获得加载ClassLoaderTest.class这个类的类加载器
while(loader != null) {
System.out.println(loader);
loader = loader.getParent(); //获得父类加载器的引用
}
System.out.println(loader);
1. Bootstrap ClassLoader
根加载器,由C++实现,不是ClassLoader子类.是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
2. Extension ClassLoader
扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。
3.App ClassLoader/System ClassLoader
应用类加载器/系统类加载器,负责记载classpath中指定的jar包及目录中class
4.Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
5.加载原理
ClassLoader使用的是双亲委托模型来搜索类的
-
每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系)
-
虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器
-
当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,
这个过程是由上至下依次检查的,
首先由最顶层的类加载器Bootstrap ClassLoader试图加载,
如果没加载到,则把任务转交给Extension ClassLoader试图加载,
如果也没加载到,则转交给App ClassLoader 进行加载,
如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。
如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。
否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
6.全盘负责
“全盘负责”是指当一个ClassLoader装载一个类时,除非显示地使用另一个ClassLoader,则该类所依赖及引用的类也由这个CladdLoader载入。
7.双亲委托
双亲委托可以避免重复加载,
当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,
而双亲委托的方式,就可以避免这种情况,
因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,
所以用户自定义的ClassLoader永远也无法加载一个自己写的String,
除非你改变JDK中ClassLoader搜索类的默认算法。
8.JVM 判定类相同
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
只有两者同时满足的情况下,JVM才认为这两个class是相同的。
四,一个类的字节码仅被加载一次
- ExtClassLoader如何保证字节码仅被加载一次
逻辑上来说ExtClassLoader的父加载器是Bootstrap,
具体到代码,ExtClassLoader继承了URLClassLoader,URLClassLoader继承了类SecureClassLoader,最终一直到抽象类ClassLoader。
为了保证“一个类的字节码仅被加载一次”这个目标。ExtClassLoader要做的有两件事:
a.保证“ private ClassLoader parent;”正确。(对于ExtClassLoader来说这里为null)
b.不要重写“ public Class<?> loadClass(*) throws ClassNotFoundException”方法。
而最终顶级抽象类ClassLoader中是这么写的
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通过上面的代码可以看出来,实际加载类时,子加载器递归调用父加载器的loadClass(**)方法,直到到达顶级(parent==null)时,方才调用findBootstrapClass0(**)。这时如果抛出了ClassNotFoundException异常,就表示还没加载呢,然后去调用具体的 findClass(name)实现加载。
- AppClassLoader 如何保证字节码仅被加载一次
AppClassLoader与ExtClassLoader在保证“一个类的字节码仅被加载一次”这个目标上类似。
虽然loadClass方法,但在最后调用了“return super.loadClass(paramString, paramBoolean);”,所以仍旧是OK的。
如下所示:
public synchronized Class loadClass(String paramString, boolean paramBoolean)
throws ClassNotFoundException
{
DownloadManager.getBootClassPathEntryForClass(paramString);
int i = paramString.lastIndexOf(46);
if (i != -1) {
SecurityManager localSecurityManager = System.getSecurityManager();
if (localSecurityManager != null)
localSecurityManager.checkPackageAccess(paramString.substring(0, i));
}
return super.loadClass(paramString, paramBoolean);
}
五,自定义自己的ClassLoader
既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,
比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。
在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。
定义自已的类加载器分为两步
自定义ClassLoader需要继承ClassLoader抽象类,重写findClass方法,这个方法定义了ClassLoader查找class的方式。
- 继承java.lang.ClassLoader
如果嫌麻烦的话,我们可以直接使用或继承已有的ClassLoader实现,比如
java.net.URLClassLoader
java.security.SecureClassLoader
java.rmi.server.RMIClassLoader
sun.applet.AppletClassLoader
Extension ClassLoader 和 App ClassLoader都是java.net.URLClassLoader的子类。
这个是URLClassLoader的构造方法:
public URLClassLoader(URL[] urls, ClassLoader parent)
public URLClassLoader(URL[] urls)
- 重写父类的findClass方法
如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
- findClass:定义查找Class的方式
- defineClass:将类文件字节码加载为jvm中的class
- findResource:定义查找资源的方式