深入理解java虚拟机(四)

(一)概述:

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

注:解析阶段在某些情况下可以在初始化之后再开始,这是为了支持java的动态绑定。

  5种情况对必须立即对类初始化:

      1.遇到new,getstatic,putstatic或invokestatic这四条字节码指令。

      2.使用java.lang.reflect包的方法对类进行反射调用的时候

      3.当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化

      4.包含main方法的那个类会先初始化

      5.如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putstatic,REF_invokestatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化


 

(二)具体过程:

     1.加载:

                  (1)通过一个类的全限定名来获取定义此类的二进制流

                  (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

                   (3)在内存中生成一个代表这个类的java.lang.class 对象,作为方法区这个类的各种数据访问入口

        注:加载阶段于连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

         JVM在加载数组的时候加载的仅仅是数组的类型类(例如String[] 加载器只会加载String这个类型类),而数组的创建则由JVM直接完成。

        1. JVM为什么只加载数组的类型类
          我认为JVM这样做的目的主要是为了节省时间,我们知道数组里面装的都是同一种类型的元素,JVM没必要将一个重复的内容加载多次浪费时间。
2. N维数组怎么加载
          如果是N维数组,类加载器会从最外层开始一层一层的递归加载,直到加载到非数组类型为止。
3. 引用类型与基本类型加载起来会不会有区别
         其实基本类型早已经在javac阶段装箱成封装对象了,例如int会被装箱成Integer,long装箱成Long等等,所以是没有区别的。

来源:https://blog.csdn.net/qq_23191031/article/details/81838211

   2.验证:

                  目的:为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

                  过程:

                               (1)文件格式验证:

                                            1.是否以魔数0xCAFEBABE开头

                                            2.主次版本号是否在当前虚拟机的处理范围内

                                            3.常量池的常量是否有不被支持的常量类型

                                             ..................

                                (2)元数据验证:

                                            1.这个类是否有父类

                                           2.这个类是否继承了不被允许继承的类

                                           3.类中字段,方法是否与父类产生矛盾

                                 (3)字节码验证:

                                 (4)符号引用验证

3.准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都要在方法区进行分配。

               注:

                  进行内存分配的仅包括类变量(即被static修饰的)不包括实例变量,实例变量被分配到java堆中。并且准备阶段过后的初始值为0

4.解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

5.初始化:到了初始化阶段才开始真正执行java程序的代码

                  初始化阶段是执行类构造器<clint>方法的过程

                   <clinit>方法:

                         1.clinit方法是由编译器自动收集类中所有变量的赋值动作和静态语句块的语句合并而成的

                        2.clinit方法与类的构造函数不同,它不需要显示的调用父类构造器。虚拟机会保证子类的<clinit>方法执行之前,父类<clinit>已经执行

                        3.父类的静态语句块要优于子类变量的赋值操作

                        4.clinit方法不是必须的

                        5.接口中不能使用静态语句块,但接口中仍然有变量初始化的赋值操作,接口也有clinit方法

                        6.如果多个线程同时去初始化一个类,那么只有一个线程会去执行<clinit>其他的都得等待

         

有这样一道面试题:

class Singleton{
    private static Singleton singleton = new Singleton();
    public static int value1;
    public static int value2 = 0;

    private Singleton(){
        value1++;
        value2++;
    }

    public static Singleton getInstance(){
        return singleton;
    }

}

class Singleton2{
    public static int value1;
    public static int value2 = 0;
    private static Singleton2 singleton2 = new Singleton2();

    private Singleton2(){
        value1++;
        value2++;
    }

    public static Singleton2 getInstance2(){
        return singleton2;
    }

}

public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1);
        System.out.println("Singleton1 value2:" + singleton.value2);

        Singleton2 singleton2 = Singleton2.getInstance2();
        System.out.println("Singleton2 value1:" + singleton2.value1);
        System.out.println("Singleton2 value2:" + singleton2.value2);
    }

 

说出运行的结果:
Singleton1 value1 : 1
Singleton1 value2 : 0
Singleton2 value1 : 1
Singleton2 value2 : 1

1 首先执行main中的Singleton singleton = Singleton.getInstance();
2 类的加载:加载类Singleton
3 类的验证
4 类的准备:为静态变量分配内存,设置默认值。这里为singleton(引用类型)设置为null,value1,value2(基本数据类型)设置默认值0
5 类的初始化(按照赋值语句进行修改):
执行private static Singleton singleton = new Singleton();
执行Singleton的构造器:value1++;value2++; 此时value1,value2均等于1
执行
public static int value1;
public static int value2 = 0;
此时value1=1,value2=0

(三)类加载器

    1 概念:  把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

   2 类与类加载器:对于任意一个类,都需要由加载它的类加载器和类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即比较两个类是否相等,只有这两个类是由同一个类加载的前提下才有意义。

   3.双亲委派模型:

     (1)类加载器的分类:

              1.启动类加载器:负责加载存放在<JAVA_HOME>lib目录下的,或者被-Xbootclasspath参数所指定的路径中的,并且是被虚拟机所识别的类库加载到虚拟机当中。

             2.扩展类加载器:负责加载<JAVA_HOME>\hb\ext目录下 ,或者被java.ext.dirs系统变量所指定的路径中的所有路径

             3.应用程序类加载器:由于这个类是CLASSLoader中的getSystemClassLoader()方法的返回值,所以一般也称为系统类加载器。它负责加载用户类路径上指定的类库。一般情况下这个就是程序默认的类记载器。

   (2)模型

         

 

   工作 过程:如果一个类收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的启动类加载器当中,只有当父类无法完成加载请求时,子加载器才会尝试自己去加载。

父类加载器已经加载过的类,不用再次加载,而且对于一些系统类,用户自定义的不起作用了,有一定安全保证

例题1

请看下面的题目,下面代码输出的结果是什么?


public class Student {

    static int age = 22;

    public Student(){
        System.out.println("无参构造方法 age = "+ age);
    }

    {

        System.out.println("普通代码块");
    }

    static {
        System.out.println("静态代码块");
    }

    public static void main(String[] args) {
       Student student = new Student(); System.out.println("main方法");
    }
}

例题1分析

输出结果如下

静态代码块
普通代码块
无参构造方法 age = 22
main方法

 

当我们的程序要执行main方法的时候,要先对该类进行初始化,在上面的类中有两个方法,一个构造方法和一个main方法,在java类编译成字节码文件的时候,字节码中只有类初始方法和对象初始化方法这两个概念,我们可以根据以下来区分

    类初始化方法:顾名思义,类初始化方法会在类初始化的时候执行,IDE会按顺序手机变量的赋值语句,静态代码块,组合成类初始化方法

如上面的Student类,下面两个语句将组成类初始化方法

 static int age = 22;
 static {
        System.out.println("静态代码块");
    }

 

    对象初始化方法:在对象实例化的时候才会执行,IDE会按顺序手机成员变量的赋值语句,普通代码块,最后收集构造函数代码组成对象初始化方法

例题2


package jvmlearn.basetype;


class A {
    static {
        System.out.println("A的静态块");
    }
}

class B extends A {
    public static int  bbb = 10;
    static {
        System.out.println("B的静态块");
    }
    public  B(){
        System.out.println("B的构造方法");
    }
}

class  C extends B{
    static {
        System.out.println("C的静态代码块");
    }
    public C(){
        System.out.println("C的构造方法");
    }
}

public class LoadDemo {
    public static void main(String[] args) {
        System.out.println("B的参数bbb="+C.bbb);
    }
}
   

例题2分析

输出结果如下

A的静态块
B的静态块
B的参数bbb=10

 

你可能会疑惑为什么C的静态代码块这句话没输出,这里要注明一下,jvm在加载类的过程中,对于静态字段只有直接定义这个字段的类才会被初始化,而不会触发子类的初始化(执行类初始化方法)

具体过程可以描述如下:
1. main方法调用C.bbb
2. bbb在B类定义,初始化B类,不初始化C类
3. 初始化B类的时候发现A类没有被初始化,去初始化A类
4. 初始化A类,输出A的静态代码块
5. 初始化完A类后,继续初始化B类,输出B的静态代码块
6. 执行main方法,输出B的参数bbb=10
例题3

package jvmlearn.basetype;


class A {
    static {
        System.out.println("A的静态块");
    }
    public A(){
        System.out.println("A的构造方法");
    }
}

class B extends A {
    public static int  bbb = 10;
    static {
        System.out.println("B的静态块");
    }
    public  B(){
        System.out.println("B的构造方法");
    }
}

class  C extends B{
    static {
        System.out.println("C的静态代码块");
    }
    public C(){
        System.out.println("C的构造方法");
    }
}

public class LoadDemo {
    public static void main(String[] args) {
       new C();
    }
}

例题3分析

结果输出

A的静态块
B的静态块
C的静态代码块
A的构造方法
B的构造方法
C的构造方法

 

加载过程可以描述为如下:
1. 执行main方法,构造C对象的实例
2. 初始化C,发现B没初始化,去初始化B
3. 初始化B,发现A没初始化,去初始化A
4. 输出A的静态块
5. A初始化完成,初始化B,输出B的静态块
6. B初始化完成,初始化C,输出C的静态代码块
7. 执行C的构造方法,调用父类B的构造方法
8. 执行B的构造方法,调用A的构造方法
9. 执行A的构造方法,调用object的构造方法,然后输出A的构造方法
10. 执行B的构造方法,输出B的构造方法
11. 执行C的构造方法,输出C的构造方法
例题4


class A {


   static A a = new A();

   static {
       System.out.println("A的静态块");
   }

    {
        System.out.println("A的普通代码块");
    }
    public  A(){
        System.out.println("A的构造方法");
        System.out.println("aaa = "+aaa +" ; bbb = "+bbb);
    }
    public static void staticMethod(){
        System.out.println("A的静态方法");
    }
    int aaa = 1;
    static int bbb = 2;


}

public class LoadDemo {
    public static void main(String[] args) {
        A.staticMethod();
    }
}

 

例题4分析

输出结果

A的普通代码块
A的构造方法
aaa = 1 ; bbb = 0
A的静态块
A的静态方法

过程分析

这一题我们可以这么来分析,上面我们说过,java虚拟机会把字节码解析成初始化方法和实例化方法,本例中的初始化方法和示例方法分别如下

    类初始化方法


    static A a = new A();
    static {
       System.out.println("A的静态块");
    }
    static int bbb = 2; 

    类实例化方法


   {
        System.out.println("A的普通代码块");
    }
    int aaa = 1;
    public  A(){//构造方法在最后
        System.out.println("A的构造方法");
        System.out.println("aaa = "+aaa +" ; bbb = "+bbb);
    }

 

执行步骤

    入口是main方法,对A类进行了实例化
    初始化A类,执行类初始化方法
    类初始化第一句为static A a = new A(),A被实例化,jvm会去执行A的实例化方法
    执行普通代码块,输出A的普通代码块
    初始化aaa = 1
    执行A的构造方法,输出A的构造方法,bbb的值在准备阶段初始化为0,aaa被初始化为1,输出aaa = 1 ; bbb = 0;
    实例化方法执行完毕,继续执行类初始化方法
    类初始化方法继续往下执行,下一行为执行静态代码块,输出A的静态代码块
    然后为bbb赋值2
    类的初始化完成,执行main中的方法,输出A的静态方法

 


https://blog.csdn.net/x1032019725/article/details/81301614  
 

 

 

面试题:

1.

类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,他们的执行顺序

答:先静态、先父后子。
先静态:父静态 > 子静态
优先级:父类 > 子类 静态代码块 > 非静态代码块 > 构造函数
一个类的实例化过程:
1,父类中的static代码块,当前类的static
2,顺序执行父类的普通代码块
3,父类的构造函数
4,子类普通代码块
5,子类(当前类)的构造函数,按顺序执行。
6,子类方法的执行,

 
 2.

自定义类加载的意义:

  1. 加载特定路径的class文件
  2. 加载一个加密的网络class文件
  3. 热部署加载class文件

如何⾃定义⼀个类加载器?你使⽤过哪些或者你在什么场景下需要⼀个⾃定义的类加载器吗?

 

参考:https://blog.csdn.net/noaman_wgs/article/details/74489549

             https://blog.csdn.net/qq_23191031/article/details/81838211

             《深入理解java虚拟机》

        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值