类的生命周期分为加载、连接(验证、准备、解析)、初始化、使用、卸载几个阶段

在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束。
但是对于初始化阶段,《Java虚拟机规范》 则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
Java虚拟机中类加载过程包括加载、验证、准备、解析和初始化这五个阶段。
在加载阶段虚拟机主要完成3件事情:
获取二进制字节流不一定要从本地文件中获取,也可以从压缩包(jar包、war包)、网络、运行时计算生成(JSP文件生成对应的Class文件)、加密文件(反编译保护措施)、数据库中获取
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,这两个阶段的开始时间仍然保持着固定的先后顺序。
这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
这一阶段是为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值。
假设一个类变量的定义为:
public static int value = 123;
变量value在准备阶段过后的初始值为0而不是123,在之后的初始化阶段才会赋值为123。
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,上面类变量value的定义修改为:
public static final int value = 123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value赋值为123。
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化阶段就是执行类构造器方法的过程。 这时Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
并不是程序员在Java代码中直接编写的方法,它是Javac编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量。
public class Test {
static int i = 1;
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); //编译通过,输出 0
}
public static void main(String[] args) {
//输出 0
System.out.println(i);
}
}
定义在它之后的变量,在前面的静态语句块可以赋值但是不能访问
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
// System.out.print(i); 这句编译器会报错“非法向前引用”
}
static int i = 1;
public static void main(String[] args) {
//输出 1
System.out.println(i);
}
}
Java虚拟机会保证先执行父类的方法
接口中不能使用静态语句块,但仍然有静态变量初始化的赋值操作,因此接口与类一样都会生成方法。但接口与类不同的是,执行接口的方法不需要先执行父接口的方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的方法。
Java虚拟机必须保证一个类的方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行完毕方法。
“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作是在Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为“类加载器”(Class Loader)。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
public class Test {
public static void main(String[] args) throws Throwable {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is==null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.hls.codebase.learn.empty.config.Test").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.hls.codebase.learn.empty.config.Test);
}
}
输出:
com.hls.codebase.learn.empty.config.Test false
类型检查的时候返回了false是因为Java虚拟机中同时存在了两个ClassLoaderTest类,一个是由虚拟机的应用程序类加载器所加载的,另外一个是由自定义的类加载器加载的,虽然它们都来自同一个Class文件,但在Java虚拟机中仍然是两个互相独立的类。
站在Java虚拟机的角度来看,只存在两种不同的类加载器:
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
站在Java开发人员的角度来看,类加载器是三层类加载器、双亲委派的类加载架构,

各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
这样做好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
用以实现双亲委派的代码,全部集中在java.lang.ClassLoader的loadClass()方法:
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;
}
}
双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,
为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。
双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但是基础类型又要调用回用户的代码就没有办法了?
接口:java.sql.Driver,定义在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类,很明显java.sql包是由BootstrapClassloader加载器加载的;
而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的,DriverManager再获取链接的时候必然要加载到com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖。
SPI打破双亲委派机制:SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中,系统中抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。
面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。 为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。
而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定,jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。

以上截图展示了SPI使用的三要素:
“动态性”指的是:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。