类的加载过程
1、类的加载
- 将 字节码文件( .class ) 读入到 JVM 所管理的内存中
- 将 字节码文件对应的类的数据结构 保存在方法区
- 最后生成一个与该类对应的 java.lang.Class 类型的对象 ( 在堆区 )
2、类的链接
- 连接是把已读入到内存的类的二进制数据合并到Java运行时环境(JRE)中去。
- 连接又分为三个阶段:验证、准备、解析。
- 验证:验保证类有正确的内部结构,并且与其它类协调一致如果JVM 检查到错误,就会抛出Error 对象。
类文件的结构检查: 确保文件遵循Java 文件的固定格式
语义检查: 确保类本身符合Java 语言的语法规定
字节码验证: 确保字节码流可以被JVM 安全地执行
- 准备: 在准备阶段,JVM 为类的静态变量分配内存,并设置默认值(byte 、short 、int 默认值都是 0,long 默认值是 0L,float 默认值是 0.0F,double 默认值是 0.0,boolean 默认值是 false,char 默认值是 u0000, 引用类型的默认值是 null)。
- 解析: 将符号引用解析为直接引用
3、类的初始化
- 初始化阶段,JVM执行类的初始化语句,为静态变量赋予初始值
- 静态变量的初始化途径:在静态变量的声明处进行初始化,在静态代码块中进行初始化
- 初始化代码可能是(声明变量时的赋值语句): protected static int a = 100 ;也可以是(静态代码块):
static {
a = 10000 ;
}
- 类初始化的一般步骤
如果该类还没有被加载和连接,那么先加载和连接该类
如果该类存在直接父类,但该父类还未初始化,则先初始化其直接父类
如果该类中存在初始化语句,则依次执行这些初始化语句
- 类的初始化时机
JVM 只有在首次主动使用某个类或接口时才会初始化它
被动使用不会导致本类的初始化
诡异的问题 解析测试案例:
package ecut.classloader;
public class Sun {
protected static int a = 100 ;//链接(准备):0//初始化: a:100
protected static int b ;//链接(准备):0//初始化: b:0
protected static Sun instance = new Sun() ;//链接(准备):null//初始化: a:101 b:1
public Sun() {
a++ ;
b++ ;
}
}
package ecut.classloader;
public class Moon {
protected static Moon instance = new Moon() ;//链接(准备):null//初始化: a:1 b:1
protected static int a = 100 ;//链接(准备):0//初始化: a:100
protected static int b ;//链接(准备):0//初始化: b:1
public Moon() {
a++ ;
b++ ;
}
}
4、类的使用
主动使用会导致类被初始化
- 创建类的实例 ( new 、反射、反序列化 、克隆 )
- 调用类的静态方法
- 访问类 或 接口的 静态属性 ( 非常量属性 ) ( 取值 或 赋值 都算 )
- 访问类 或 接口 的 非编译时常量,也将导致类被初始化:public static final long time = System.currentTimeMillis();
- 调用反射中的某个些方法,比如 Class.forName( "edu.ecut.Student" );
- 初始化某个类时,如果该类有父类,那么父类将也被初始化
- 被标记为启动类的那些类(main)
被动使用不会导致类被初始化
- 程序中对编译时常量的使用视作对类的被动使用
- JVM初始化某个类时,要求其所有父类都已经被初始化,但是 该规则不适用 于 接口 类型
- 只有当程序访问的静态变量或静态方法的确在当前类或接口定义时,才能看作是对类或接口的主动使用,比如使用了 Sub.method() ,而 method() 是继承自 Base ,则只初始化 Base 类
- 调用 ClassLoader 的 loadClass( ) 加载一个类,不属于对类的主动使用
5、类的卸载:当一个类不再被任何对象所使用时,JVM会卸载该类。
类加载器的双亲委派模型
1、父亲委托机制
- 设loader 要加载A 类,则loader 首先委托自己的父加载器去加载A 类,如果父加载器能加载A 类则由父加载器加载,否则才由loader 本身来加载A类。
- 这种机制能更好地保证Java 平台的安全性
- 父亲委托机制中,每个类加载器都有且只有一个父加载器,除了JVM 自带的根类加载器( Bootstrap Loader )
2、模型示意图
根类加载器(BootstrapLoader)
- 负责加载虚拟机的核心类库,比如java.lang.* 等
- 从系统属性sun.boot.class.path 所指定的目录中加载类库
- 该加载器没有父加载器,它属于JVM 的实现的一部分(用C++实现)
扩展类加载器(ExtClassLoader)
- 其父加载器为BootstrapLoader 类的一个实例
- 该加载器负责从java.ext.dirs 系统属性所指定的目录中加载类库或者从JDK_HOME/jre/lib/ext 目录中加载类库
- 该加载器对应的类是纯Java 类,其父类是java.lang.ClassLoader
系统类加载器(AppClassLoader)
- 也称作应用类加载器,其父加载器默认为ExtClassLoader 类的一个实例
- 负责从CLASSPATH 或系统属性java.class.path 所指定的目录中加载类库
- 它是用户自定义类加载器的默认父加载器
- 其父类也是java.lang.ClassLoader
过程
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的来加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的启动类加载器
- 只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)的时候,子加载器才会尝试自己去加载
使用这种模型的好处
- java类随着它的来加载器一起具备了一种带有优先级的层次关系,对于保证java程序的稳定运作很重要
- 代码集中在java.lang.ClassLoader的loadClass()方法中。
牛客网笔试题
1、What will happen when you attempt to compile and run the following code?
public class Test{ static{ int x=5; } static int x,y; public static void main(String args[]){ x--; myMethod( ); System.out.println(x+y+ ++x); } public static void myMethod( ){ y=x++ + ++x; } }
正确答案: D
A、compiletime error
B、prints:1
C、prints:2
D、prints:3
E、prints:7
F、prints:8
2、有关静态初始化块说法正确的是?
正确答案: A B C
A、无法直接调用静态初始化块
B、在创建第一个实例前或引用任何静态成员之前,将自动调用静态初始化块来初始化
C、静态初始化块既没有访问修饰符,也没有参数
D、在程序中,用户可以控制合适执行静态初始化块
3、子类A继承父类B, A a = new A(); 则父类B构造函数、父类B静态代码块、父类B非静态代码块、子类A构造函数、子类A静态代码块、子类A非静态代码块 执行的先后顺序是?
正确答案: C 你的答案: C (正确)
A、父类B静态代码块->父类B构造函数->子类A静态代码块->父类B非静态代码块->子类A构造函数->子类A非静态代码块
B、父类B静态代码块->父类B构造函数->父类B非静态代码块->子类A静态代码块->子类A构造函数->子类A非静态代码块
C、父类B静态代码块->子类A静态代码块->父类B非静态代码块->父类B构造函数->子类A非静态代码块->子类A构造函数
D、父类B构造函数->父类B静态代码块->父类B非静态代码块->子类A静态代码块->子类A构造函数->子类A非静态代码块
4、下面代码的输出是什么?
public class Base { private String baseName = "base"; public Base() { callName(); } public void callName() { System. out. println(baseName); } static class Sub extends Base { private String baseName = "sub"; public void callName() { System. out. println (baseName) ; } } public static void main(String[] args) { Base b = new Sub(); } }
正确答案: A
A、null
B、sub
C、base
解析:
- new Sub();在创造派生类的过程中首先创建基类对象,然后才能创建派生类。
- 创建基类即默认调用Base()方法,在方法中调用callName()方法,由于派生类中存在此方法,则被调用的callName()方法是派生类中的方法,此时派生类还未构造,所以变量baseName的值为null
5、What will be printed when you execute the following code?
class C { C() { System.out.print("C"); } } class A { C c = new C(); A() { this("A"); System.out.print("A"); } A(String s) { System.out.print(s); } } class Test extends A { Test() { super("B"); System.out.print("B"); } public static void main(String[] args) { new Test(); } }
正确答案: B
A、BB
B、CBB
C、BAB
D、None of the above
转载请于明显处标明出处