我们知道我们写的程序经过编译后成为了.class文件,.class文件中描述了类的各种信息,最终都需要加载到虚拟机之后才能运行和使用。而虚拟机如何加载这些.class文件?.class文件的信息进入到虚拟机后会发生什么变化
类使用的7个阶段
类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这七个阶段的发生顺序如下图 :
图中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。接下来讲解加载、验证、准备、解析、初始化五个步骤,这五个步骤组成了一个完整的类加载过程。
加载是类加载的第一个阶段。有两种时机会触发类加载:
预加载
虚拟机启动时加载,加载的是JAVA_HOME/lib/
下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时非常常常用到的,像java.lang.*、java.util.、java.io. 等等,因此随着虚拟机一起加载。这里预加载的是Java常用类,为了方便后续开发中的使用。
要证明这一点很简单,写一个空的main函数,设置虚拟机参数为"-XX:+TraceClassLoading"来获取类加载信息,运行一下:
[Opened E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Object from E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.io.Serializable from E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Comparable from E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.String from E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from E:\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.Type from E:\jdk1.8\jre\lib\rt.jar]
....
运行时加载
虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
那么,加载阶段做了什么,其实加载阶段做了有三件事情:
虚拟机规范对这三点的要求并不具体,因此虚拟机实现与具体应用的灵活度都是相当大的。例如第一条,根本没有指明二进制字节流要从哪里来、怎么来,因此单单就这一条,就能变出许多花样来:
总而言之,在类加载整个过程中,这部分是对于开发者来说可控性最强的一个阶段。
链接包含三个步骤: 分别是 验证Verification , 准备Preparation , 解析Resolution 三个过程
验证Verification
连接阶段的第一步,这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
Java语言本身是相对安全的语言(相对C/C++来说),但是前面说过,.class文件未必要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件。在字节码语言层面上,Java代码至少从语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
验证阶段将做一下几个工作,具体就不细讲了,这是虚拟机实现层面的问题:
准备Preparation
准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。 关于这点,有两个地方注意一下:
这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。
各个数据类型的零值如下表:
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
chart | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
下面两段代码,code-1 将会输出 0,而 code-2 将无法通过编译。
code-1
public class A {static int a ;public static void main(String[] args) {System.out.println(a);}
}
code-2
public class B {public static void main(String[] args) {int a ;System.out.println(a);}
}
原因:
这是因为局部变量不像类变量那样存在准备阶段。类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也可以是指定值);另外一次在初始化阶段,赋予程序员定义的值。
因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。
解析Resolution
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。来了解一下符号引用和直接引用有什么区别:
符号引用
符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
这个其实是属于编译原理方面的概念,符号引用包括了下面三类常量:
这么说可能不太好理解,结合实际看一下,写一段很简单的代码:
用javap把这段代码的.class反编译一下:
Constant pool:#1 = Methodref #3.#24 // java/lang/Object."":()V#2 = Class #25 // com/lagou/concurrent/demo/test/TestClassLoader#3 = Class #26 // java/lang/Object#4 = Utf8 i#5 = Utf8 I#6 = Utf8 d#7 = Utf8 D#8 = Utf8 #9 = Utf8 ()V#10 = Utf8 Code#11 = Utf8 LineNumberTable#12 = Utf8 LocalVariableTable#13 = Utf8 this#14 = Utf8 Lcom/lagou/concurrent/demo/test/TestClassLoader;#15 = Utf8 print#16 = Utf8 trueOrFalse#17 = Utf8 ()Z#18 = Utf8 main#19 = Utf8 ([Ljava/lang/String;)V#20 = Utf8 args#21 = Utf8 [Ljava/lang/String;#22 = Utf8 SourceFile#23 = Utf8 TestClassLoader.java#24 = NameAndType #8:#9 // "":()V#25 = Utf8 com/lagou/concurrent/demo/test/TestClassLoader#26 = Utf8 java/lang/Object
看到Constant Pool也就是常量池中有22项内容,其中带"Utf8"的就是符号引用。比如#25,它的值是"com/lagou/concurrent/demo/test/TestClassLoader",表示的是这个类的全限定名;又比如#4为i,#5为I,它们是一对的,表示变量时Integer(int)类型的,名字叫做i;#6为d、#7为D也是一样,表示一个Double(double)类型的变量,名字为d;#15、#16表示的都是方法的名字。
其实总而言之,符号引用和我们上面讲的是一样的,是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。
直接引用
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。
解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做了哪些工作呢?大体可以分为
类的初始化阶段是类加载过程的最后一个步骤, 之前介绍的几个类加载的动作里, 除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外, 其余动作都完全由Java虚拟机来主导控制。 直到初始化阶段, Java虚拟机才真正开始执行类中编写的Java程序代码, 将主导权移交给应用程序。
初始化阶段就是执行类构造器()方法的过程。 ()并不是程序员在Java代码中直接编写的方法, 它是Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问 到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问, 如下代码所示 :
public class TestClinit {static {i = 0; // 给变量复制可以正常编译通过System.out.print(i); // 这句编译器会提示“非法向前引用”} static int i = 1;
}
()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法) 不同, 它不需要显式地调用父类构造器, Java虚拟机会保证在子类的()方法执行前, 父类的()方法已经执行完毕。 因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。
由于父类的()方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值 操作, 如下代码所示中, 字段B的值将会是2而不是1。
class TestClinit02 {static class Parent {public static int A = 1;static {A = 2;}} static class Sub extends Parent {public static int B = A;}public static void main(String[] args) {System.out.println(Sub.B);}
}
()方法对于类或接口来说并不是必需的, 如果一个类中没有静态语句块, 也没有对变量的赋值操作, 那么编译器可以不为这个类生成()方法。 接口中不能使用静态语句块, 但仍然有变量初始化的赋值操作, 因此接口与类一样都会生成 ()方法 。
但接口与类不同的是, 执行接口的()方法不需要先执行父接口的()方法, 因为只有当父接口中定义的变量被使用时, 父接口才会被初始化。 此外, 接口的实现类在初始化时也 一样不会执行接口的()方法。
Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步, 如果多个线程同时去初始化一个类, 那么只会有其中一个线程去执行这个类的()方法, 其他线程都需要阻塞等待, 直到活动线程执行完毕()方法。 如果在一个类的()方法中有耗时很长的操作, 那就 可能造成多个进程阻塞, 在实际应用中这种阻塞往往是很隐蔽的
主要是为了弄明白类的初始化和对象的初始化之间的差别
观察下述代码:
public class ParentA {static {System.out.println("1");}public ParentA() {System.out.println("2");}
}
class SonB extends ParentA {static {System.out.println("a");}public SonB() {System.out.println("b");}public static void main(String[] args) {ParentA ab = new SonB();ab = new SonB();}
}
结果:
1
a
2
b
2
b
其中 static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,所以上面的 static 代码块只会执行一次,它对应的是方法。
所以,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。再加上继承关系的先后原则,不难分析出正确结果
结论
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
jvm支持两种类型的加载器,分别是引导类加载器和 自定义加载器 。其中,引导类加载器是由c/c++实现的,自定义加载器是由java实现的。 jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。
按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
上图中的加载器划分为包含关系而并非继承关系
在日常的Java开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式。
下面代码展示了获取各种累加载器的方式。从系统类加载器层层往上递进,找到父类加载器。
public class ClassLoaderTest {public static void main(String[] args) {// 获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);// 获取系统类加载器的父类加载器,扩展类加载器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader);// 获取启动类加载器的上层启动类加载器,这里会获取不到ClassLoader bootstrapClassLoader = extClassLoader.getParent();System.out.println(bootstrapClassLoader);// 获取用户自定义类的加载器 classLoader的打印结果和systemClassLoader的结果完全一致// 得到默认使用的系统类加载器ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();System.out.println(classLoader);// 核心类库使用的是启动类加载器 以String为例ClassLoader stringClassLoader = String.class.getClassLoader();System.out.println(stringClassLoader);}
}
**双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。**每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 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 方法已经帮我们写好了,我们无需去写。
几个重要函数
loadClass
默认实现如下:
public Class> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}
再看看 loadClass(String name, boolean resolve)
函数:
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
{// 多个线程同时加载类时会调用synchronized进行同步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 statssun.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)
函数即实现了双亲委派模型!整个大致过程如下:
换话句话说,如果自定义类加载器,就必须重写 findClass 方法!
findClass 的默认实现如下:
/*** Finds the class with the specified binary name.* This method should be overridden by class loader implementations that* follow the delegation model for loading classes, and will be invoked by* the {@link #loadClass loadClass} method after checking the* parent class loader for the requested class. The default implementation* throws a ClassNotFoundException.** @param name* The binary name of the class** @return The resulting Class object** @throws ClassNotFoundException* If the class could not be found** @since 1.2*/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 函数。
隔离加载类
修改类加载方式
扩展加载源
防止源码泄漏
实现方式:
所有用户自定义类加载器都应该继承ClassLoader类
在自定义ClassLoader的子类时,我们通常有两种做法:
首先,我们定义一个待加载的普通 Java 类编译后的.class文件,放在某一目录下,比如我这里放在"E:/code/"路径下
然后,定义一个自定义类加载器,该类继承自ClassLoader
,并按上述讲解中的,重写findClass
方法:
public class MyClassLoader extends ClassLoader {private String codePath;public MyClassLoader(String codePath, ClassLoader parent) {super(parent);this.codePath = codePath;}public MyClassLoader(String codePath) {this.codePath = codePath;}@Overrideprotected Class> findClass(String name) throws ClassNotFoundException {BufferedInputStream bis = null;ByteArrayOutputStream baos = null;try{// 1. 获取字节码路径String fileName = codePath + name + ".class";// 2. 获取输入流bis = new BufferedInputStream(new FileInputStream(fileName));// 3. 获取输出流baos = new ByteArrayOutputStream();// 4. io读写byte[] bytes = new byte[1024];int len;// 根据名称读取文件,存入字节数组while((len=bis.read(bytes)) != -1){baos.write(bytes, 0, len);}// 5. 获取内存中字节数组byte[] byteCode = baos.toByteArray();// 6. 调用defineClass将字节数组转成Class对象Class> defineClass = defineClass(null, byteCode, 0, byteCode.length);return defineClass;} catch (IOException e) {e.printStackTrace();}finally {try {bis.close();} catch (IOException e) {e.printStackTrace();}try {baos.close();} catch (IOException e) {e.printStackTrace();}}return null;}
}
然后,写一个测试类进行测试:
public class ClassLoaderTest {public static void main(String[] args) throws ClassNotFoundException {MyClassLoader classLoader = new MyClassLoader("E:/code/");// 利用自定义类加载器加载Person类生成的.class文件Class> clazz = classLoader.loadClass("Person");System.out.println("我是由"+clazz.getClassLoader().getClass().getName()+"类加载器加载的");}
}
最后运行结果如下:
我是由com.lagou.concurrent.demo.test.MyClassLoader类加载器加载的
可以看到,我们自定义了类加载器,并使用我们自定义类加载器成功实现了加载自定义类的.class字节码文件。初步探究了Java中类加载机制的过程。接下来,继续深入对于类加载过程的源码进行分析。
关系类图如下:
我们先从启动类说起 ,有一个Launcher类, 在sun.misc包下
public class Launcher {private static URLStreamHandlerFactory factory = new Launcher.Factory();//静态变量,初始化,会执行构造方法private static Launcher launcher = new Launcher();private static String bootClassPath = System.getProperty("sun.boot.class.path");private ClassLoader loader;private static URLStreamHandler fileHandler;public static Launcher getLauncher() {return launcher;}//构造方法执行public Launcher() {Launcher.ExtClassLoader var1;try {//初始化扩展类加载器var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {//初始化应用类加载器this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}//设置ContextClassLoader ,设置为扩展类加载器Thread.currentThread().setContextClassLoader(this.loader);String var2 = System.getProperty("java.security.manager");if (var2 != null) {SecurityManager var3 = null;if (!"".equals(var2) && !"default".equals(var2)) {try {var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 = new SecurityManager();}if (var3 == null) {throw new InternalError("Could not create SecurityManager: " + var2);}System.setSecurityManager(var3);}}...
}
构造方法 Launcher() 中做了四件事情
其中launcher是staitc的,所以初始化的时候就会创建对象,也就是触发了构造方法,所以初始化的时候就会执行上面四个步骤
看下ExtClassLoader的创建中的关键几步
static class ExtClassLoader extends URLClassLoader {private static volatile Launcher.ExtClassLoader instance;public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {if (instance == null) {Class var0 = Launcher.ExtClassLoader.class;synchronized(Launcher.ExtClassLoader.class) {if (instance == null) {instance = createExtClassLoader();}}}return instance;}private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {try {return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction() {public Launcher.ExtClassLoader run() throws IOException {File[] var1 = Launcher.ExtClassLoader.getExtDirs();int var2 = var1.length;for(int var3 = 0; var3 < var2; ++var3) {MetaIndex.registerDirectory(var1[var3]);}return new Launcher.ExtClassLoader(var1);}});} catch (PrivilegedActionException var1) {throw (IOException)var1.getException();}}void addExtURL(URL var1) {super.addURL(var1);}public ExtClassLoader(File[] var1) throws IOException {super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);}private static File[] getExtDirs() {String var0 = System.getProperty("java.ext.dirs");File[] var1;if (var0 != null) {StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);int var3 = var2.countTokens();var1 = new File[var3];for(int var4 = 0; var4 < var3; ++var4) {var1[var4] = new File(var2.nextToken());}} else {var1 = new File[0];}return var1;}
关键的几个步骤 :
var1 = Launcher.ExtClassLoader.getExtClassLoader();
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
String var0 = System.getProperty("java.ext.dirs");
也在看下AppClassLoader的创建中的关键几步
/
**
* var1 类全名
* var2 是否连接该类
*/
public Class> loadClass(String var1, boolean var2) throws ClassNotFoundException {int var3 = var1.lastIndexOf(46);if (var3 != -1) {SecurityManager var4 = System.getSecurityManager();if (var4 != null) {var4.checkPackageAccess(var1.substring(0, var3));}}//一般都是false,想要返回TRUE可能需要设置启动参数lookupCacheEnabled为true。//为true时,具体的逻辑也是C++写的,所以做了什么就不大清楚了if (this.ucp.knownToNotExist(var1)) {//如果这个类已经被这个类加载器加载,则返回这个类,否则返回NullClass var5 = this.findLoadedClass(var1);if (var5 != null) {if (var2) {//如果该类没有被link(连接),则连接,否则什么都不做this.resolveClass(var5);}return var5;} else {throw new ClassNotFoundException(var1);}} else {return super.loadClass(var1, var2);}}
关键的几个步骤:
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
final String var1 = System.getProperty("java.class.path");
Launcher类中的静态变量
private static String bootClassPath = System.getProperty("sun.boot.class.path");
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),这里我们主要介绍ClassLoader中几个比较重要的方法。
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下:
loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。:
protected Class> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded// 先从缓存查找该class对象,找到就不用重新加载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();// 如果都没有找到,则通过自定义实现的findClass去查找并加载c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//是否需要在加载时进行解析if (resolve) {resolveClass(c);}return c;}}
使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类:
在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,**findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。**需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的ClassLoader类中findClass()方法源码如下:
//直接抛出异常
protected Class> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(defineClass中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader 的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:
protected Class> findClass(String name) throws ClassNotFoundException {// 获取类的字节数组byte[] classData = getClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {//使用defineClass生成class对象return defineClass(name, classData, 0, classData.length);}
}
需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。
使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
上一篇:驱动程序开发:多点电容触摸屏