类加载器
ClassLoader是负责加载类的对象。ClassLoader类是一个抽象类,给定一个类的二进制名(任何一个类都有一个根据JLS规范定义的String类型的二进制名,ClassLoader中使用该名进行加载,其实在jni开发中,用的也是这个,详见jni tips),典型的策略是将二进制名转换成文件名然后从文件系统读取class文件。
每个class都包含一个classloader的引用,数组类型的class不是有classloader创建的,而是在运行时由java runtime自动创建,数组类型获取classloader时,返回的是数组元素的类型。如果数组元素是基本类型或者是由bootstrap classloader加载的类(比如rt.jar包),那么没有classloader。
如下代码:
1 | Utils.printString("string class: " + String.class); |
输出1
2
3
4
5
6
7
8string class: class java.lang.String
string class classloader: null
int array class: class [I
int array classloader: null
string array classloader: class [Ljava.lang.String;
string array classloader: null
refre array class: class [Lcom.zgq.java.ClassLoaderTest;
refre array classloader: sun.misc.Launcher$AppClassLoader@4b67cf4d
由上面得到结论
- 数组类型的ClassLoader是其元素的ClassLoader,如果元素ClassLoader为空,那么数组ClassLoader也为空
- 由于String在rt.jar包中,是有bootstrap加载的,所以getClassLoader返回空
- int数组的class类型是[I,java语言规范中,[表示数组类型,I表示int
程序中继承classloader来实现自己的classloader,这样可以延用jvm的动态加载机制。
类加载器常用于安全管理机制,保证安全域。
ClassLoader使用代理模式来寻找类或者资源,每个ClassLoader都有一个父ClassLoader,当要请求查找一个新的类或者资源时,ClassLoader的实例自身在搜索前,会让父ClassLoader先去查找。jvm内建的ClassLoader叫bootstrap classloader,这个加载器没有父加载器,但是可以作为其他类加载器的父引用。
支持同步加载的类加载器叫parallel capable class loader,这样的类加载器在初始化的时候需要注册方法ClassLoader.registerAsParallelCapable,注意ClassLoader默认注册了改方法,但是,他的子类如果想支持同步还要重新注册。
某些条件下,代理模型并不是严格分级的,ClassLoader需要支持并发,否则加载类的过程可能引起死锁。详见loadClass方法。
一般情况下,jvm从本地文件系统加载类,比如在UNIX系统下,会直接从CLASSPATH环境变量指定的路径中加载。但是,有时候类文件是从其他方法比如网络上下载下来,这时我们可以使用defineClass方法将byte数组转换成类文件。新定义的类可以通过Class.newInstance创建实例。
自定义类加载器,需要注意两个方法,一个是defineClass,是一个final方法,由ClassLoader定义,这个方法把我们读入的字节数组转换成Class对象。另一个是要覆写findClass方法,这个方法会在委托失败后被调用:1
2
3
4
5
6
7
8
9
10
11
12class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}
这个类加载器的加载流程为:因为没有指定父加载器,所以会先由bootstrap class loader加载,bootstrap class loader加载失败后再由NetworkClassLoader定义的findClass加载类
数组的类
数组类型的ClassLoader有可能是bootstrapclassloader,也可能是用户自定义ClassLoader。如果元素类型为C的数组已经被初始化过,也就是说已经有classLoader加载过元素类型为C的数组,那么,这时候已经有了该元素类型的数组类型,等再次创建该元素类型的数组时,不需要创建新的数组类型。1
2
3
4//这时JVM已经有了一个class为[Ljava.lang.string的数组类型
String[] stringArr = {"545"};
//这时JVM不会再创建一个[Ljava.lang.string的类型
String[] stringAr1 = {"444"};
- 如果数组元素是引用类型,那么也会按照一般规则向上递归,加载所有父类。
- JVM根据数组元素类型和数组的维度(注意是维度,不是长度,一维数组是[,二维数组是[[)创建数组类型
- 如果数组元素是引用类型,那么结果数组类型的ClassLoader和元素的ClassLoader一致,否则就是bootstrap ClassLoader
- 任何情况下,JVM都将(1)里的ClassLoader作为数组类型的初识ClassLoader
- 如果数组元素是引用类型,那么数组类型的访问权限由元素的访问权限决定,否则就是public
类的加载过程
类从被加载到JVM中开始,到卸载为止,整个生命周期包括加载、连接、初始化、使用、卸载,其中连接又分为三步:验证、准备、解析。
官方文档在这里,java虚拟机规范第五章详细讲了类加载过程的几个阶段,章首有一段非常概括的话:java虚拟机动态的加载、链接和初始化类和接口。加载是虚拟机根据特定的名称查找类或者接口的二进制表示,然后从这个二进制表示创建类或接口的过程。链接是为了让类或者接口能被虚拟机执行,而将类或接口并入虚拟机运行时的过程。类或接口的初始化过程就是执行类或接口的初始化方法
文档从5个方面阐述了类和接口的加载过程:
- 运行时常量池,虚拟机从类或接口的二进制表示中获取字符引用,其实就是使用class文件中的常量池表构建运行时常量池。
- 虚拟机启动时是怎样的加载、链接、初始化过程
- 类或者接口的二进制表现是如何被类加载器加载并创建类和接口
- 详细解释链接过程
- 类和接口的初始化过程
- 绑定本地方法的概念
运行时常量池
虚拟机的内存结构中,有块逻辑区域叫方法区,方法区是可供各个线程共享的运行时内存区域。方法区与传统语言中编译代码存储区非常相似,存储了每个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容,还有一些特殊函数,比如类、实例、接口的初始化函数。方法区是堆的逻辑组成部分,但是简单的虚拟机可以不实现垃圾回收与压缩,方法区大小可以固定也可以动态扩展,但如果方法区内存不足,会报OutOfMemoryError。
在创建类或接口时,虚拟机会根据类或接口的二进制表示中的常量池表来构造运行时常量池,运行时常量池初始时都是符号引用,这些符号是按照以下方法在类或接口的二进制表示中得出的,以类或接口的名称为例:
- 类或接口的符号引用来自常量池表中的CONSTANT_Class_info,这种引用提供类或接口的名称
- 对于非数组的类或接口,这个名称就是类或接口的二进制名称
- 对于n维数组,会以n个[开头,后面跟数组元素类型
- 如果数组元素类型是基本类型,有对应的字段描述符,比如int以I表示
- 如果数组元素类型是引用类型,则以L加上元素的二进制名称,并以;结束
- 类或接口中的字段的符号引用来自类或接口的二进制表示中的CONSTANT_Filedref_info结构,这种引用包括了字段名称和描述符,以及指向字段所属类或接口的符号引用
此外还有方法、方法句柄、方法类型以及调用点限定符的符号引用,这里不在赘述
…..
需要注意的是字符串常量,字符串常量是指向String类实例的引用,它来自类或接口的二进制表示中的CONSTANT_String_info结构。CONSTANT_String_info给出了由Unicode码点序列组成的字符串常量。java规定,相同的字符串常量,也就是包含同一份Unicode码点序列的常量,必须指向同一个String实例。此外,如果在任意字符串上调用String.intern方法,那么其返回的String实例,必须和直接以常量形式出现的字符串实例完全相同,即下式永远为true:1
("a" + "b" + "c").intern() == "abc";
这里要注意的是字符串的创建方式对引用值的影响,举例如下:1
2
3
4
5
6
7
8
9//这样创建的字符串,会先检查方法区中字符串常量池中是否有值相等的实例,如果有,直接返回那个实例的引用给a,否则创建一个值为test的实例,再将引用返回给a
String a = "test";
String b = "test";
Utils.printString("a == b is" + (a == b));
//这样创建的字符串,会先在堆上创建一个String实例,引用地址返回给c,然后JVM会去方法区中的字符串常量池查看是否有"test"字符串的对象,没有的话就分配一个空间来存放"test",并将其空间地址存入堆中new出来的对象中;直接将那个实例存入堆中new出来的对象中
String c = new String("test");
String d = new String("test");
Utils.printString("c == d is" + (c == d));
Utils.printString("a == c is" + (a == c));
所以上面代码结果1
2
3a == b is true
c == d is false
a == c is false
而上面intern方法的作用就是,如果字符串常量池中存在值相等的字符串实例,则返回,否则在字符串常量池中创建一个实例并返回。
虚拟机启动
引导类加载器(boostrap class loader)创建一个初始类,然后虚拟机链接该初始类,初始化它并调用他的void main(String[] args)方法
创建和加载
如果要创建标记为N的类或接口C,需要在java虚拟机的方法区上为C创建与虚拟机实现相匹配的内部表示。C的创建是由另一个类D触发的,它通过自己的运行时常量池引用了C,比如D中包含C的引用。当然,反射或者一些特殊方法也可能触发。
类加载器L可能直接定义或委托其他类加载器的方式来创建C,如果L直接定义了C,那么说L是C的定义加载器。而类加载器在加载时,有可能将加载请求委托给别的类加载器完成,那么这时候可以说是L导致了C的加载,或者说L是C的初始加载器。
虚拟机运行时,类或者接口不仅仅由它的名称来确定,而是由名称和类加载器共同确定
D触发创建C,如果D是由引导类加载器加载,那么C也由引导类加载器加载,如果D由用户自定义类加载器加载,那C也由用户自定义类加载器加载。也就是说两者一致。
链接
链接类或接口包括验证和准备类或接口,它的直接父类,它的直接父接口,它的元素类型(如果是一个数组)。解析这个类或者接口的符号引用是可选的。
java虚拟机允许灵活的选择链接时机,但必须抱枕
- 链接前成功加载过
- 初始化前成功验证和准备过
- 当程序执行某个可能直接或间接链接一个类或接口的工作时,而在链接过程中出错,那错误的抛出点应该在执行动作的点
虚拟机没有强制规定解析符号引用的时机,可以在使用到类或接口的符号引用时才去逐一解析(延迟解析),也可以在验证类的时候就解析每个引用(预先解析)。验证
确保类或者接口的二进制表示在结构上是正确的。验证静态约束和结构化约束,见4.9节。
如果虚拟机尝试验证类或接口,但因为抛出LinkageError或LinkageError子类的实例而导致失败,则之后对此类的验证总会因为相同原因失败。准备
准备阶段的任务是创建类或者接口的静态字段,并用默认值初始化这些字段。这个阶段不会执行任何的虚拟机字节码指令。解析
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中
解析是根据运行时常量池里的符号引用来动态决定具体值的过程。或者说解析就是把符号引用转换成直接引用的过程:
- 运行时常量池里字段的符号引用作为字段的”唯一标识符“(class+NameAndType),确定其是什么引用类型,从而由jvm定位到该类的结构体的内存地址
- 运行时常量池里字段的符号引用作为方法的”唯一标识符“(class+NameAndType),确定改方法是哪个类的,再根据NameAndType从方法表中找到对应的方法
同理还有类和接口解析,普通方法解析
……
其至于这个过程发生了什么,参照知乎收藏。这里先不展开描述。初始化
初始化对类和接口来说,就是执行它的初始化方法。到了这个阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
tip