zoukankan      html  css  js  c++  java
  • jvm--2.类加载机制


    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_HOMElib 目录中类库,加载到虚拟机内存中。
    启动类加载器,无法被java程序直接使用。

    II.扩展类加载器(Extension ClassLoader) ,负责加载 JAVA_HOMElibext 目录中的类库

    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程序的稳定性非常重要。


  • 相关阅读:
    LeetCode 4. Median of Two Sorted Arrays
    LeetCode 67. Add Binary
    LeetCode 66. Plus One
    Linux驱动之平台设备驱动模型简析(驱动分离分层概念的建立)
    Linux驱动之一个简单的输入子系统程序编写
    Linux驱动之输入子系统简析
    Linux驱动之定时器在按键去抖中的应用
    Linux驱动之同步、互斥、阻塞的应用
    Linux驱动之异步OR同步,阻塞OR非阻塞概念介绍
    Linux驱动之异步通知的应用
  • 原文地址:https://www.cnblogs.com/fubaizhaizhuren/p/5938477.html
Copyright © 2011-2022 走看看