3.JVM类加载机制
(1)类加载机制
虚拟机把描述类的数据从Class文件,用ClassLoader ,加载到内存,并对数据进行校验、转换解析和初始化,最终形成虚拟机直接使用的java类型,
这就是虚拟机的类加载机制。
(2)在java语言里面,类型的加载、连接、初始化过程都是在程序运行期间完成的,
缺点:增加性能开销
优点:提高程序灵活性
(3)类的生命周期
a.加载
b.连接(包括:验证,准备,解析)
c.初始化
d.使用
e.卸载(出内存)
类的加载过程必须按照这五个步骤,按部就班的 开始 。
c.初始化
什么情况下,会开始加载?JVM规范并没有强制性约束,但是有五种情况,必须立刻对类进行初始化(加载连接必然要在初始化之前开始)
I. 遇到,new , getstatic , putstatic , invokestatic四条字节码指令时,如果没有进行过初始化,则需要先触发其初始化
这四条字节码指令使用场景:
new: 实例化对象
getstatic: 读取一个类的静态变量
putstatic: 设置一个类的静态变量
invokestatic: 调用一个类的静态方法的时候
II.使用java.lang.reflect包,对类进行反射调用的时候,如果类没有过初始化,则对其初始化。
III.当初始化一个类的时候,发现其父类还没有过初始化,需要初始化
IV.JVM启动时,main()方法所在的类
V.当使用JDK1.7的动态语言支持时,方法句柄对应的类没有过初始化,则对其初始化
(Test3继承Test2,当调用Test3的静态变量时,先初始化Test3; Test2也有静态变量a,Test3继承Test2 , 调用Test3.a时候,
只会初始化Test2,不会初始化Test3 , 至于会不会加载和连接Test3 ,取决于JVM的具体实现,Sun HotSpot虚拟机,
可通过XX:+TraceClassLoading观察到,此操作会导致子类的加载 )
Test2 [] test = new Test2[10]; 不会初始化Test2
c是Test的一个常量,在常量池里面,所以,调用Test2.c,也不会初始化Test2
当一个类引用了另一个类的常量时,编译时,会把被引用的常量,转化为引用者自己常量池中的常量,所以,编译完后,两个类就没有关系了
(4)类加载过程
a. 加载 ,是类加载(Class Loading)过程中的一个阶段。三个阶段:
I.通过一个类的全限定名,来获取此类的二进制字节流。
II.将字节流所代表的静态存储结构,转化为方法区的运行时数据结构。
III.在内存中生成一个代表这个类java.lang.Class对象,作为方法区这个类的各种数据访问入口。
(数组不是类加载器创建,是由JVM自己创建的。
对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是,存放在方法区里面。这个对象将作为,程序访问方法区中数据的外部接口。
)
b.连接
I.验证:确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机。
这个阶段是否严禁,直接决定了java虚拟机能否承受恶意代码的攻击。
只有通过了验证,字节流才会存入方法区,所以验证早于加载的存储。
II.准备:准备阶段是正式为类变量(static),分配内存,并设置变量的初始值,这些变量所使用的内存都将在方法区进行分配。
第一,这个时候进行内存分配的仅仅是类变量(static),不包括实例变量。实例变量将会在对象初始化时候分配在堆中。
第二,这里所说的初始值,通常情况下是类型0值,即public static int a = 123 , 此时只为a分配a=0。
第三,如果指定了public final static int a = 123,此时,为a初始化a=123。
III.解析:解析阶段是,虚拟机将常量池内的符号引用,替换为直接引用的过程。
解析动作主要针对 类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
c.初始化 , 初始化是类加载过程的最后一步,初始化阶段,才真正开始执行,类中定义的java程序代码。
I. 初始化,是执行类构造器clinit()方法的过程,而非类的构造方法。
II. client()方法,是编译器收集类中所有类变量(static) 的赋值动作,和静态方法块中的语句合并产生的。
编译器收集的顺序,是由语句在源文件中的顺序决定的。
III.静态语句块中,只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态块中可以赋值,但是不能访问。
static{
i = 0;//可以执行
System.out.print(i);// 会提示非法向前引用
static int i =1;
}
public Class Parent{
public static int A = 1;
static {
A = 2;
}
}
public Class Child extends Parent{
public static int B = A;
}
public class Test{
public static void main(String [] args){
System.out.println(Child.B); //
}
}
上面代码会打印2,因为clinit()方法执行顺序,由语句在源文件中的顺序决定的。
IV.clinti()方法,与类的构造函数不同,他不需要显示调用父类构造器,JVM会保证在子类的clinit方法执行前执行。
V.clinit()方法对于类或者接口来说不是必须的。如果一个类没有静态块,也没有赋值操作,就不用clinit()方法。
4.类加载器
(1)
JVM设计团队,把类加载阶段中“通过一个类的全限定名,来获取描述此类的二进制字节流” 这个动作,放到JVM外部,以便让
应用程序自己决定如何去获取所需要的类。实现这个动作的代码块,称为类加载器。
代码:
public static void main(String args []) throws InstantiationException, IllegalAccessException, ClassNotFoundException{
ClassLoader myLoader = new ClassLoader() {
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()];
return defineClass(name , b , 0 , b.length);
}catch(IOException e){
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
}
代码构造了一个简单的类加载器,使用这个类加载器加载了一个名为"com.jvm.ClassLoaderTest" 的类,并实例化这个类的对象,
从第一句打印可以看出这个对象确实是com.jvm.ClassLoaderTest 实例化出来的。
但是,从第二句发现,这个对象对com.jvm.ClassLoaderTest 做所属类型检查的时候返回了false,这是因为在jvm中存在了两个 ClassLoaderTest类,
一个是由系统应用程序加载的,一个是由自定义类加载器加载的。
虽然他们来自同一个Class文件,但是,是两个独立的类。
所以,对于任何一个类,都需要由加载他的类加载器,和 这个类本身一同确定其在JVM中的唯一性,每一个类加载器都有一个独立的命名空间。
所以,比较两个类是否相等,只有在这两个类是否由同一个类加载器加载的前提下才有意义,否则,即使这两个类源自同一个Class文件,被同一个JVM加载,
只要加载他们的类加载器不同,那这两个类必定不相等。
(2)双亲委派模型
a.从JVM角度来看,只存在两种类加载器:
I.启动类加载器(Bootstrap ClassLoader) ,使用C++语言实现,是虚拟机自身的一部分。
II.所有其他类加载器,都由JAVA语言实现,独立于虚拟机外部,并且全部继承抽象类java.lang.ClassLoader
b.从开发人员的角度来看,会使用3中系统提供的类加载器
I.启动类加载器(Bootstrap ClassLoader),负责加载 JAVA_HOME\lib 目录中类库,加载到虚拟机内存中。
启动类加载器,无法被java程序直接使用。
II.扩展类加载器(Extension ClassLoader) ,负责加载 JAVA_HOME\lib\ext 目录中的类库
III.应用程序类加载器(Application ClassLoader)又称系统类加载器, 负责加载 javaBuildPath指定的类库,
如果程序中没有指定类加载器,一般情况下,这个就是程序中的类加载器。
VI.自定义类加载器。
c.类加载器关系
启动类加载器(Bootstrap ClassLoader)
^
|
|
扩展类加载器(Extension ClassLoader)
^
|
|
应用程序类加载器(Extension ClassLoader)
^
|
|
自定义类加载器(User ClassLoader)
d.这种层次关系模型,称为类加载器的双亲委派模型。在JDK1.2被引入
双亲委派模型,要求,除了顶层的 启动类加载器 之外,其余的类加载器,都应有自己的父类加载器。这里的父子关系一般不用继承,而是使用组合(Composition)
关系,来复用父类加载器的代码。
e.双亲委派模型,工作过程:
I.如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成。每一个层次类加载器都是如此。
II.当父类反馈自己无法完成这个加载请求时,子加载器才会尝试自己去尝试加载。
f.好处:JAVA类随着它的类加载器一起具备了,一种带有优先级的层次关系。
例如:java.lang.Object类,存放在rt.jar中,无论哪一个类加载器加载这个类,都是为派给最顶端的Bootstrap ClassLoader加载,因此Object类在程序各种
类加载器环境中都是同一个类。
相反:如果没有双亲委派模型,如果用户自己编写了称为java.lang.Object类,并放在程序的ClassPath中,那么,系统中将会出现多个不同的Object类,Java类型
体系中最基本的行为都无法保证。
综上,双亲委派模型,对于java程序的稳定性非常重要。