类加载器并不是JVM固定的一部分,之所以这么说是因为JVM内部有他们自己实现的类加载器,这表明:外部的类加载器是可以自定义的。对于JAVA来说,没有什么不可能,就算去写自己的VM也完全没问题。
类加载器是非常重要的一部分,它描述了class文件是如何进入虚拟机内存的,为此产生了哪些行为等一系列重要的问题。JAVA本身提供了三个类加载器,分别应对不同等级的类文件(比如说VM内部的类文件和你写的类文件),为了不混淆这些内容,类加载器是空间隔离的(和C++的名字空间一样),也就是说如果不同的类加载器加载了同一个class文件,比如像这样:
public class ClassLoaderTest {
private class MyLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
String file = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream input = getClass().getResourceAsStream(file);
if(input == null){
return super.loadClass(name);
}
byte[] b = null;
try{
b = new byte[input.available()];
input.read(b);
}catch (IOException e){
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}
}
public void print() throws ClassNotFoundException, IllegalAccessException, InstantiationException{
MyLoader myLoader = new MyLoader();
Object obj = myLoader.loadClass("thread.li.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof thread.li.ClassLoaderTest);
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException{
new ClassLoaderTest().print();
}
}
/*output
class thread.li.ClassLoaderTest
false
*/
答案居然不是true,很神奇,obj明明是ClassLoaderTest的对象,这就是类加载器的空间隔绝效果。
类加载的时机
首先明确,class文件就是一串二进制的不定长的有规则的字符串。
JAVA是解释型语言,即边解释边执行程序,因此类加载器只有在被需要的时候才会启动,这大概分为五种情况:
1、使用new实例化对象或操作静态字段/方法的时候(final字段例外,final字段有比较特殊的实现)。
2、加载一个类时,发现其父类未被加载,则加载其父类(一个递归的过程),如此第一个加载完成的类一定是Object。
3、通过反射对某个类进行操作时,如果其未加载,则对它进行加载。
4、JVM启动时,入口类必须被加载。
5、如果方法句柄(MethodHandle)对应的类未被加载,则对其加载。
当且仅当以上五种情况才会启动类加载。有些情况看似引用了没有使用的类,但并不会引起加载动作,比如以下三种情况:
1、通过子类名引用父类的静态字段/方法。
2、创建引用类型数组(非基本数据类型数组)时,不会引起引用类型代表的类的加载动作。
3、使用类常量(静态常量)时,类常量在编译阶段对它的值进行了特殊存储(放在属性表里)。
类加载过程
加载——>验证——>准备——>解析——>初始化——>使用——>卸载
验证准备解析这三个阶段又可叫做链接。以上这些阶段必须按部就班的开始,但这并不意味着他们也按部就班的进行和完成。他们可能混合交叉的运行,比如加载和验证就是交叉进行的,解析阶段例外,类可能在初始化之后再解析,这是为了配合动态绑定。
加载:
这个阶段主要完成以下三件事情,1、通过类的全限定名获取此类的二进制字节流。
2、将此字节流的静态存储结构转换成方法区内的运行时数据结构。
3、在内存中生成一个代表此类的class对象,作为方法区中此类数据的访问入口。
ps:这部分的数据结构由实现虚拟机的团队自行定义,class对象在哪也自行选择,类的全限定名在哪获取也不做硬性规定。加载和验证阶段的部分内容是交叉进行的。
验证:
这阶段就是对class文件中的内容进行诸多验证以确保class文件内容合法且没有被篡改且逻辑无矛盾。但就像测试一样,这不可能穷尽错误。
大致分为四阶段:文件格式验证,元数据验证,字节码验证,符号引用验证。
此阶段对jvm的自我保护有着非常重要的作用,但无人为刻意或意外的情况下,几乎不会出现错误,因此理论上这个阶段可以关闭。
准备:
这阶段会为类变量(静态)分配内存并赋初值,这里说的初值是数据类型的默认初值(大部分是0,引用是null),赋值号后面的值(如果有的话)要在初始化阶段才会赋上去。
类常量(static final)例外,类常量会直接赋”=”后面的值,由于类常量特殊的实现方式,它的值会直接存在字段表里。
解析:
将class文件中的符号引用解析成指向内存的直接引用,该阶段比较灵活,可以马上运行然后对结果缓存,也可以用的时候才解析,所以他什么时候运行并不是很重要,只要解析不出错且够快就可以。
初始化:
这个阶段的主要任务就是执行类构造器。类构造器区别于自定义的实例构造器,它是由虚拟机自动生成,它是由虚拟机收集类变量的赋值操作和静态块组成的,换言之如果前两者都没有,那么类构造器也就不用生成了。
注意,父类构造器一定先于子类构造器运行,那么最先运行的类构造器一定是Object(如果有的话);静态块只能看见它上面的内容看不见后面的;对于上面的执行顺序,接口是例外的,父接口并不一定先于子接口运行类构造器;类构造器是同步的,在类的整个生命周期它只会运行一次,不管有多少线程访问,它仅仅运行一次。
数组
数组类的加载由jvm直接负责,不归外部的类加载器管。
总结一下接口的特殊性:接口从不主动的加载,只有用到才会加载。