类的加载过程总的来说分为7个过程:加载,验证,准备,解析,初始化,使用,卸载,其中类的验证,准备,解析又称为连接阶段

java虚拟机规范并没有规定什么时候要进行加载阶段,但是规定了什么时候必须进行初始化阶段,故而初始化之前要进行加载,验证等阶段。 1,遇到new指令的时候,或调用类静态方法,又或者访问类的静态属性(被final修饰的字段除外,已经被放在常量池里面) 2,初始化子类的时候,发现父类还未初始化必须先初始化父类。 3,反射调用的时候 4,main方法所在的类,最先初始化。
例子1
/**
* 被动使用类字段演示一: 通过子类引用父类的静态字段,不会导致子类初始化
**/
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
} 该例子输出:
SuperClass init!
123 说明通过子类引用父类的静态字段,不会导致子类初始化
例子2
/**
*
* 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
**/
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization1 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
} 输出结果 没有输出“ConstClass init!
hello world 说明常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
接下来我们简单介绍下各个阶段: 加载阶段要完成3个步骤 1,通过全限定名找到类,并将类变成二进制字节流 2,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据 的访问入口(Class对象比较特殊,它虽然是对象,但是存放在方法区里面)
验证阶段 主要验证字节流的信息是否符合java虚拟机的规范验证魔数,版本等信息
准备阶段 主要为类变量(static修饰)分配内存空间,并设置初始值。类变量是存在方法区的,所以分配内存空间是在方法区进行。
public static int value=; 在该阶段value的值是0不是123,当在初始化阶段才会变成123。
public static final int value=; 被final修饰在准备阶段就已经是123
解析阶段 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化阶段 初始化阶段是真正执行java代码的阶段。 在准备阶段虚拟机已经为类变量赋予初始值,在初始化阶段才赋予程序员制定的值。 初始化阶段是执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
public class Test{
static{
i=0;//给变量赋值可以正常编译通过
System.out.print(i);//这句编译器会提示"非法向前引用"
}
static int i=1;
} 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。。。字段B的值将会是2而不是1
static class Parent{
public static int A=1;
static{
A=2;
}
}
static class Sub extends Parent{
public static int B=A;
}
public static void main(String[]args){
System.out.println(Sub.B);
} 静态代码块 构造代码块 构造方法执行顺序
class Parent1 {
static {
System.out.println("Parent--静态代码块");
}
{
System.out.println("Parent--构造代码块");
}
public Parent1() {
System.out.println("Parent--构造方法");
}
}
public class Child extends Parent1 {
static {
System.out.println("Child--静态代码块");
}
{
System.out.println("Child--构造代码块");
}
public Child() {
System.out.println("Child--构造方法");
}
public static void main(String[] args) {
new Child();
}
} 执行结果
Parent--静态代码块
Child--静态代码块
Parent--构造代码块
Parent--构造方法
Child--构造代码块
Child--构造方法 java虚拟机将加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。
类加载器只用于实现类的加载动作。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方 法、isInstance()方法的返回结果
从虚拟机的角度类加载器有两种:启动类的加载器,其他类的加载器 从开发人员的角度类加载器有三种:启动类的加载器,扩展加载器,应用程序类加载器 启动类加载器:负责将存放在<JAVA_HOME>\lib目录中的类库加载到虚拟机内存中 扩展加载器:负责将存放在<JAVA_HOME>\lib\ext目录中的类库加载到虚拟机内存中 应用程序类加载器:它负责加载用户类路径(ClassPath)上所指定的类库
双亲委派机制 类加载器之间如下图的这种层次关系,称为类加载器的双亲委派模型。

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。