java的类加载机制以及加载模型

版权声明:本文为博主原创文章,未经博主允许不得转载。学习交流QQ群: 173124648 https://blog.csdn.net/u013126379/article/details/77483003

1、概述

我们先来看下下面的java代码:(大家感觉会输出什么结果呢?)
public class TestClass {
	static
    {
        System.out.println("TestClass");
    }
	
	 public static void main(String[] args)
	    {
	        System.out.println(SubClass.value);
	    }
}
 class SuperClass extends TestClass
{
    static
    {
        System.out.println("SuperClass init!");
    }
 
    public static int value = 123;
 
    public SuperClass()
    {
        System.out.println("init SuperClass");
    }
}
 class SubClass extends SuperClass
{
    static
    {
        System.out.println("SubClass init");
    }
 
    static int a;
 
    public SubClass()
    {
        System.out.println("init SubClass");
    }
}
运行的结果:
TestClass
SuperClass init!
123

2、类加载的过程

整个类从加载到虚拟机内存开始,再到卸载出内存结束的整的过程中包含了以下生命周期:
加载、验证、准备、解析、初始化、使用、卸载
其中验证、准备、解析三个阶段统称为连接。



2.1 加载
当一个类在加载的过程中需要以下操作:
1)通过一个类 的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2.2 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
1)文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
2)元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
4)符号引用验证:确保解析动作能正确执行。

2.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会行。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

2.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

2.5 初始化

类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备极端,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主管计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程.<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:
public class Test
{
    static
    {
        i=0;
        System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
    }
    static int i=1;
}
<clinit>()方法与实例构造器<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类<init>()方法执行之前,父类的<clinit>()方法方法已经执行完毕,回到本文开篇的举例代码中,结果会打印输出:TestClass就是这个道理。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
<clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有好事很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。
我们看下面的代码:

public class TestClass3 {
	public static void main(String[] args) {
		Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                A dlc = new A();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
	}

	
	static class A{
		static
        {
            if(true)
            {//死循环,将造成其他线程阻塞
                System.out.println(Thread.currentThread()+"init A");
                while(true)
                {
                }
            }
        }
		
	}
}

运行结果:
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init A
我们对上面的代码进行修改后,看下:
public class TestClass3 {
	public static void main(String[] args) {
		Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                A dlc = new A();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
	}

	
	static class A{
		static
        {
            if(true)
            {
                System.out.println(Thread.currentThread()+"init A");
                try {//让该线程睡眠5s
					TimeUnit.SECONDS.sleep(5);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
            }
        }
		
	}
}

运行结果:
Thread[Thread-0,5,main] start
Thread[Thread-0,5,main]init A对象
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over

综上:我们发现innt A对象只输出了一次,所以说如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。


猜你喜欢

转载自blog.csdn.net/u013126379/article/details/77483003