欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

jvm详解,

来源: javaer 分享于  点击 13753 次 点评:263

jvm详解,




Java虚拟机

一、Java虚拟机概要

  1. Java的技术体系包括:

    1. 支撑Java程序运行的虚拟机。

    2. 提供各种开发领域接口支持的Java Api。

    3. Java编程语言以及很多第三方的java框架。

  2. Jvm有四部分组成:

    1. ClassLoader类加载器;

    2. Runtime data area 运行数据区 ;

    3. Execution Engine 执行引擎 ;

    4. Native Interface 本地接口 。

    5. 架构图如下:

二、ClassLoader类加载简介

  1. Classloader的作用就是将class加载到内存里面。

  2. java虚拟机结束生命周期的几种情况:

    1. 执行了system.exit()方法;0:正常退出,其他的为非正常退出。

    2. 程序正常执行结束。

    3. 程序在执行过程中遇到了异常或错误而异常终止。

    4. 由于操作系统出现错误导致java虚拟机进程终止。

  3. 一个class文件到被执行之前会有3个过程:

    1. 加载:从硬盘查找到class文件,并将class转成二进制数据加载到内存中。

    2. 连接:

      1. 验证:确保被加载的类的正确性,正常来说通过javac编译生成的class是不会有问题的,但是不排除通过其他工具进行编译的class文件,所以需要再次验证类的正确性。

      2. 准备:为类的静态变量分配内存,并将其初始化为默认值。因为此时还没有生成实例对象,所以只能初始静态变量。

      3. 解析:把类中的符号引用转换成为直接引用。

    3. 初始化:为类的静态变量赋予正确的初始化值。

    注释:初始化与准备阶段不一样,准备阶段只是赋值默认值,而初始化值是最终值。例如

    public class Test{

    ​ private static void int a = 3

    }

    以上代码等同于:

    public class Test{

    ​ private static void int a;

    ​ static{

    ​ a = 3;

    ​ }

    }

    
    最终的值虽然是3,但是却经过两次的赋值,第一次是在准备阶段,将0(int的默认初始值是:0)赋值给a,初始化阶段又将3赋值给a。
  4. java程序对类的使用方式可分为两种:

    1. 主动使用:6种情况(初始化类的时机)

      • 创建类的实例:比如new的时候。

      • 访问某个类或者接口的静态变量,或者对该静态变量赋值。

      • 调用类的静态方法。

      • 反射(如使用class.forName()).

      • 初始化一个类的子类。也可看做对父类的主动使用。

      • java虚拟机启动时被标明为启动类的类。

    2. 被动使用;除了以上6种,其余的全部为被动使用,都不会导致类的初始化。

    3. 
      注释:所有的java虚拟机实现必须在每个类或接口被java程序“首次主动使用”时才初始化。

三、Classloader详解

  1. 类的加载:

  2. 加载class文件的几种方式:

    • 从本地系统直接加载;

    • 通过网络下载class文件;

    • 从zip、jar文件中加载class文件;

    • 从专有数据库中加载class文件;

    • 将java源文件动态编译为class文件。

  3. 有两种类型的类加载器:

    1. java虚拟机自带的加载器:

      • 根类加载器(bootstrap,使用c++编写)

      • 扩展加载器(使用java代码实现)

      • 系统加载器(使用java代码实现)

      • 自定义类加载器:都是classloader的子类。

  4. 每个类都可以通过getClassLoader方法获取加载这个类的classloader。

    class.getClassLoader()方法

  5. 类的验证:

    1. 类文件的结构检查,主要是用于验证非jdk编译的class文件,保证安全。

    2. 语义检查:确保类符合java语言的语法规定。

    3. 字节码验证:

    4. 二进制兼容验证:确保相互引用的类之间协调一致,比如类A的方法中调用了B类的方法,jvm在验证A类时,会去检查方法区内是否包含B类的方法。

  6. 类的准备:jvm为类的静态变量分配内存,并设置初始值。比如:为int类型的静态变量分配4个字节的内存、long型的分配8个字节,并赋值0。

  7. 类的解析:将类的二进制数据中的符号引用替换成直接引用(或叫指针引用)。例如A类中a方法需要调用B类中的b方法。

    public class A{

    ​ public void a(){

    ​ B.b();

    ​ }

    }

    class B{

    ​ public void b(){

    ​ ........

    ​ }

    }

    
    在A类的二级制数据中,包含了对B类b方法的符号引用,这个符号引用由方法全名和相关描述组成,那么在解析阶段,jvm会把这个符号引用替换成一个指针,这个指针指向B类的b方法在的方法区内的内存位置,这个指针就是直接引用。
  8. 类和类之间的调用其实就是通过指针来指向需要引用的内存空间。

  9. 类的初始化:jvm执行类的初始化语句,为类的静态变量赋予初始值。

    
    初始化类的静态变量有两种方式:
    1.在静态变量生命处进行初始化。
    2.在静态代码块中进行初始化。例子:
    public Class A{
        private static int a = 1;//第一种方式
        
        private static int b;
        
        static{
            b = 1;//第二种方式
            ....
        }
    }
  10. 类的初始化步骤:

    1. 加入类没有被加载和连接,那就先加载和连接。

    2. 如果类存在父类,而且父类没有被初始化,就先初始化父类。

    3. 如果类中有初始化语句(比如:静态变量、静态代码块),就依次执行这些初始化语句。

  11. 当调用一个类中静态常量时,根据jvm的编译情况决定是否初始类:

    第一种情况:不会初始类,因为jvm在编译的时候6/3已经计算出来为2,编译成class的时候已经是2了,相当于public static final int x = 2,所以调用静态常量时不会初始化类。
    public Class A{
        public static final int x = 6/3;
    }
    public Class B{
        public static void main(String args[]){
            A.x;
        }
    }
    
    第二种情况:会被初始化,因为jvm在编译的时候还不知道Random执行后的值,需要初始化之后才知道,所以类会被初始化。
    public Class A{
        public static final int x = new Random().nextInt(100);
    }
    public Class B{
        public static void main(String args[]){
            A.x;
        }
    }

  12. 当一个类在初始化的时候,先初始化所有的父类,但不会先去初始化它实现的所哟接口。当初始化一个接口时,也不会去先初始化它的父接口。除非程序首次使用特定父接口的静态变量时才会导致接口的初始化。

  13. 程序中对子类的主动使用会导致父类的初始化,但是对父类的主动使用不会导致子类初始化。比如:初始化了Object不会导致Object的所有子类都初始化。

  14. 
    public Class parent{
        static int a = 3;
        
        static{
            system.out.println("parent!");
        }
    }
    
    public Class children{
        static int b = 4;
        static{
            system.out.println("children!");
        }
    }
    
    public Class Test{
        static{
            system.out.println("test!");
        }
        public static void main(String[]args){
            system.out.println(Children.b);
        }
    }
    执行结果:
    test!
    parent!
    children!
    4
    解析:因为使用main方法时,相当于先初始化了test类,但是main方法调用了children的静态变量,所以需要初始化children类,但是它还有parent为父类,所以需要先初始化父类,所以先将父类初始化,在初始化子类,最后将b的值打印出来。
  15. 如果只是定义了一个对象,使用new,也不会去初始类,例如上面的列子:如果parent parent;只是定义了,没有new,也不会去初始parent类。

  16. 
    public Class parent{
        static int a = 3;
        
        static{
            system.out.println("parent!");
        }
        
        static void some(){
            system.out.println("some!");
        }
    }
    
    public Class children{
        static{
            system.out.println("children!");
        }
    }
    
    public Class Test{
        static{
            system.out.println("test!");
        }
        public static void main(String[]args){
            system.out.println(Children.a);
            parent.some();
        }
    }
    执行结果:
    test!
    parent!
    3
    some!
    解析:只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。实例中a是parent中定义的,而不是children中定义的,所以不会去初始化children类。

四、classloader深度剖析

  1. 当调用ClassLoader类的loadclass方法加载一个类,并不是对类的主动使用,不会导致类的初始。

    
    package com.user.net;
    
    public class MyTest {
        
        public static void main(String[]args)throws Exception{
            //获得系统类加载器
            ClassLoader clazz = ClassLoader.getSystemClassLoader();
            //将person类加载到内存中
            Class<?> clazzs = clazz.loadClass("com.user.net.person");
            System.out.println("==========================");
            //通过class.forName将person类进行初始化
            clazzs = Class.forName("com.user.net.person");
        }
    }
    
    class person{
        
        static {
            System.out.println("pserson!");
        }
    }
    执行结果:
    ==========================
    pserson!
    解析:ClassLoader类的loadclass方法加载一个类,并不是对类的主动使用,不会导致类的初始。但是例子中class.forName方法属于6种主动使用中的一种,所以会将person进行初始化。
  2. 类加载器的父亲委托机制:类加载器把类加载到java虚拟机中,除了jvm自带的根类加载器外(bootstrup),其余的加载器都有且只有一个父加载器。当java程序请求加载器加载时,加载器会先委托自己的父类加载器去加载,若父类能加载则完成加载任务,否则有加载器本身去加载。

  3. 三种加载器他们的关系虽是父子关系,但是并没有继承关系,他们之间的关系是组合关系。

  4. 初始类加载器:当一个类需要加载时,类加载器先去父类加载器进行加载,依次类推,能够加载的那个类加载器叫做初始类加载器。

  5. 定义类加载器:如果一个类的加载器父类能加载这个类,那么从这个类加载父类一下所有的类加载器都成为定义类加载器。

  6. 当生成一个自定义的类加载器实例时,如果没有指定它的父加载器(生成自定义类加载器的时有空构造方法、带有参数的构造方法,如果使用空构造,那就是使用默认系统类加载器,如果使用了带有参数的构造方式,构造参数中会去指定父类加载器),那系统类加载就会成为自定义类加载器的父加载器。

  7. 运行时包:当两个类属于同一个package下面,而且package下面的类都是由同一个类加载器加载的才能成为运行时包。成为同一个运行时包,这两个条件缺一不可。

    
    举例:如果自定义写一个java.lang.String类,与java.lang.*,虽然是同一个package,但不是一个类加载器进行加载,所以说他们并不是运行时包。
  8. 双亲委托加载的好处:

    1. 防止类被重复加载。

    2. 处于安全的考虑,防止恶意代码冒充核心代码被加载。

  9. 命名空间:每一个classloader都会有一个命名空间,这个命名空间中存放着这个classloader加载的类,每一个class都只能调用每个对应的方法区中的方法。如果想要调用不同命名空间中的对象方法,需要通过反射重新加载。

五、自定义类加载器

  1. 自定义类加载器的主要几个方法:

    1. loadClassData方法:从磁盘或者其他地方读取class文件,将文件写进将byte数组。

    2. findClass方法:通过defineClass(四个参数)方法,将byte数组转成class对象,四个参数分别是:class的名称(包括package)、byte数组、数组开始、数组结束。这个方法必须要重新classloader中的findClass方法,因为这个方法可以添加自己的逻辑,比如说解码。

    3. loadClass方法:这个方法是classLoader类中定义的方法,这个方法的作用是,先去找当前类加载器的父类加载器,去查看这个父类加载器有没有加载这个类,如果有就返回class对象,如果没有加载就会去找这个类进行加载(加载就会去调用findClass方法),如果找不到就抛异常,子类就去执行,子类也会去调用findClass方法。

  2. 自定义类加载器的好处:

    1. 用于处理加密过后的class类:class类很容易就会被反编译,被反编译后就可以轻松获取class类的内容,通过自定义类加载器可以很好的处理这些问题,例如,在编译过后的class类通过各种算法(如通过base64进行编译)进行加密,传统jdk的三种类加载器就不会去单独处理解密这个操作,所以自定义类加载器会先进行解密,解密之后再去加载。

    2. 从非标准的来源加载类(从指定的来源加载类 ):在正常加载类的时候都是通过本地jar包、网上加载class,如果需要从数据库、云端或者其他地方加载,这时候使用自定义类加载器。

  3. 如果自定义类加载器的构造方法中classloader参数设置为null,就会默认父类加载器是根类加载器,不会默认为系统类加载器。

  4. 编写一个类加载器:

    
    package com.user.net;
    
    import java.io.ByteArrayOutputStream;
    import java.io.FileInputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.Channels;
    import java.nio.channels.FileChannel;
    import java.nio.channels.WritableByteChannel;
    
    /**
     * 自定义类加载器
     * @author zhouJ
     *
     */
    public class MyClassLoader extends ClassLoader{
        
        //记录类加载器的名称
        private String name;
        
        //记录类加载器的位置
        private String path = "F://";
        
        //记录加载类的格式
        private String type = ".class";
        
        public MyClassLoader(String name){
            super();
            this.name = name;
        }
        
        //设置有父类加载器的构造方法
        public MyClassLoader(ClassLoader parent , String name){
            super(parent);//通过父类的构造方法设置父类加载器
            this.name = name;
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte [] bytes = null;
            Class clazz = null;
            try{
                bytes = this.loadClassData(name);
                clazz = this.defineClass(name, bytes, 0, bytes.length);
                if(clazz == null){
                    return null;
                }
            }catch(Exception e){
                e.printStackTrace();
            }
            return clazz;
        }
    
        /**
         * 用于读取class文件的内容
         * @param path
         * @return
         */
        private byte[] loadClassData(String name) throws Exception{
            //使用NIO读取class文件
            name = name.replace(".", "\\");
            name = name.concat(type);
            this.name = name;
            FileInputStream fis = new FileInputStream(path + name);
            FileChannel fileC = fis.getChannel();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            WritableByteChannel outC = Channels.newChannel(baos);
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            while (true) {
                int i = fileC.read(buffer);
                if (i == 0 || i == -1) {
                    break;
                }
                buffer.flip();
                outC.write(buffer);
                buffer.clear();
            }
            fis.close();
            return baos.toByteArray();
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPath() {
            return path;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
        
        
        public static void main(String[] args){
            MyClassLoader loader = new MyClassLoader("loader1");
            //不同类加载器设置类加载的位置
            loader.setPath("F:\\");
            //设置loader为loder1的父类加载器
            MyClassLoader loader1 = new MyClassLoader(loader,"loader2");
            loader1.setPath("E:\\");
            try {
                Class clazz = loader1.loadClass("类名,包括package");
                Object obj = clazz.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
    }
    

  5. 同一个命名空间的类时相互可见的,子类加载器命名空间的包含所有父加载器的命名空间所以子类加载器加载的类能看到父类加载器加载的类,反之不行。如果两个加载器之间没有父子关系,那么各自加载的类相互不可见。

  6. 当两个不同类加载器加载的class需要互相引用时,无法直接引用,直接引用会报错,只能通过反射进行引用,因为反射在使用反射的时候class.forname方法获取的是当前调用类的类加载器,而非系统类加载器,所以就可以解释为什么反射可以解决两个不同类加载器之间无法直接调用的原因。

  7. 类的卸载:一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

  8. java自带的类加载器所加载的类,在jvm生命周期中始终不会被卸载,只有自定义的类加载器加载的类才会被卸载。

  9. 一个类加载器销毁只要将加载器设为null,如果重新加载后,那么加载的类与之前加载的类的hashcode是不一样的。

  10. 通过对象的hashcode方法可以获取对象的内存空间值,通过这个值可以判断两个对象是否同一个对象。

  11. class实例与加载这个class的加载器之间为双向关联关系,因为类加载器内部实现中,用一个java集合来存放所加载的所有类的引用,同时一个class对象可以通过getClassLoader方法获得他的类加载器。

  12. 当执行了classloader,classload,newInstance三个方法后的引用关系。如图:



流程图解析:当ClassLoader被new出现在堆区会有一个classLoader对象,当调用classLoader的loadClass方法后会产生一个class引用变量,同时在堆区产生一个class对象,当执行了newInstance方法后,会产生一个object的引用变量,同时在堆区产生一个object对象,object对象可以通过class方法获取class对象,class对象可通过getclassLoader获取classloader对象,classloader对象通过classloader产生class对象。当classloader引用变量、class引用变量、object引用变量都变成null。最后方法区的二进制数据也会清空。


Java虚拟机

一、Java虚拟机概要

  1. Java的技术体系包括:

    1. 支撑Java程序运行的虚拟机。

    2. 提供各种开发领域接口支持的Java Api。

    3. Java编程语言以及很多第三方的java框架。

  2. Jvm有四部分组成:

    1. ClassLoader类加载器;

    2. Runtime data area 运行数据区 ;

    3. Execution Engine 执行引擎 ;

    4. Native Interface 本地接口 。

    5. 架构图如下:

二、ClassLoader类加载简介

  1. Classloader的作用就是将class加载到内存里面。

  2. java虚拟机结束生命周期的几种情况:

    1. 执行了system.exit()方法;0:正常退出,其他的为非正常退出。

    2. 程序正常执行结束。

    3. 程序在执行过程中遇到了异常或错误而异常终止。

    4. 由于操作系统出现错误导致java虚拟机进程终止。

  3. 一个class文件到被执行之前会有3个过程:

    1. 加载:从硬盘查找到class文件,并将class转成二进制数据加载到内存中。

    2. 连接:

      1. 验证:确保被加载的类的正确性,正常来说通过javac编译生成的class是不会有问题的,但是不排除通过其他工具进行编译的class文件,所以需要再次验证类的正确性。

      2. 准备:为类的静态变量分配内存,并将其初始化为默认值。因为此时还没有生成实例对象,所以只能初始静态变量。

      3. 解析:把类中的符号引用转换成为直接引用。

    3. 初始化:为类的静态变量赋予正确的初始化值。

    注释:初始化与准备阶段不一样,准备阶段只是赋值默认值,而初始化值是最终值。例如

    public class Test{

    ​ private static void int a = 3

    }

    以上代码等同于:

    public class Test{

    ​ private static void int a;

    ​ static{

    ​ a = 3;

    ​ }

    }

    
    最终的值虽然是3,但是却经过两次的赋值,第一次是在准备阶段,将0(int的默认初始值是:0)赋值给a,初始化阶段又将3赋值给a。
  4. java程序对类的使用方式可分为两种:

    1. 主动使用:6种情况(初始化类的时机)

      • 创建类的实例:比如new的时候。

      • 访问某个类或者接口的静态变量,或者对该静态变量赋值。

      • 调用类的静态方法。

      • 反射(如使用class.forName()).

      • 初始化一个类的子类。也可看做对父类的主动使用。

      • java虚拟机启动时被标明为启动类的类。

    2. 被动使用;除了以上6种,其余的全部为被动使用,都不会导致类的初始化。

    3. 
      注释:所有的java虚拟机实现必须在每个类或接口被java程序“首次主动使用”时才初始化。

三、Classloader详解

  1. 类的加载:

  2. 加载class文件的几种方式:

    • 从本地系统直接加载;

    • 通过网络下载class文件;

    • 从zip、jar文件中加载class文件;

    • 从专有数据库中加载class文件;

    • 将java源文件动态编译为class文件。

  3. 有两种类型的类加载器:

    1. java虚拟机自带的加载器:

      • 根类加载器(bootstrap,使用c++编写)

      • 扩展加载器(使用java代码实现)

      • 系统加载器(使用java代码实现)

      • 自定义类加载器:都是classloader的子类。

  4. 每个类都可以通过getClassLoader方法获取加载这个类的classloader。

    class.getClassLoader()方法

  5. 类的验证:

    1. 类文件的结构检查,主要是用于验证非jdk编译的class文件,保证安全。

    2. 语义检查:确保类符合java语言的语法规定。

    3. 字节码验证:

    4. 二进制兼容验证:确保相互引用的类之间协调一致,比如类A的方法中调用了B类的方法,jvm在验证A类时,会去检查方法区内是否包含B类的方法。

  6. 类的准备:jvm为类的静态变量分配内存,并设置初始值。比如:为int类型的静态变量分配4个字节的内存、long型的分配8个字节,并赋值0。

  7. 类的解析:将类的二进制数据中的符号引用替换成直接引用(或叫指针引用)。例如A类中a方法需要调用B类中的b方法。

    public class A{

    ​ public void a(){

    ​ B.b();

    ​ }

    }

    class B{

    ​ public void b(){

    ​ ........

    ​ }

    }

    
    在A类的二级制数据中,包含了对B类b方法的符号引用,这个符号引用由方法全名和相关描述组成,那么在解析阶段,jvm会把这个符号引用替换成一个指针,这个指针指向B类的b方法在的方法区内的内存位置,这个指针就是直接引用。
  8. 类和类之间的调用其实就是通过指针来指向需要引用的内存空间。

  9. 类的初始化:jvm执行类的初始化语句,为类的静态变量赋予初始值。

    
    初始化类的静态变量有两种方式:
    1.在静态变量生命处进行初始化。
    2.在静态代码块中进行初始化。例子:
    public Class A{
        private static int a = 1;//第一种方式
        
        private static int b;
        
        static{
            b = 1;//第二种方式
            ....
        }
    }
  10. 类的初始化步骤:

    1. 加入类没有被加载和连接,那就先加载和连接。

    2. 如果类存在父类,而且父类没有被初始化,就先初始化父类。

    3. 如果类中有初始化语句(比如:静态变量、静态代码块),就依次执行这些初始化语句。

  11. 当调用一个类中静态常量时,根据jvm的编译情况决定是否初始类:

    第一种情况:不会初始类,因为jvm在编译的时候6/3已经计算出来为2,编译成class的时候已经是2了,相当于public static final int x = 2,所以调用静态常量时不会初始化类。
    public Class A{
        public static final int x = 6/3;
    }
    public Class B{
        public static void main(String args[]){
            A.x;
        }
    }
    
    第二种情况:会被初始化,因为jvm在编译的时候还不知道Random执行后的值,需要初始化之后才知道,所以类会被初始化。
    public Class A{
        public static final int x = new Random().nextInt(100);
    }
    public Class B{
        public static void main(String args[]){
            A.x;
        }
    }

  12. 当一个类在初始化的时候,先初始化所有的父类,但不会先去初始化它实现的所哟接口。当初始化一个接口时,也不会去先初始化它的父接口。除非程序首次使用特定父接口的静态变量时才会导致接口的初始化。

  13. 程序中对子类的主动使用会导致父类的初始化,但是对父类的主动使用不会导致子类初始化。比如:初始化了Object不会导致Object的所有子类都初始化。

  14. 
    public Class parent{
        static int a = 3;
        
        static{
            system.out.println("parent!");
        }
    }
    
    public Class children{
        static int b = 4;
        static{
            system.out.println("children!");
        }
    }
    
    public Class Test{
        static{
            system.out.println("test!");
        }
        public static void main(String[]args){
            system.out.println(Children.b);
        }
    }
    执行结果:
    test!
    parent!
    children!
    4
    解析:因为使用main方法时,相当于先初始化了test类,但是main方法调用了children的静态变量,所以需要初始化children类,但是它还有parent为父类,所以需要先初始化父类,所以先将父类初始化,在初始化子类,最后将b的值打印出来。
  15. 如果只是定义了一个对象,使用new,也不会去初始类,例如上面的列子:如果parent parent;只是定义了,没有new,也不会去初始parent类。

  16. 
    public Class parent{
        static int a = 3;
        
        static{
            system.out.println("parent!");
        }
        
        static void some(){
            system.out.println("some!");
        }
    }
    
    public Class children{
        static{
            system.out.println("children!");
        }
    }
    
    public Class Test{
        static{
            system.out.println("test!");
        }
        public static void main(String[]args){
            system.out.println(Children.a);
            parent.some();
        }
    }
    执行结果:
    test!
    parent!
    3
    some!
    解析:只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。实例中a是parent中定义的,而不是children中定义的,所以不会去初始化children类。

四、classloader深度剖析

  1. 当调用ClassLoader类的loadclass方法加载一个类,并不是对类的主动使用,不会导致类的初始。

    
    package com.user.net;
    
    public class MyTest {
        
        public static void main(String[]args)throws Exception{
            //获得系统类加载器
            ClassLoader clazz = ClassLoader.getSystemClassLoader();
            //将person类加载到内存中
            Class<?> clazzs = clazz.loadClass("com.user.net.person");
            System.out.println("==========================");
            //通过class.forName将person类进行初始化
            clazzs = Class.forName("com.user.net.person");
        }
    }
    
    class person{
        
        static {
            System.out.println("pserson!");
        }
    }
    执行结果:
    ==========================
    pserson!
    解析:ClassLoader类的loadclass方法加载一个类,并不是对类的主动使用,不会导致类的初始。但是例子中class.forName方法属于6种主动使用中的一种,所以会将person进行初始化。
  2. 类加载器的父亲委托机制:类加载器把类加载到java虚拟机中,除了jvm自带的根类加载器外(bootstrup),其余的加载器都有且只有一个父加载器。当java程序请求加载器加载时,加载器会先委托自己的父类加载器去加载,若父类能加载则完成加载任务,否则有加载器本身去加载。

  3. 三种加载器他们的关系虽是父子关系,但是并没有继承关系,他们之间的关系是组合关系。

  4. 初始类加载器:当一个类需要加载时,类加载器先去父类加载器进行加载,依次类推,能够加载的那个类加载器叫做初始类加载器。

  5. 定义类加载器:如果一个类的加载器父类能加载这个类,那么从这个类加载父类一下所有的类加载器都成为定义类加载器。

  6. 当生成一个自定义的类加载器实例时,如果没有指定它的父加载器(生成自定义类加载器的时有空构造方法、带有参数的构造方法,如果使用空构造,那就是使用默认系统类加载器,如果使用了带有参数的构造方式,构造参数中会去指定父类加载器),那系统类加载就会成为自定义类加载器的父加载器。

  7. 运行时包:当两个类属于同一个package下面,而且package下面的类都是由同一个类加载器加载的才能成为运行时包。成为同一个运行时包,这两个条件缺一不可。

    
    举例:如果自定义写一个java.lang.String类,与java.lang.*,虽然是同一个package,但不是一个类加载器进行加载,所以说他们并不是运行时包。
  8. 双亲委托加载的好处:

    1. 防止类被重复加载。

    2. 处于安全的考虑,防止恶意代码冒充核心代码被加载。

  9. 命名空间:每一个classloader都会有一个命名空间,这个命名空间中存放着这个classloader加载的类,每一个class都只能调用每个对应的方法区中的方法。如果想要调用不同命名空间中的对象方法,需要通过反射重新加载。

五、自定义类加载器

  1. 自定义类加载器的主要几个方法:

    1. loadClassData方法:从磁盘或者其他地方读取class文件,将文件写进将byte数组。

    2. findClass方法:通过defineClass(四个参数)方法,将byte数组转成class对象,四个参数分别是:class的名称(包括package)、byte数组、数组开始、数组结束。这个方法必须要重新classloader中的findClass方法,因为这个方法可以添加自己的逻辑,比如说解码。

    3. loadClass方法:这个方法是classLoader类中定义的方法,这个方法的作用是,先去找当前类加载器的父类加载器,去查看这个父类加载器有没有加载这个类,如果有就返回class对象,如果没有加载就会去找这个类进行加载(加载就会去调用findClass方法),如果找不到就抛异常,子类就去执行,子类也会去调用findClass方法。

  2. 自定义类加载器的好处:

    1. 用于处理加密过后的class类:class类很容易就会被反编译,被反编译后就可以轻松获取class类的内容,通过自定义类加载器可以很好的处理这些问题,例如,在编译过后的class类通过各种算法(如通过base64进行编译)进行加密,传统jdk的三种类加载器就不会去单独处理解密这个操作,所以自定义类加载器会先进行解密,解密之后再去加载。

    2. 从非标准的来源加载类(从指定的来源加载类 ):在正常加载类的时候都是通过本地jar包、网上加载class,如果需要从数据库、云端或者其他地方加载,这时候使用自定义类加载器。

  3. 如果自定义类加载器的构造方法中classloader参数设置为null,就会默认父类加载器是根类加载器,不会默认为系统类加载器。

  4. 编写一个类加载器:

    
    package com.user.net;
    
    import java.io.ByteArrayOutputStream;
    import java.io.FileInputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.Channels;
    import java.nio.channels.FileChannel;
    import java.nio.channels.WritableByteChannel;
    
    /**
     * 自定义类加载器
     * @author zhouJ
     *
     */
    public class MyClassLoader extends ClassLoader{
        
        //记录类加载器的名称
        private String name;
        
        //记录类加载器的位置
        private String path = "F://";
        
        //记录加载类的格式
        private String type = ".class";
        
        public MyClassLoader(String name){
            super();
            this.name = name;
        }
        
        //设置有父类加载器的构造方法
        public MyClassLoader(ClassLoader parent , String name){
            super(parent);//通过父类的构造方法设置父类加载器
            this.name = name;
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte [] bytes = null;
            Class clazz = null;
            try{
                bytes = this.loadClassData(name);
                clazz = this.defineClass(name, bytes, 0, bytes.length);
                if(clazz == null){
                    return null;
                }
            }catch(Exception e){
                e.printStackTrace();
            }
            return clazz;
        }
    
        /**
         * 用于读取class文件的内容
         * @param path
         * @return
         */
        private byte[] loadClassData(String name) throws Exception{
            //使用NIO读取class文件
            name = name.replace(".", "\\");
            name = name.concat(type);
            this.name = name;
            FileInputStream fis = new FileInputStream(path + name);
            FileChannel fileC = fis.getChannel();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            WritableByteChannel outC = Channels.newChannel(baos);
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            while (true) {
                int i = fileC.read(buffer);
                if (i == 0 || i == -1) {
                    break;
                }
                buffer.flip();
                outC.write(buffer);
                buffer.clear();
            }
            fis.close();
            return baos.toByteArray();
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPath() {
            return path;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
        
        
        public static void main(String[] args){
            MyClassLoader loader = new MyClassLoader("loader1");
            //不同类加载器设置类加载的位置
            loader.setPath("F:\\");
            //设置loader为loder1的父类加载器
            MyClassLoader loader1 = new MyClassLoader(loader,"loader2");
            loader1.setPath("E:\\");
            try {
                Class clazz = loader1.loadClass("类名,包括package");
                Object obj = clazz.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
    }
    

  5. 同一个命名空间的类时相互可见的,子类加载器命名空间的包含所有父加载器的命名空间所以子类加载器加载的类能看到父类加载器加载的类,反之不行。如果两个加载器之间没有父子关系,那么各自加载的类相互不可见。

  6. 当两个不同类加载器加载的class需要互相引用时,无法直接引用,直接引用会报错,只能通过反射进行引用,因为反射在使用反射的时候class.forname方法获取的是当前调用类的类加载器,而非系统类加载器,所以就可以解释为什么反射可以解决两个不同类加载器之间无法直接调用的原因。

  7. 类的卸载:一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

  8. java自带的类加载器所加载的类,在jvm生命周期中始终不会被卸载,只有自定义的类加载器加载的类才会被卸载。

  9. 一个类加载器销毁只要将加载器设为null,如果重新加载后,那么加载的类与之前加载的类的hashcode是不一样的。

  10. 通过对象的hashcode方法可以获取对象的内存空间值,通过这个值可以判断两个对象是否同一个对象。

  11. class实例与加载这个class的加载器之间为双向关联关系,因为类加载器内部实现中,用一个java集合来存放所加载的所有类的引用,同时一个class对象可以通过getClassLoader方法获得他的类加载器。

  12. 当执行了classloader,classload,newInstance三个方法后的引用关系。如图:


流程图解析:当ClassLoader被new出现在堆区会有一个classLoader对象,当调用classLoader的loadClass方法后会产生一个class引用变量,同时在堆区产生一个class对象,当执行了newInstance方法后,会产生一个object的引用变量,同时在堆区产生一个object对象,object对象可以通过class方法获取class对象,class对象可通过getclassLoader获取classloader对象,classloader对象通过classloader产生class对象。当classloader引用变量、class引用变量、object引用变量都变成null。最后方法区的二进制数据也会清空。

相关文章

    暂无相关文章
相关栏目:

用户点评