JVM类加载机制

类的加载过程

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

转载请于明显处标明出处

http://www.cnblogs.com/AmyZheng/p/8647217.html

原文地址:https://www.cnblogs.com/AmyZheng/p/9412114.html