• JVM入门--类加载器


    一、基础架构

    概览

    img

    我们平时说的栈是指的Java栈,native method stack 里面装的都是native方法

    细节架构图

    img

    1589163380073


    二、类加载器

    1、类的加载

    1587995522913

    img

    • 方法区并不是存放方法的区域,其是存放类的描述信息(模板)的地方
    • Class loader只是负责class文件的加载,相当于快递员,这个“快递员”并不是只有一家,Class loader有多种
    • 加载之前是“class”,加载之后就变成了“Class”,这是安装java.lang.Class模板生成了一个实例。“Class”就装载在方法区,模板实例化之后就得到n个相同的对象
    • JVM并不是通过检查文件后缀是不是.class来判断是否需要加载的,而是通过文件开头的特定文件标志1587995665988

    2、类的加载过程

    img

    注意:加载阶段失败会直接抛出异常

    2.1、加载

    ​ 把.class文件读入到java虚拟机中

    • 通过“类全名”来获取定义此类的二进制字节流

    1587997991771

    ​ 动态编译:jsp-->java-->class

    • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构

    • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

    2.2、链接

    1. 验证
    • 确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

    • 验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

    2. 准备
    • 类变量(静态变量)分配内存并设置类变量默认值-->0/false/null(不包含final修饰的static,final修饰的变量会显示初始化
    • 在初始化之前,若使用了类变量,用到的是默认值,并非代码中赋值的值
    • 不会实例变量分配初始化、类变量分配在方法区中,实例变量会随对象分配到java堆中
    3. 解析

    ​ 虚拟机常量池内的符号引用替换为直接引用 ,类名、字段名、方法名--->具体内存地址或偏移量

    2.3、初始化

    1. 主动/被动使用

    2. 初始化注意点
    • 类变量被赋值、实例变量被初始化

    • 每个类/接口被Java程序首次主动使用的时候才会被java虚拟机初始化

    • 从上到下初始化

    • 初始化一个类时,要求它的父类都已经被初始化了除接口

      • 当初始化一个类的时候并不会先初始化它实现的接口

      • 当初始化一个接口的时候,并不会初始化它的父接口

        一个父接口并不会因为它的子接口或实现类的初始化而初始化,只有当首次使用其特定的静态变量时(即运行时常量,如接口中引用类型的变量)时才会初始化

    3. 深入理解举例1
    • 对于静态字段来说,只有直接定义了该字段的类才会被初始化
    • 每个类在初始化前,必须先初始化其父类(除接口)
    • 追踪类的加载情况:-XX:+TraceClassLoading(+表示开启,-表示关闭)
    • 对于常量(这里指编译器确定的常量)来说,常量值在编译阶段会存入到调用它的方法所在的类的常量池中,本质上调用类没有直接引用到定义常量的类
    • 对于引用类型数组来说,其类型是由JVM在运行期间动态生成的,表示为[L+自定义类全类名(一维)这种形式
    • 准备阶段只是分配内存、赋默认值,初始化阶段才是真正的赋值(自己设定的值)
      • 初始化阶段是从上到小初始化赋值
    public class ClassLoaderTest {
        public static void main(String[] args) {
            //单独测试下列语句
            //1.
            System.out.println(Child.str1);
            /*输出
            * Parent static block
            * hello I'm Parent
            */
            //2.
            System.out.println(Child.str2);
            /*输出
            * Parent static block
            * Child static block
            * hello I'm Child
            */
            //3.
            System.out.println(Parent.str3);
            /*输出
            * hello I'm Parent2
            * */
            //4.
            System.out.println(Parent.str4);
            /*输出
            * Parent static block
            * 78f59c0d-b91c-4e32-8109-dec5cb23aa13
            * */
            //5.
            Parent[] parents1=new Parent[1];
            System.out.println(parents1.getClass());
            Parent[][] parents2=new Parent[2][2];
            System.out.println(parents2.getClass());
            /*输出
            * class [Lcom.lx.Parent;
            * class [[Lcom.lx.Parent;
            * */
            //6.
            System.out.println(Singleton1.count1);
            System.out.println(Singleton1.count2);
            System.out.println(Singleton2.count1);
            System.out.println(Singleton2.count2);
            /*输出
            * 1,1,1,0
            * */
            
        }
    }
    class Parent{
        public static  String str1 = "hello I'm Parent";
    	public static final String str3 = "hello I'm Parent2";
        public static final String str4 = UUID.randomUUID().toString();
    
        
        static {
            System.out.println("Parent static block");
        }
    }
    class Child extends Parent{
        public static  String str2 = "hello I'm Child";
    
        static {
            System.out.println("Child static block");
        }
    }
    
    class Singleton1 {
        public static int count1;
        public static int count2=0;
        public static Singleton1 singleton1=new Singleton1();
    
        public Singleton1() {
            count1++;
            count2++;
        }
    
        public Singleton1 getInstance(){
            return singleton1 ;
        }
    }
    class Singleton2 {
        public static int count1;
        public static Singleton2 singleton2=new Singleton2();
    
        public Singleton2() {
            count1++;
            count2++;
        }
    
        public static int count2=0;
    
        public Singleton2 getInstance(){
            return singleton2 ;
        }
    }
    
    4. 结果分析
    1. Child属于被动使用,Parent是主动使用,所以只会初始化Parent
    2. Child属于主动使用,所以会初始化Child,由于初始化的类具有父类所以先初始化父类
    3. Parent并没有被使用到,str3的值在编译期间就被存入CLassLoaderTest这个调用它的方法所在的类的常量池中,与Parent无关
    4. str4不是编译期间就能确定的常量,就不会放到调用方法类的常量池中,在运行时主动使用Parent类进而需要初始化该类
    5. 没有对Parent类初始化,引用数组类型并非Parent类,而是jvm动态生成的class [Lcom.lx.Parent
    6. 首先访问Singleton的静态方法--》Singleton是主动使用--》先初始化
      1. 第一种:准备阶段给count1,2分配空间默认值已经为0了,此时给类变量singleton初始化,调用构造方法,分别加一
      2. 第二种:同上,但是在给singleton初始化时,count2并未初始化,自增只是暂时的,随后就要对它初始化,所以在count2初始化前对他进行的操作时无效的。

    类加载情况

    情况1:

    • 加载object....类
    • 加载启动类
    • 加载父类
    • 加载子类

    类的加载并非一定要该类被主动使用化

    1588845311178

    情况2:同上

    情况3:

    ​ 自定义的类只加载了启动类(调用常量的方法所在的类)

    1588917876432

    情况4:加载启动类以及Parent类

    反编译结果

    情况1:

    1588918065497

    情况2:类似1

    情况3:没有引用到Parent类(定义常量的类)

    1588917553379

    1588917592415

    情况4:类似1

    5. 深入理解举例2

    接口中定义的变量都是常量

    常量又分为编译期常量和运行期常量,编译期常量的值在编译期间就可以确定,直接存储在了调用类的常量池中,所以访问接口中的编译期常量并不会导致接口的初始化,只有访问接口中的运行期常量才会引起接口的初始化。

    父接口并不会因为子接口或是实现类的初始化而初始化,当访问到了其特定的静态变量时(即运行时常量,如接口中引用类型的变量)才会初始化

    public class ClassLoaderTest2 {
        public static void main(String[] args) {
            System.out.println(new demo2().a);
            System.out.println("=====");
            System.out.println(son1.a);
            new demo1().show();
            System.out.println(demo1.str);
            System.out.println(son1.b);
            System.out.println(demo1.s);//System.out.println(son1.s);
            /*输出
            * father2 singleton
            * 1
            * =====
            * 1
            * show method
            * string
            * father1 singleton
            * com.lx.father1$1@1b6d3586
            * */
        }
    }
    interface father1{
        int a=1;
        void show();
        String str="string";
        Singleton1 s=new Singleton1(){
            {
                System.out.println("father1 singleton");
            }
        };
    }
    interface son1 extends father1 {
        int b=0;
        Singleton1 s1=new Singleton1(){
            {
                System.out.println("son1 singleton");
            }
        };
    }
    class demo1 implements father1{
        @Override
        public void show() {
                System.out.println("show method");
        }
    }
    class father2{
        int a=1;
        void show(){}
        String str="string";
        Singleton1 s=new Singleton1(){
            {
                System.out.println("father2 singleton");
            }
        };
    }
    class demo2 extends father2{
    
    }
    
    6. 结果分析

    ​ 第3行:子类初始化前必须初始化父类

    ​ 第5-8行:访问到编译时常量(已经存入了调用方法类的常量池中),不会导致初始化

    ​ 第9行: 访问了运行时常量,需要初始化定义该运行时常量的类

    3、类加载器分类

    img

    一、java虚拟机自带的类加载器

    1. 启动类加载器(Bootstrap) ,C++所写,不是ClassLoader子类

      1587996894484

    2. 扩展类加载器(Extension) ,Java所写

      1587996931132

    3. 应用程序类加载器(AppClassLoader)。

      • 自定义类一般为系统(应用)类加载器加载

    二、用户自定义的类加载器

    import com.gmail.fxding2019.T;
    
    public class  Test{
        //Test:查看类加载器
        public static void main(String[] args) {
    
            Object object = new Object();
            //查看是那个“ClassLoader”(快递员把Object加载进来的)
            System.out.println(object.getClass().getClassLoader());
            //查看Object的加载器的上一层
            // error Exception in thread "main" java.lang.NullPointerException(已经是祖先了)
            //System.out.println(object.getClass().getClassLoader().getParent());
    
            System.out.println();
    
            Test t = new Test();
            System.out.println(t.getClass().getClassLoader().getParent().getParent());
            System.out.println(t.getClass().getClassLoader().getParent());
            System.out.println(t.getClass().getClassLoader());
        }
    }
    
    /*
    *output:
    * null
    * 
    * null
    * sun.misc.Launcher$ExtClassLoader@4554617c
    * sun.misc.Launcher$AppClassLoader@18b4aac2
    * */
    
    • 如果是JDK自带的类(Object、String、ArrayList等),其使用的加载器是Bootstrap加载器;如果自己写的类,使用的是AppClassLoader加载器;Extension加载器是负责将把java更新的程序包的类加载进行
    • 输出中,sun.misc.Launcher是JVM相关调用的入口程序
    • Java加载器个数为3+1。前三个是系统自带的,用户可以定制类的加载方式,通过继承Java. lang. ClassLoader

    4、双亲委派机制

    Java虚拟机采用按需加载的方式,当需要使用该类是才会去讲class文件加载到内存生成class对象,加载类是采用的是双亲委派机制

    自底向上检查类是否已经被加载

    自顶向下尝试加载类

    img

    原理图:

    1587996814470

    另外一种机制:

    1587997009728

    双亲委派优势:

    • 避免类的重复加载。
    • 保护程序安全、防止核心api被恶意篡改(如下例子)

    用户自定义的类加载器不可能加载到一个有父加载器加载的可靠类,从而防止不可靠恶意代码代替父加载器加载的可靠的代码。例如:Object类总是有跟类加载器加载,其他用户自定义的类加载器都不可能加载含有恶意代码的Object类

    //测试加载器的加载顺序
    package java.lang;
    
    public class String {
    
        public static void main(String[] args) {
    
            System.out.println("hello world!");
    
        }
    }
    
    /*
    * output:
    * 错误: 在类 java.lang.String 中找不到 main 方法
    * */
    

    解释:

    ​ 交给启动类加载器之后(java.lang.String/由java开头的包名)归它管,所以它首先加载这个类(如果核心api内没有改类也会报错),轮不到让系统类加载器去加载该类,即无法加载到自己所写的String类,核心api中的String类没有main方法,所以会报错说找不到main方法

    5、补充:

    类的实例化

    • 为新的对象分配内存
    • 为实例变量赋默认值
    • 为实例变量赋值(自己定义的)
    • 为其生成/方法或者说构造方法

    判断为同一个类的必要条件

    1587998788675

    1587998831945

    使用类加载器的原因

    1587998555990

    自定义类加载器

    1587998581137

    获取类加载器方法:

    1587998660341

    沙箱安全机制

    1587998743968

    命名空间

    1589172763494

    image-20200513210956821

    loadClass方法

    ​ 通过调用ClassLoader类的loadClass方法加载一个类,并不是对一个类的主动使用,不会导致初始化。

    类的卸载

    1589172984560

    1589173023474


  • 相关阅读:
    739. Daily Temperatures
    535. Encode and Decode TinyURL
    811. Subdomain Visit Count
    706. Design HashMap
    C++-static作用(转)
    大学四年应当如何渡过(转)
    计算机导论第八章-总结
    计算机导论第四章习题
    计算机导论-第一章习题
    20世纪最伟大的十大算法
  • 原文地址:https://www.cnblogs.com/jklixin/p/12891035.html
Copyright © 2020-2023  润新知