zoukankan      html  css  js  c++  java
  • 从阿里巴巴面试题到java类加载机制

    首先很经典的阿里巴巴面试题 加上我自己的一些疑惑代码

    public class Text {
        public static int k = 0;
        public final int k1 = 3;    //自己加的
        public static Text t1 = new Text("t1");
        public static Text t2 = new Text("t2");
        public static int i = print("i");
        public static int n = 99;
        public int j = print("j");
        public final int a = 1; //自己加的
        public final static int b = 2;  //自己加的
    
        {
            print("构造块");
        }
        static {
            print("静态块");
        }
    
        public Text(String str) {
            System.out.println((++k) + ":" + str + "   i=" + i + "    n=" + n);
            ++i;
            ++n;
        }
    
        public static int print(String str) {
            System.out.println((++k) + ":" + str + "   i=" + i + "    n=" + n);
            ++n;
            return ++i;
        }
    
        public static void main(String args[]) {
            Text t = new Text("init");
        }
    }
    

    首先看一下代码执行过程

    可以看到首先debug停在了32行的print方法,然后看下线程的clinit的值

    init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。即是:init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。

    这里可以看出类加载执行步骤第一步:给类里所有的静态属性赋默认值,如果是final static修饰的基本类型或字符串,会在将对应值放入运行时常量池,属性则赋值为此ConstantValue。

    然后到下面就进入了init,说明调用了构造方法,此时k1 k2有值,而j a是默认值,说明init的时候先按照顺序依次给所有非静态属性直接赋值


    进入第一个断点33行,看到参数值是j,说明是给i赋值的时候调用了print方法,方法执行完之后,执行代码块的print方法,然后断点进入了构造方法

    执行完构造方法后继续走发现又进入了clinit,此时t1已经有了值

    这里可以得出第二步:静态属性默认值赋完之后,执行赋值的操作,此时如果有对象构造的方法,暂停clinit方法,进入对象构造init,先给对象所有非静态属性赋值和执行构造块(同级别),再执行构造方法,然后完成后将对象的地址赋给静态变量,继续执行clinit

    第三步:继续执行静态块代码,静态块代码和静态属性同级,依次执行。加载完静态属性和静态块后,类加载完毕,静态属性和类绑定的。

    第四步:类加载完之后,main方法执行,构造一个对象,执行init方法,先依次给对象非静态属性赋值,再执行构造方法。

    至此,这道题的答案已经有了,不是很难的知识点吧,那就再来点复杂的类加载机制。


    类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。

    当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。

    双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
    使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

    使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。

    双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。

    1、加载
    简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对戏那个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
    2、链接
    链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
    1)、验证
    验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
    格式验证:验证是否符合class文件规范
    语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
    操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
    
    2)、准备
    为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
    被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
    3)、解析
    将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
    可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)
    3、初始化
    将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
    所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。
    如果父类还没有被初始化,那么优先对父类初始化,但在<clinit>方法内部不会显示调用父类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的父类<clinit>方法已经被执行。
    JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
    
  • 相关阅读:
    在 Solaris 下有内核参数对应 TIME_WAIT 状态保持时间
    Properties获取属性
    jQuery Validate 动态添加验证
    解决struts2 action中double型在页面默认带有小数点的方法
    jsp、java下载附件
    方法参数数量不确定时应该怎么做?
    eclipse的java转web项目
    iBatis批量操作
    ORA19706和_external_scn_rejection_threshold_hours的前世今生
    Oracle官方书籍阅读顺序
  • 原文地址:https://www.cnblogs.com/sansamh/p/9924123.html
Copyright © 2011-2022 走看看