深入java虚拟机学习 -- 类的加载机制(三)
类的初始化时机
在上篇文章中讲到了类的六种主动使用方式,反射是其中的一种(Class.forName(“com.jack.test”)),这里需要注意一点:当调用ClasLoader类的loadClass方法对类进行加载的时候,并不是对类的主动调用,不会导致类的初始化。
那么接下来我继续给大家2个例子,让我们来看看他们的执行结果分别是什么样的,看看你能猜对吗?
public class Test2 { public static void main(String[] args) { System.out.println(FinalTest2.x); } } class FinalTest2{ public static final int x=6/2; static { System.out.println("I am a final x"); } }
public class Test2 { public static void main(String[] args) { System.out.println(FinalTest2.x); } } class FinalTest2{ public static final int x=new Random().nextInt(); static { System.out.println("I am a final x"); } }
是不是看到结果很意外呢,2份代码看起来几乎是一模一样的,而且都是static修饰的,为什么和上篇文章讲的不一样了呢,为什么第一个demo里面的静态块没有执行呢? 下面让我们带着这一系列问题来解决下。
讲解
如果大家细心的话可以看到多了一个final修饰符,是的,结果的造成就是它在起作用。
public static final int x=6/2; 这行代码,java虚拟机在编译期就可以知道x的值是什么,因此在编译期就已经把3放到了常量池,所以在main方法中调用的时候不会触发类的初始化 即此时的x为编译期的常量
public static final int x=new Random().nextInt(); 这行代码中的x在编译期不能确定具体的值,需要等到运行的时候才能确定x的值,所以在运行时会触发类的初始化,即此时的x为编译器的变量
接口和父类
前面讲的类主动加载的7种方式,都是再说单个类的情况,下面我们来介绍下接口和父类
当Java虚拟机初始化一个类的时候,要求他的所有父类都已经被初始化,但是如果此类实现的有接口,则:
- 在初始化一个类的时候,并不会先初始化它所实现的接口
- 在初始化一个接口的时候,并不会先初始化它的父接口
因此,一个父接口并不会因为他的子接口或者实现类的初始化而初始化。只有当程序首次使用他的静态变量时,才会导致该接口的初始化。
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。这句话是什么意思呢?下面让我们看下这个Demo
public class Test3 { public static void main(String[] args) { System.out.println(Child.x); } } class Parent{ public static int x=3; static { System.out.println("this is a parent"); } } class Child extends Parent{ static { System.out.println("this is a Child"); } }
public class Test3 { public static void main(String[] args) { System.out.println(Child.x); } } class Parent{ public static int x=3; static { System.out.println("this is a parent"); } } class Child extends Parent{ public static int x=3; static { System.out.println("this is a Child"); } }
上面两个例子同样是相差一行代码,结果却差别很大,一个Child发生了初始化,另一个没有。
双亲委派模型
类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委派模型机制,这种机制能更好的保证Java平台的安全,在这种机制中,除了java虚拟机自带的根加载器以外(根加载器由c++实现,没有父加载器),其余的类加载器有且只有一个父加载器。当Java程序请求加载Loader1加载一个类的时候,loader1首先会委托自己的父加载器去加载这个类,若父加载器还有父加载器的话,依次类推,如果父加载器能加在,则有父加载器完成加载,否则才会有loader1本身加载。
需要注意的事,这里的加载器之间的父子关系实际上指的是加载器对象之间的包装关系,并不是类之间的继承关系。一对父子加载器可能是同一个加载器类的2个实例,也可能不是,只是在子加载器对象中包装了一个父加载器对象。
若有一个类加载器能成功加载Sample类,那么这个类加载器被成为定义类加载器,所有能成功返回Clas对象的引用的类加载器(包括定义类加载器)都被称为初始类加载器。
双亲委派模型的有点是能够提高软件系统的安全性。在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。例如:java.lang.Object类总是由根加载器加载,其他任何用户自定义的类加载器都不可能加载该类。
运行时包
由同一类加载器加载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看他们的包名是否相同,还要看定义类加载器是否相同。只有属于同一个运行时包的类才能相互访问包可见(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类去访问核心类库的包可见成员。
假设用户自定义了一个类:java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的加载器加载, 所以他们属于不同的运行时包,因此java.lang.Spy不能访问核心类库java.lang包中的可见成员。
我们下一篇来着重说下如何来实现一个自定义的类加载器