JVM类加载子系统-JVM笔记(一)

JVM架构-英

类加载子系统的作用

image-20210414172806919

1、类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
2、ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
3、加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

类加载器ClassLoader的角色

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块
类的加载过程
image.png
  • 加载
    1、通过一个类的全限定名获取定义此类的二进制字节流。
    2、将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
    3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    加载class文件的方式:
    1、从本地系统中直接加载
    2、通过网络获取,比如Web Applet
    3、从zip压缩包中读取,成为jar、war格式的基础
    4、运行时计算生成,如动态代理技术
    5、由其他文件生成,如JSP应用
    6、从专有数据库中提取class文件
    7、从加密文件中获取,防class文件被反编译的保护措施

  • 链接
    1、验证(verify)

    确保class文件的字节流中包含的信息符合当前虚拟机的要求,保证 被加载类的正确性,不会危害虚拟机自身安全。
    主要包括4种验证:文件格式、元数据、字节码、符号引用

    2、准备(prepare)

    为类变量分配内存并且设置该类变量的默认初始值,即零值。
    不包含使用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。
    不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。

    3、解析(resolve)

    符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用

  • 初始化

    1、初始化阶段就是执行类构造器方法clinit()的过程。
    2、此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
    3、构造器方法中指令按语句在源文件中出现的顺序执行。
    4、clinit()不同于类的构造器,构造器是虚拟机视角下的init()
    5、若该类具有父类,JVM会保证子类的clinit()执行之前,父类的clinit()已经执行完毕。
    6、虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。

    image-20210414193930659

    Demo:

    public class ClassInitTest {
        private static int num = 1;
    
        static {
            num = 2;
            number = 20;
        }
    
        private static int number = 10;
    
        public static void main(String[] args) {
            System.out.println(ClassInitTest.num);//2
            System.out.println(ClassInitTest.number);
        }
    }
    

    Run->

    2
    10
    
    image-20210414194304616

    任何一个类声明以后,内部至少存在一个类的构造器

    image-20210414194825819
    image-20210414194947722

    JVM会保证子类的clinit()执行之前,父类的clinit()已经执行完毕

    public class ClinitTest1 {
        static class Father{
            public static int A = 1;
            static{
                A = 2;
            }
        }
    
        static class Son extends Father{
            public static int B = A;
        }
    
        public static void main(String[] args) {
            //加载Father类,其次加载Son类。
            System.out.println(Son.B);//2
        }
    }
    

    run->

    2
    

    虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁

    public class DeadThreadTest {
        public static void main(String[] args) {
            Runnable r = () -> {
                System.out.println(Thread.currentThread().getName() + "开始");
                DeadThread dead = new DeadThread();
                System.out.println(Thread.currentThread().getName() + "结束");
            };
    
            Thread t1 = new Thread(r, "线程1");
            Thread t2 = new Thread(r, "线程2");
    
            t1.start();
            t2.start();
        }
    }
    
    class DeadThread {
        static {
            if (true) {
                System.out.println(Thread.currentThread().getName() + "初始化当前类");
                while (true) {
    
                }
            }
        }
    }
    
    image-20210414195738686

类加载器的分类

1、JVM支持两种类型的类加载器,分别是:引导类加载器(Bootstrap ClassLoader)和自定义加载器(User-Defined ClassLoader)
2、从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

image-20210414200113074

类加载器初始化过程:

在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。

虚拟机自带的加载器
public class ClassLoaderTest {
    public static void main(String[] args) {

        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取其上层:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d

        //获取其上层:获取不到引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //对于用户自定义类来说:默认使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null


    }
}

Run>

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1d44bcfa
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null
  • 启动类加载器(引导类加载器Bootstrap ClassLoader)

    1、这个类加载使用C/C++语言实现,嵌套在JVM内部。
    2、它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、reources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
    3、并不继承自java.lang.ClassLoader,没有父加载器。
    4、加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
    5、出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。

  • 扩展类加载器(Extension ClassLoader)

    1、Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
    2、派生于ClassLoader类。
    3、父类加载器为启动类加载器。
    4、从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

  • 应用程序类加载器(系统类加载器AppClassLoader)

    1、Java语言编写,由sun.misc.Launcher$AppClassLoader实现。
    2、派生于ClassLoader类。
    3、父类加载器为扩展类加载器。
    4、它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
    5、该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
    6、通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

public class ClassLoaderTest1 {
    public static void main(String[] args) {
        System.out.println("**********启动类加载器**************");
        //获取BootstrapClassLoader能够加载的api的路径
        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urLs) {
            System.out.println(element.toExternalForm());
        }
        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);

        System.out.println("***********扩展类加载器*************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }

        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d

    }
}

Run>

**********启动类加载器**************
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/classes
null
***********扩展类加载器*************
/Users/jiangbin/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
null
用户自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由上述三种类加载器相互配合执行的,在必要时,还可以自定义类加载器,来定制类的加载方式。

为什么要自定义类加载器?
1、隔离加载类
2、修改类加载器的方式
3、扩展加载源
4、防止源码泄露

用户自定义类加载器实现的步骤

  1. 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
  2. 在jdk1.2之前,在自定义类加载器时,总会去继承CLassLoader类并重写loadClass()方法,从而实现自定义类加载器类,但是在jdk1.2之后,不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中。
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {

        try {
            byte[] result = getClassFromCustomPath(name);
            if(result == null){
                throw new FileNotFoundException();
            }else{
                return defineClass(name,result,0,result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException(name);
    }

    private byte[] getClassFromCustomPath(String name){
        //从自定义路径中加载指定类:细节略
        //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
        return null;
    }

    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class clazz = Class.forName("One",true,customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

双亲委派机制

image-20210414200113074

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

工作原理
  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
  3. 如果父类加载器可以完成类加载任务,就成功返回,若父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载。
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 检查当前类加载器是否已经加载了该类
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {  //如果当前加载器父加载器不为空则委托父加载器加载该类
                    c = parent.loadClass(name, false);
                } else {  //如果当前加载器父加载器为空则委托引导类加载器加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {  //不会执行
            resolveClass(c);
        }
        return c;
    }
}
双亲委派机制的优势

1、避免类的重复加载
2、保护程序安全,防止核心API被随意篡改

自定义类:java.lang.String

看一个类加载示例:

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("**************My String Class**************");
    }
}

运行结果: 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args)

沙箱安全机制

自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

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

1、类的完整类名必须一致,包括包名。
2、加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但是只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

对类加载器的引用

JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用

Java程序对类的使用方式分为:主动引用和被动引用

主动使用,又分为下面7种情况:

1、创建类的实例

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

//FinalTest.x目前是个编译期常量,它会在编译期间将其放到常量池,并不会导到FinalTest的主动使用,
class FinalTest {
    public static final int X = 3;

    static {
        System.out.println("FinalTest static block ");
    }
}

class FinalTest2 {
    public static  int X = 3;

    static {
        System.out.println("FinalTest2 static block ");
    }
}

class FinalTest3 {
    public static  final  int X = new Random().nextInt(3);

    static {
        System.out.println("FinalTest3 static block ");
    }
}


public class MyTest8 {

    public static void main(String[] args) {
        System.out.println(FinalTest.X);
        System.out.println("-------------");

        System.out.println(FinalTest2.X);//1、创建类的实例

        System.out.println("-------------");

        // getstatic     #8                  // Field com/shengsiyuan/jvm/classloader/FinalTest3.X:I
        System.out.println(FinalTest3.X);//2、访问某个类或接口的静态变量,或者对该静态变量赋值
    }

}

3、调用类的静态方法

class Patent {
    static int a = 3;

    static {
        System.out.println("Patent static block");
    }

}

class Child extends Patent {
    static int b = 4;

    static {
        System.out.println("Child static block");
    }
}
public class MyTest9 {

    static  {
        System.out.println("MyTest9 static block");
    }

    public static void main(String[] args) {

        System.out.println(Child.b);

    }
}

Run>

MyTest9 static block
Patent static block
Child static block
4

为什么是按这样一个顺序输出的呢?由于使用到了main()静态方法,所以会导致MyTest9的主动使用,所以"MyTes9 static block"会被初始化,其原因还是七种主动使用的情况之一 调用类的静态方法

接着由于"Child.b"子类静态变量的使用会导到它的父类进行初始化,所以"Parent static block"输出了,最后自己再初始化,所以"Child static block"输出了,最终再输出要打印的变量的值,为了进一步查看类的加载信息,还是给JVM加上"-XX:+TrancClassLoading"参数来进行观测

4、反射,如Class.forName()

class CL{
    static {
        System.out.println("Class CL");
    }
}
/*
调用classLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
 */
public class MyTest12 {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class cl = classLoader.loadClass("com.atguigu.classloader.CL");//不会触发类的初始化
        System.out.println(cl);
        System.out.println("-------------------------");
        cl = Class.forName("com.atguigu.classloader.CL");//使用了反射,这属于类初始化时机的反射时机。会触发类的初始化。
        System.out.println(cl);
    }
}

RUN>

class com.atguigu.classloader.CL
-------------------------
Class CL
class com.atguigu.classloader.CL

Process finished with exit code 0
class Parent2 {
    static int a = 2;

    static {
        System.out.println("Parent2 static block");
    }
}

class Child2 extends Parent2 {
    static int b = 4;

    static {
        System.out.println("Child2 static block");
    }
}

public class MyTest10 {

    static {
        System.out.println("MyTest10 static block");
    }

    public static void main(String[] args) {
        Parent2 parent2;  //定义不属于主动使用类,不会导致类的初始化

        System.out.println("------------------");

        parent2 = new Parent2();//1创建类的实例

        System.out.println("------------------");

        System.out.println(parent2.a);

        System.out.println("------------------");

        System.out.println(Child2.b);//访问某个类或接口的静态变量
    }
}
MyTest10 static block
------------------
Parent2 static block
------------------
2
------------------
Child2 static block
4

5、初始化一个类的子类

6、Java虚拟机启动时被标明为启动类的类
7、JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果。REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上7种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

特别备注

本系列文章总结自宋红康详解java虚拟机,相关文字图片素材取自课程中的课件。如有侵权,请联系我删除,谢谢!

你可能感兴趣的:(JVM类加载子系统-JVM笔记(一))