当程序运行至主动需求使用某个类的数据,而JVM中并不存在该类时,JVM将会通过加载、连接、初始化三个步骤来对该类进行加载,这一系列的三步操作被合称为类的加载或者类的初始化
在java的实现方案中,类型的加载是时机是运行期间加载的
显然,动态加载将会带来更大的灵活性,但是对于性能来说似乎并不是优秀的方案,jvm将要设法避免发生程序运行至某处后需要暂停等待类加载完毕才能继续向下运行的窘境
因此,jvm进行的是预测可能会使用的类进行预加载,也因此,在加载类时,如果出现了错误,直到程序真正使用到该类的时候,才会抛出错误
java的类加载是动态的,也因此这个加载的过程有许多值得深入探究的点
类的加载分为加载、连接、初始化三部分,同时,连接又可再次细分为验证、准备、和解析,需要注意的是,这个顺序是开始的顺序,即这些行为将会按需唤醒,而并不一定是按这个顺序执行
这之中其中,解析可能会在初始化之后再进行,其原因是由动态绑定导致的
首先,何为绑定,我们先来看一段代码
class father{
say(){
System.out.printf("你好");
}
}
class son extend father{
say(){
System.out.printf("你好我是子类");
}
}
public class test{
father f = new father();
fahter s = new son();
f.say();
s.say();
}
编译器在编译test为.class文件时,其中出现了代码f.say()和s.say(),对于编译器来说,并不能确定say()方法的具体代码所存在的类已经被加载,因此会在此处存放一个方法的符号引用
而在正式运行时,我们需要获取say()方法对应的代码的确切地址,这个将say()方法的符号引用转换为直接引用的过程,就称为绑定
我们知道,方法的具体代码在类被加载完毕时,和其他的类的元数据一同存放于元空间之中,也就是说,在类加载完毕时,方法的具体地址就已经确定了,理论来说,此时我们就应该可以进行方法的绑定,在此时就可以进行的绑定被称为静态绑定,因为程序的运行实际上还没有正式开始
同时我们看到,此处的示例代码中,f和s对象都是father类型,但是分别被以两种不同的构造方法实现
对于编译器来说,这两个fahter对象都在执行say()方法,而这个方法具体指向的地址要根据两个构造方法的运行结构以确定到底运行哪个类中的方法代码
因而,绑定在面对可能存在的多态关系时,并不能在实际运行之前就确定say()方法的具体地址,此时进行的绑定会推迟到运行期间再生效,这种绑定被称为动态绑定
何时进行静态绑定:
因为要忌惮可能存在的继承关系与多态,多数情况下都会使用动态绑定——即知道程序开始运行,对象被具体实现后再根据对象的信息来定位确切的方法代码的直接内存,因而,我们需要注意的是何种特殊情况会使用静态绑定:
当方法被private、static、final修饰时,将会进行静态绑定,这是因为:
被private修饰的方法-子类无法继承
被static修饰的方法-和类本身绑定,不依赖对象存在
被final修饰的方法-子类不能重写
本质上,加载阶段所做的行为概括为一句话:通过类加载器将.class文件中的二进制数据加载进jvm,并将数据存于方法区,之后,使用这些数据在堆中创造一个唯一对应的class对象
在这里,有两个较有争议的话题:
并没有明确规定是在java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面——这句话出自经典书籍:《深入理解Java虚拟机 第2版》
但是根据我们前三章的知识可以很清楚的明白,方法区其实是一个设计上的区域名称,也就是每个虚拟机都会使用不同的底层方法实现方法区,而此处的描述专指HotSpot虚拟机,HotSpot虚拟机在对于方法区在6、7、8连续三个版本都对方法区的具体实现有所改动,且本书写成于2011年,8更新于2016年,自然就可以得出,这句话至少并不是一个足够全面的回答
在博客:JDK 1.8 下的 java.lang.Class 对象和 static 成员变量在堆还是方法区?
中,我们可以看到详细的关于class对象的存放的解析,在我们最常用的版本jdk8以及jdk7中class对象确实是存放在堆之中的
作为引子,在此提出一个问题:我们知道,类加载行为的第一步,加载操作,其行为是将类的元数据加载入元空间然后再创造出一个对应的java.lang.class对象,到这一步,加载才结束
那么class类在加载时呢,是否会有这个对象被创造出来?
首先是答案:class类加载时和普通的类一样,都会创建class对象。那么,class类都未加载进入jvm,是谁创建出的这个对象呢,答案很简单,创造这个对象的并未使用class类,这个class对象是由jvm底层的,由c++实现的klass对象创造的
那么何为klass对象?klass对象是klass-oop模型的具体实现的一部分
那么何又为klass-oop模型?klass-oop模型是:对于在jvm中如何表示java中的对象一种设计实现,我们常用的hot-spot虚拟机选用的就是这种实现
hot-spot虚拟机是使用c++语言写出的,那么对于一个java对象该如何表示呢,最简单的思路是,每创建一个java对象,那么就创建一个对应的c++对象来表示它,hot-spot在这个基础上进行了升级——若直接使用这种设计方法,每个对象都需要有一个对应的虚函数表(你可一理解为c++中用来装对象对应的类的成员变量常量和方法的地方)
他采取一种将对象一分为二的方法:
klass:
他将对象之间公用的部分——方法、常量等部分使用一个对象封存,存放于元空间之中,该对象名为klass,存放的就是我们常说的类的元数据
oop:
而每个对象的独有属性又交给另一个对象封存,存放于堆空间之中,其名为oop,且可以分为如下具体结构
若深入探究klass的实现代码,其中有一行:
// java/lang/Class instance mirroring this class
oop _java_mirror;
klass的代码中实现了一个oop对象,并命名其为_java_mirror,该oop对象拥有对klass对象本体进行访问的权限,在之前已经强调过,c++中的oop对象实质上就是普通的java实例对象,也就是说,元数据klass对象在新建的时候偷偷的新建了一个java实例对象,且该对象包含访问klass元数据的方法,那么,这个对象究竟是谁呢——自然,就是我们讨论的主角:class对象
在此讨论的似乎都是加载被触发后所做的行为——自然,加载是类加载的第一步,那么在这之前发生了一些什么呢
如图所示,我们所讨论的“加载”行为值得是从.class文件开始一直到生成class对象结束的过程,而在这之前,发生的行为是,由我们这些程序员书写完毕一个.java文件,之后保存,由javac编译器执行编译为一个描述了类文件具体信息的class对象,包括了类的所有常量与类的所有方法,之后再保存为一个.class文件
加载这个行为本身全权由类加载器执行,类加载器本身自然成为了一个知道讨论的独立模块,在此,由我个人兴趣使然,将会深入探究类加载器这一庞大的模块
类加载器可以粗略分为系统自带的三个加载器和用户自定义的类加载器,之间的关系如下:
一些值得一谈的特性:
类之中的怪物——数组,之前提过,数组的类型和基本类型很像,都是由jvm创造的,因此:
1.他们的类对象与类加载器无关,由jvm创建
2.获取他们的类加载器对象时,会获取到其元素类型的类加载器,若其元素类型为基础类型则为null
启动类加载器由c++编写,属于本地类,因此在java代码中尝试获取扩展加载器的父类将会直接获取一个null,代码如下:
public class t022 {
public static void main(String[] args) {
System.out.println(t022.class.getClassLoader());
System.out.println(t022.class.getClassLoader().getParent());
System.out.println(t022.class.getClassLoader().getParent().getParent());
}
}
/*
输出内容为:
sun.misc.Launcher$AppClassLoader@4e0e2f2a
sun.misc.Launcher$ExtClassLoader@2a139a55
null
*/
知道根类加载器是c++书写的,自然可以知道,三个加载器之间并不是父子类关系——其中扩展类加载器和引用加载器甚至是兄弟关系,他们只是单纯的在getparent之中直接返回了他的父类关系的加载器,所以只是逻辑上的父类,实际上类java内部的类加载器关系为:
这其中
通常来说,类加载器默认使用双亲委派模型,而我们的自定义加载器可以自行创建新的模式,或者继承这种特点。且自定义加载器都会被默认为是应用加载类的子加载器,当然,也可以手动设置具体的父类加载器
突然插入一个前面完全没提过的类,大概给大家一个大哥你谁啊的感觉,首先,它是干什么的?看如下出自ClassLoader类源码:
```java
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
大意为,类加载器的initSystemClassLoader初始化系统类加载器方法,实质上其实是实例化sun.misc.Launcher类,也就是说,sun.misc.Launcher是类加载器的启动类
当然,细心的伙伴应该还发现了,之前代码中两个类加载器的输出内容为:sun.misc.Launcher$AppClassLoader@4e0e2f2a
和sun.misc.Launcher$ExtClassLoader@2a139a55
这其中,$意味着内部类,也就是说,实际上这两个类加载器都是launcher类的内部类!
我们直接进入launcher类的代码看看他做了些什么!
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
简单归纳launcher所做的:
值得注意的是launcher类中,并没有配置根类加载器(因为是本地类),但是为其配置了地址:sun.boot.class.path
在这里,sun.boot.class.path指的其实就是大部分的java开头的核心类存放的地址,也意味着根类加载器负责的是核心类的加载
同样的,在扩展类加载器和应用类加载器的源码中也进行了配置:
扩展类加载器
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
可以看到,扩展类加载器负责的地址为:java.ext.dirs,该地址将会自动拼接为$JAVA_HOME/jre/lib/ext,此处存放的是java自带的许多扩展jar包,包括一些加解密功能包,也就是说扩展加载器负责加载的是java自带的一部分扩展.jar包
应用加载器
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
}
应用加载器使用的地址为:java.class.path,该地址为编译环境默认的生成class文件的地址,也就是说应用加载器负责加载的是我们自行书写的class文件
这三者加载的目标有一个很明显的规律,越是上级的类加载器,加载的区域就越为核心,在此基础上,我们再更加深入一点,了解类加载器的安全机制:
双亲委派机制:
双亲委派机制是什么,他是类加载器默认遵从的一种运行模式,在该模式下:
任何类加载器在加载类时,都将先唤醒自己的父加载器,请求自己的父类加载器进行处理,以此类推,直至请求至根类加载器
然后从根类加载器开始,在自己司职的空间尝试加载该类,若找不到目标类,回到调用自己的子加载器,子加载器再在自己的空间内查找,以此类推,直到回到最初发起请求的类
再此模式下,能够保证程序运行时的类是正确的核心类
比如,自定义的类加载器收到的他人传输的类文件中有一个全限定名设定为java.lang.String的类,企图用修改过的类代替核心类,那么,自定义加载器会一直向父类加载器请求加载该限定名的类,一直到根类加载器,在其管理的jdk工具包中找到了java.lang.String
也就是这个希望你使用自定义加载器加载他人提供的核心类的企图,在该模式下会被强制替换为使用根类加载器在jdk工具包中加载核心类
总的来说,双亲委派机制为安全而生,其目的是为了保证程序运行时加载的始终是正确的核心类
命名空间
命名空间是类加载器的一种属性,可以理解为类加载器可访问的权限。
先简略描述命名空间的性质
比如,应用加载器的命名空间包括扩展类加载器和根类加载器,因此,由他加载的类可以加载包括:我们自行编写的代码所产生的所有类+扩展加载器加载的扩展jar包+根类加载器加载的java核心类
而扩展类加载器所只包含自己和根类加载器,因此,这些扩展类加载器加载的类,不能加载我们自行编写的所有类,以此类推,核心包中的代码也无权干涉核心包类以外的全部类
因此,在这种模式下可以保证我们书写的源码可以畅通无阻的使用所有工具包中的类的同时,也能确保上传核心包代码的oracle和上传扩展类包的其他公司不会在代码中掺杂会干涉我们源码的代码
总的来说:
作为类加载器中的原始类,除了根类加载器外全被的类都是其子类,因此classloader类的源码值得深入探讨。实际上,当我们要自定义类加载器时,实际一般都是直接继承ClassLoader,这是因为其子类SercureClassLoader类添加了类源头验证之类的安全验证,使得我们并不能自由的加载自定义的类,因此,我们自定义的类加载器通常都是直接继承ClassLoader类,并且通过重写其中的逻辑达成自定义加载器的目的,这其中有几个尤为常用的方法:
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方法由两部分构成:
1.将加载名交给父类加载器,请求父类加载器进行查询
2.若父类加载器加载不到,启动自己的的findClass方法,自行加载该类
该类本身的地位实际上就是所有类加载器在加载类时调用的方法,且该方法的具体实现逻辑实际上就是双亲委托机制的实现——也就是说如果我们希望定义一个自定义的类加载器:
classLoader类中的findClass类并没有详细定义细节,而是直接抛出异常,通常来说findClass方法中定义的是从何处获取.class文件,并将其转换为字节流(实际上,只要是字节流就可以,并不需要是来自.class文件的,因此大大提高了数据来源的灵活性),最终将类的全限定名和字符流数据交给defindClass方法,将类正式加入jvm之中
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
一般来说,自定义的类加载器就是对该方法进行重写,在之后的自定义类加载器中,将会深入讲解其配置方式
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
其具体做的行为是,通过参数,类的全限定名和字符流数据在jvm之中生成一个类,通常和findClass方法连用于自定义类加载器的构造。
值得一提的是,此时的类值是进行了“加载”,也就是后续的连接、初始化并没有进行!