1. 类的加载、连接与初始化过程
类的加载:
1. 类的加载的最终产品是位于内存的Class对象。
2. Class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区内的数据结构的接口。
有两种类型的类加载器:
1. java虚拟机自带的加载器
1.1 根类加载器Bootstrap ClassLoader
$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。
1.2 扩展类加载器 Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar
或-DJava.ext.dirs指定目录下的jar包。
1.3 系统(应用)类加载器System ClassLoader
负责加载classpath中指定的jar包及目录中的class。
2.用户自定义的类加载器
2.1java.lang.ClassLoader的子类
2.2用户可以定制类的加载方式
3. 类加载器并不需要等到某个类被“首次主动使用”时再加载它。
4. JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误。类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类的验证:
1. 类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
2. 类的验证的内容(jdk版本不同会有差异,此处只是列举一些关键的)
2.1 类文件的结构检查
2.2 语义检查
2.3 字节码验证
2.4 二进制兼容性的验证
类的准备
在准备阶段,java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下Sample类,在准备阶段,将int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0。
public class Sample {
private static int a = 1;
public static long b;
static{
b =2;
}
}
类的初始化:
在初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:
(1) 在静态变量的声明处进行初始化。
(2) 在静态代码块中进行初始化。
例如在以下的代码中,静态变量a和b都被显式初始化,而静态变量c没有被显式初始化,它将保持默认值。
public class Sample {
//在静态变量的声明处进行初始化
public static int a = 1;
public static long b;
public static long c;
static{
//在静态代码块中处进行初始化
b =2;
}
}
静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。例如当以下Sample类被初始化后,它的静态变量a的取值为4。
public class Sample {
//在静态变量的声明处进行初始化
public static int a = 1;
static{
a =2;
}
static{
a =4;
}
public static void main(String[] args) {
System.out.println("a = " + a);
}
}
类的初始化步骤:
1. 假如这个类还没有被加载和连接,那就先进行加载和连接。
2. 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
3. 假如类中存在初始化语句,那就依次执行这些初始化语句。
类的初始化时机
主动使用
1. 创建类的实例
2. 访问某个类或接口的静态变量,或者对该静态变量赋值
3. 调用类的静态方法
4. 反射
5. 初始化一个类的子类
6. Java虚拟机启动时被标明为启动类的类
7. JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化。
类的初始化时机
1. 除了上述七种情形,其他使用Java类的方式都被看做是被动使用,不会导致类的初始化。
2. 当Java虚拟机初始化一个类时,要求它的所有的父类都已经被初始化,但是这条规则并不适用于接口。
2.1 在初始化一个类时,并不会初始化它所实现的接口
2.2 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
3. 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。
4. 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
类加载器
类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父类加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader本身加载Sample类。
除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。
加载class文件的方式
1. 从本地系统中直接加载
2. 通过网络下载class文件
3.从zip、jar等归档文件中加载.class文件
4.从专有数据库中提取.class文件
5. 将java源文件动态编译为.class文件
类加载器的双亲委托机制
当一个类加载器想要去加载某个类时,它并不由自己立刻去加载,而是会委托父类加载器去完成,直至追溯到根类加载器,由其尝试着加载那个class文件,如果加载不成功,就会把结果向下返回,从上至下到最开始的那个类加载器,在整个过程中,在任何的类加载层次上只要它能成功的加载这个类,就宣告这个加载动作是成功的,就会把结果返回给最初尝试加载的类加载器。
命名空间
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类,在不同的命名空间中,有可能出现类的完整名字相同的两个类。
不同类加载器的命名空间关系
同一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间,因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载加载的类能看见根类加载器加载的类。
由父加载器加载的类不能看见子加载器加载的类
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
类的卸载
由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。java虚拟机本身会始终引用这些类加载器。而这些类加载器则始终引用它们所加载的类的class对象,因此这些class对象始终是可触及的。
由用户自定义的类加载器所加载的类是可以被卸载的。
一个类何时结束生命周期,取决于代表它的class对象何时结束生命周期。
类加载器的双亲委托模型的好处
1、可以确保java核心库的安全:所有的Java应用都至少会引用
java.lang.Object类,也就是说在运行期,java.labg.Object这个类会被加载到java虚拟机中,如果这个加载过程是由java应用自己的类加载器所完成的,那么很可能就会在java虚拟机中存在多个版本的java.lang.Object类,而且这些类之间是不兼容的,互相不可见的(存在于不同的命名空间中)。借助于双亲委托机制,java核心类库中的类的加载工作都是由根类加载器来统一完成加载工作,从而确保了java应用所使用的都是同一个版本的java核心类库,他们是相互兼容的。
2、确保java核心类库所提供的类不会被自定义的类所取代。
3、不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在java虚拟机中,只要用不同的类加载器来加载他们即可。不同类加载器所加载的类是不兼容的,这就相当于在java虚拟机的内部创建了一个有一个相互隔离的java类空间,这类技术在很多框架中得到了实际的应用。
内建于jvm中的启动类加载器会加载java.lang.classLoader以及其它的java平台类,当jvm启动时,一块特殊的机器码会运行,它hui加载扩展类加载器与系统类加载器,这块特殊的机器码叫作启动类加载器。
启动类加载器并不是java类,而其它的加载器则都是java类,启动类加载器是特定于平台的机器指令,它负责开启整个加载过程,它还负责加载供jre正常运行所需要的基本组件,包括java.util与 java.lang包中的类等等。
当前类加载器(Current ClassLoader)
每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其它类(指的是所依赖的类)。如果classX引用了classY,那么classX的类加载器就会去加载classY(前提是classY尚未被加载)。
线程上下文类加载器 (Context ClassLoader)
线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。java应用运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源。
线程上下文类加载器的重要性
SPI:Service Provide Interface,服务提供者接口,自定义一些标准,具体的实现由厂商来完成 。是JVM内置的一种服务提供发现机制。如JDBC、JNDI、JAXP等。
父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的ClassLoader来加载类,这就改变了父ClassLoader不能使用子ClassLoader或是其他没有父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。
线程上下文类加载器就是当前线程的currentClassLoader。
在双亲委托模型下,类加载器是由上至下的,即下层的类加载器会委托上层进行加载,但是对于SPI来说,有些接口是java核心类库所提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同jar包(厂商提供),java的启动类加载器是不会加载其它来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。
在框架开发、底层组件开发、应用服务器、web服务器的开发,就会用到线程上下文类加载器。比如,tomcat 框架,就对加载器就做了比较大的改造。
tomcat 的类加载器是首先尝试自己加载,自己加载不了才委托给它的双亲,这于传统的双亲委托模型是相反的。