JVM相关(6)-- 类加载机制

相关好文:面试官对于JVM类加载机制的猛烈炮火,你能顶住吗?

https://juejin.im/post/5d1d4f8ef265da1b60291fec

类是在运行期间第一次使用时动态加载的,而不是一次性加载。因为如果一次性加载,那么会占用很多的内存。

类加载的机制的层次结构

每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:

加载:类加载过程的一个阶段,通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

1、通过一个类的全限定名获取描述此类的二进制字节流;

2、将这个字节流所代表的静态存储结构保存为方法区的运行时数据结构;

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

验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值,使用的是方法区的内存。

实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在Java堆中。

注意,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

public static int value = 123;

如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。

public static final int value = 123;

解析:主要将常量池中的【符号引用】替换为【直接引用】的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

解析的时候class已经被加载到方法区的内存中,因此要把符号引用转化为直接引用,也就是能直接找到该类实际内存地址的引用。

分为类或接口的解析,字段解析,类方法解析,接口方法解析。

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

初始化阶段是执行类构造器方法的过程,方法由类变量的赋值动作和静态语句块按照在源文件出现的顺序合并而成,该合并操作由编译器完成。

1)方法对于类或接口不是必须的,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那么编译器不会生成

2)方法与实例构造器不同,不需要显式的调用父类的方法,虚拟机会保证父类的优先执行;

3)为了防止多次执行,虚拟机会确保方法在多线程环境下被正确的加锁同步执行,如果有多个线程同时初始化一个类,那么只有一个线程能够执行方法,其它线程进行阻塞等待,直到执行完成。

4)注意:执行接口的方法不需要先执行父接口的,只有使用父接口中定义的变量时,才会执行。

对象的初始化顺序:

                  基类的static域加载

                               |

                               |

                 子类的static域加载                        

                              |

                              |

基类的成员变量初始化(基本类型初始化为默认值,对象引用设为null,若有初值则初始化为初值)           

                             |

                             |

基类的构造器加载(若在构造器中调用的方法在子类中被覆盖过,则调用覆盖后的方法)

                              |

                              |

             子类的成员变量初始化                                  

                               |

                               |

                 子类的构造器加载


类初始化场景:

1,主动引用

虚拟机中严格规定了有且只有5种情况必须对类进行初始化。

* 执行new、getstatic、putstatic和invokestatic指令;

* 使用reflect对类进行反射调用;

* 初始化一个类的时候,父类还没有初始化,会事先初始化父类;

* 启动虚拟机时,需要初始化包含main方法的类;

* 在JDK1.7中,如果java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化;

2,被动引用

以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

以下几种情况,不会触发类初始化

1)通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

class Parent {

    static int a = 100;

    static {

       System.out.println("parent init!");

    }

}

class Child extends Parent {

    static {

       System.out.println("child init!");

    }

}

public class Init{ 

    public static voidmain(String[] args){ 

       System.out.println(Child.a); 

    } 

输出结果为:

parent init!

100

2)定义对象数组,即通过数组定义来引用类,不会触发该类的初始化。

public class Init{ 

    public static voidmain(String[] args){ 

        Parent[] parents =new Parent[10];

    } 

无输出,说明没有触发类Parent的初始化,但是这段代码做了什么?先看看生成的字节码指令

anewarray指令为新数组分配空间,并触发[Lcom.ctrip.ttd.whywhy.Parent类的初始化,这个类由虚拟机自动生成。

3)常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

class Const {

    static final int A =100;

    static {

       System.out.println("Const init");

    }

}

public class Init{ 

    public static voidmain(String[] args){ 

        System.out.println(Const.A); 

    } 

输出:

100

说明没有触发类Const的初始化,在编译阶段,Const类中常量A的值100存储到Init类的常量池中,这两个类在编译成class文件之后就没有联系了。

4)通过类名获取Class对象,不会触发类的初始化。

public class test {

   public static voidmain(String[] args) throws ClassNotFoundException {

        Class c_dog =Dog.class;

        Class clazz =Class.forName("zzzzzz.Cat");

    }

}

class Cat {

    private String name;

    private int age;

    static {

       System.out.println("Cat is load");

    }

}

class Dog {

    private String name;

    private int age;

    static {

       System.out.println("Dog is load");

    }

}

执行结果:Cat is load,所以通过Dog.class并不会触发Dog类的初始化动作。

5)通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

public class test {

   public static voidmain(String[] args) throws ClassNotFoundException {

        Class clazz =Class.forName("zzzzzz.Cat", false, Cat.class.getClassLoader());

    }

}

class Cat {

    private String name;

    private int age;

    static {

        System.out.println("Catis load");

    }

}

6)通过ClassLoader默认的loadClass方法,也不会触发初始化动作

new ClassLoader(){}.loadClass("zzzzzz.Cat");


JVM类加载器

这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),下面分别介绍:

启动(Bootstrap)类加载器

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

扩展(Extension)类加载器

扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

应用程序类(Application)类加载器

也称应用程序加载器是指Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

自定义(User)类加载器

JVM提供的类加载器只能加载指定目录的类(jar和class),如果我们想从其他地方甚至网络上获取class文件,就需要自定义类加载器来实现,自定义类加载器主要都是通过继承ClassLoader或者它的子类来实现,但无论是通过继承ClassLoader还是它的子类,最终自定义类加载器的父加载器都是应用程序类加载器,因为不管调用哪个父类加载器,创建的对象都必须最终调用java.lang.ClassLoader.getSystemClassLoader()作为父加载器,getSystemClassLoader()方法的返回值是sun.misc.Launcher.AppClassLoader即应用程序类加载器。

        在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。

双亲委派模式工作原理

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

工作过程:

一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。

双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

//双亲委派模型的工作过程源码

protected synchronized Class loadClass(String name,boolean resolve)

throws ClassNotFoundException

{

    // First, check if theclass has already been loaded

    Class c =findLoadedClass(name);

    if (c == null) {

        try {

            if (parent !=null) {

                c =parent.loadClass(name, false);

            } else {

                c =findBootstrapClassOrNull(name);

            }

        } catch(ClassNotFoundException e) {

            //ClassNotFoundException thrown if class not found

            // from thenon-null parent class loader

            //父类加载器无法完成类加载请求

        }

        if (c == null) {

            // If still notfound, then invoke findClass in order to find the class

            //子加载器进行类加载

            c =findClass(name);

        }

    }

    if (resolve) {

        //判断是否需要链接过程,参数传入

        resolveClass(c);

    }

    return c;

}

双亲委派模型的工作过程如下:

(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。

(2)如果没有找到,就去委托父类加载器去加载(如代码c = parent.loadClass(name, false)所示)。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。

(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用拓展类加载器来尝试加载,继续失败则会使用AppClassLoader来加载,继续失败则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。

双亲委派模式优势

采用双亲委派模式的好处是:

1)Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

2)其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常:java.lang.SecurityException:Prohibited package name: java.lang。

类加载器间的关系

我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点:

启动类加载器,由C++实现,没有父类。

拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

自定义类加载器,父类加载器肯定为AppClassLoader。


类与类加载器

在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:

* 类的完整类名必须一致,包括包名。

* 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

显示加载与隐式加载

所谓class文件的显示加载与隐式加载的方式是指JVM加载class文件到内存的方式,显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。在日常开发以上两种方式一般会混合使用,这里我们知道有这么回事即可。

编写自己的类加载器

通过前面的分析可知,实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,继承URLClassLoader则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。那么编写自定义类加载器的意义何在呢?

* 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。

* 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。

* 以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。

* 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。

自定义类加载器

(1)从上面源码看出,调用loadClass时会先根据委派模型在父加载器中加载,如果加载失败,则会调用当前加载器的findClass来完成加载。

(2)因此我们自定义的类加载器只需要继承ClassLoader,并覆盖findClass方法,下面是一个实际例子,在该例中我们用自定义的类加载器去加载我们事先准备好的class文件。


自定义一个People.java类做例子

public class People {

    //该类写在记事本里,在用javac命令行编译成class文件,放在d盘根目录下

    private String name;

    public People() {}

    public People(Stringname) {

         this.name = name;

    }

    public String getName(){

         return name;

    }

    public voidsetName(String name) {

         this.name = name;

    }

    public String toString(){

         return "I am apeople, my name is " + name;

    }

}


自定义类加载器

自定义一个类加载器,需要继承ClassLoader类,并实现findClass方法。其中defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class(只要二进制字节流的内容符合Class文件规范)。

public class MyClassLoader extends ClassLoader

{

    public MyClassLoader()

    {

    }

    public MyClassLoader(ClassLoader parent)

    {

        super(parent);

    }

    protected ClassfindClass(String name) throws ClassNotFoundException

    {

        File file = newFile("D:/People.class");

        try{

            byte[] bytes = getClassBytes(file);

            //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class

            Class c = this.defineClass(name, bytes, 0, bytes.length);

            return c;

        }

        catch (Exception e)

        {

           e.printStackTrace();

        }

        return super.findClass(name);

    }

    private byte[] getClassBytes(File file) throws Exception

    {

        //这里要读入.class的字节,因此要使用字节流

        FileInputStream fis= new FileInputStream(file);

        FileChannel fc =fis.getChannel();

       ByteArrayOutputStream baos = new ByteArrayOutputStream();

        WritableByteChannel wbc = Channels.newChannel(baos);

        ByteBuffer by = ByteBuffer.allocate(1024);


        while (true){

            int i =fc.read(by);

            if (i == 0 || i== -1)

            break;

            by.flip();

            wbc.write(by);

            by.clear();

        }

        fis.close();

        returnbaos.toByteArray();

    }


    public static voidmain(String[] args)

    {

        MyClassLoader mcl =new MyClassLoader();

        Class clazz;

        try

        {

            clazz =Class.forName("People", true, mcl);

            Object obj;

            obj =clazz.newInstance();

           System.out.println(obj);

           System.out.println(obj.getClass().getClassLoader());//打印出我们的自定义类加载器

        }

        catch(ClassNotFoundException | InstantiationException | IllegalAccessException e)

        {

           e.printStackTrace();

        }

    }

}

运行结果:

I am a

people, my name is null

com.huawei.monitor.calcsvc.utils.MyClassLoader@15db9742


Tomcat类加载机制:

Tomcat6.0:

当应用需要到某个类时,则会按照下面的顺序进行类加载:

  1)使用bootstrap引导类加载器加载

  2)使用system系统类加载器加载

  3)使用应用类加载器在WEB-INF/classes中加载

  4)使用应用类加载器在WEB-INF/lib中加载

       5)使用common类加载器在CATALINA_HOME/lib中加载

Tomcat7.0+:

加载类或资源时,要查看的仓库及其顺序如下:

1)JVM 的 Bootstrap 类

2)应用类加载器加载Web 应用的/WEB-INF/classes 类

3)应用类加载器加载Web 应用的/WEB-INF/lib/*.jar 类

4)System 类加载器的类

5)Common 类加载器的类


如果 Web 应用类加载器配置有 ,则顺序变为:

1)JVM 的 Bootstrap 类

2)System 类加载器的类

3)Common 类加载器的类

4)应用类加载器加载Web 应用的/WEB-INF/classes 类

5)应用类加载器加载Web 应用的/WEB-INF/lib/*.jar 类

你可能感兴趣的:(JVM相关(6)-- 类加载机制)