2_类加载子系统

1、类加载过程

在Java中数据类型分为基本数据类型和引用数据类型。
基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载

在这里插入图片描述

1.1、加载(Loading)

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

生成的Class对象的位置(HotSpot虚拟机):
JDK1.7中,在方法区或者说永久代中
JDK1.8中,在方法区或者说元空间中

注意
方法区其实只是个虚拟的概念,方法区具体的实现是永久代或者元空间,1.7是永久代,1.8是元空间】
永久代和元空间最大的区别:JDK7的永久代放在堆中并且独立于堆,JDK8的元空间完全剥离虚拟机,存在于直接内存中

怎么理解Class对象与new出来的对象之间的关系?

每个类都对应有一个Class类型的对象,多个new出来的实例。
每个new出来的对象都是以Class类为模板参照出来的。
为什么可以参照?因为Class对象提供了访问方法区内的数据结构的接口(访问入口)

1.2、链接 (Linking)

验证和加载同时进行,解析环节在初始化之后执行

验证(Verification)

  1. 确保Class文件的字节流中包含信息符合当前虚拟机要求,确保被加载类的正确性,不会危害虚拟机自身安全
  2. 主要包括四种验证:文件格式验证(魔术oxCAFEBABE),元数据验证,字节码验证,符号引用验证
    在这里插入图片描述
  3. 格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。
  4. 格式验证之外的验证操作将会在方法区中进行
  5. 符号引用的验证是在解析阶段进行的。

准备(Preparation)

  1. 为类变量分配内存,并将其初始化为默认值;8种基本类型的默认值+引用类型的null

  2. 这里不包含用static final修饰的基本数据类型字段,因为final在编译的时候就会分配了,准备阶段会显式赋值

  3. 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中

  4. 在这个阶段并不会像初始化阶段中那样有初始化或者代码执行。

  5. 注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的 boolean 的默认值就是false

  6. 使用字面量的方式定义一个字符串常量,是在准备环节直接进行显式赋值

    public static final String constStr="CONST";
    

解析(Resolution)

  1. 将常量池中的符号引号转换为直接引用的过程
  2. 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
  3. 符号引用以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
  4. JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。将其在常量池中的符号引用替换成直接其在内存中的直接引用。
  5. 符号引号有:类和接口的权限定名、字段的名称和描述符、方法的名称和描述符

1.3、初始化(Initialization)

到了初始化阶段,才真正开始执行类中定义的java程序代码

  1. 为类的静态变量赋予正确的初始值
  2. 初始化阶段就是执行类构造器方法<clinit>()的过程。此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来

在这里插入图片描述

  1. 若该类具有父类,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) {
        //这个输出2,则说明父类已经全部加载完毕
        System.out.println(Son.B);
    }
}

  1. Java编译器并不会为所有的类都产生()初始化方法。场景如下:
//哪些场景下,java编译器就不会生成<clinit>()方法
public class InitializationTest1 {
    //场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public int num = 1;
    //场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
    public static int num1;
    //场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public static final int num2 = 1;
}
  1. static与final的搭配问题
    (使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行)

/**
 * 说明:使用static + final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
 * 情况1:在链接阶段的准备环节赋值
 * 情况2:在初始化阶段<clinit>()中赋值
 * 结论:
 * 在链接阶段的准备环节赋值的情况:
 * 1. 对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
 * 2. 对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
 *
 * 在初始化阶段<clinit>()中赋值的情况:
 * 排除上述的在准备环节赋值的情况之外的情况。
 * 最终结论:使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。
 */
public class InitializationTest2 {
    public static int a = 1;//在初始化阶段<clinit>()中赋值
    public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值

    public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值
    public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段<clinit>()中赋值

    public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
    public static final String s1 = new String("helloworld1");//在初始化阶段<clinit>()中赋值

    public static String s2 = "helloworld2";//在初始化阶段<clinit>()中赋值
    public static final int NUM1 = new Random().nextInt(10);//在初始化阶段<clinit>()中赋值
}

6、clinit()的调用会死锁吗?

  1、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕

  2、正是因为函数()带锁线程安全的,因此,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息

class StaticA {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("StaticB");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticA init OK");
    }
}

class StaticB {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("StaticA");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticB init OK");
    }
}

public class StaticDeadLockMain extends Thread {
    private char flag;

    public StaticDeadLockMain(char flag) {
        this.flag = flag;
        this.setName("Thread" + flag);
    }

    @Override
    public void run() {
        try {
            Class.forName("Static" + flag);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(getName() + " over");
    }

    public static void main(String[] args) throws InterruptedException {
        StaticDeadLockMain loadA = new StaticDeadLockMain('A');
        loadA.start();
        StaticDeadLockMain loadB = new StaticDeadLockMain('B');
        loadB.start();
    }
}

1.4、类的生命周期

加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)

初始化(Initialization)
使用(Using)
卸载(Unloading)

2、类加载器及其分类和测试

ClassLoader作用:
ClassLoader是java核心组件。所有Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通ClassLoader去改变链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。

四者之间是包含关系,在下层加载器中,包含着上层加载器的引用。
使用参数 -XX:+TraceClassLoading参数可以追踪类加载过程。

2.0、ClassLoader 和 Launcher的关系

加载器实现的核心类为 Launcher 类,路径:sun.misc

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
    	//一、获取扩展类加载器
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    	//二、根据扩展类加载器获取应用类加载器
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    ...
}    

2.1、启动类加载器,Bootstrap ClassLoader

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

2.2、扩展类加载器,Extension ClassLoader

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

2.3、应用程序类加载器,Application ClassLoader

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

2.4、自定义类加载器(继承ClassLoader实现)

为什么要自定义类加载器?
隔离加载类,修改类加载的方式(根据实际情况在某个时间点按需进行动态加载),扩展加载源、防止源码泄露

  1. 定制类的加载方式(自定义类加载器通常需要继承于 ClassLoader)
  2. 实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源
  3. 自定义类加载器,重写 loadClass 方法 可以破坏双亲委派机制
  4. 如何自定义:
    继承Java.lang.ClassLoader;
    1.2之前重写loadClass(),1.2之后建议把自定义的类加载逻辑写在findClass()里,根据参数指定类的名字,返回对应的Class对象的引用;
    无复杂需求,继承ClassLoader子类URLClassLoader,避免重写findClass()及其获取字节码流的方式。

自定义类加载器示例

public class UserClassLoader extends ClassLoader {
    private String rootDir;

    public UserClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    /**
     * 编写findClass方法的逻辑
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 获取类的class文件字节数组
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //调用defineClass(),将字节数组的数据转化为class实例
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return
     */
    private byte[] getClassData(String className) {
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            // 读取类文件的字节码
            while ((len = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类文件的完全路径
     */
    private String classNameToPath(String className) {
        return rootDir + "\\" + className.replace('.', '\\') + ".class";
    }

    public static void main(String[] args) {
        //此处为class文件所在目录
        String rootDir = "D:\\testJVM\\out\\production\\testJVM\\";

        try {
            //创建自定义的类的加载器1
            UserClassLoader loader1 = new UserClassLoader(rootDir);
            Class clazz1 = loader1.findClass("User");

            //创建自定义的类的加载器2
            UserClassLoader loader2 = new UserClassLoader(rootDir);
            Class clazz2 = loader2.findClass("User");
            //clazz1与clazz2对应了不同的类模板结构
            System.out.println(clazz1 == clazz2);
            System.out.println(clazz1.getClassLoader());
            System.out.println(clazz2.getClassLoader());

            Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("User");
            System.out.println(clazz3.getClassLoader());

            System.out.println(clazz1.getClassLoader().getParent());

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出结果

false
UserClassLoader@131245a
UserClassLoader@15fbaa4
sun.misc.Launcher$AppClassLoader@dad5dc
sun.misc.Launcher$AppClassLoader@dad5dc

分类
在JVM规范中,加载器分为两类:
启动类加载器:使用C/C++语言实现的,嵌套在JVM内部
自定义类加载器:将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器


测试

  1. 每个Class对象都会包含一个定义它的ClassLoader的一个引用
  2. 获取ClassLoader的途径
//(1). 获得当前类的ClassLoader
clazz.getClassLoader()
//(2). 获得当前线程上下文的ClassLoader(系统类加载器)
Thread.currentThread().getContextClassLoader()
//(3). 获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
  1. 数组类的Class对象,不是由类加载器去加载的,而是在Java运行期JVM根据需要自动创建的。对于数组的类加载器来说,是通过Class.getClassLoader()返回的,与数组中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的(基本数据类型由虚拟机预先定义)
public class ClassLoaderDemo {
    public static void main(String[] args) {
        ClassLoader classloader1 = ClassLoader.getSystemClassLoader();
        //sun.misc.Launcher$AppClassLoader@dad5dc
        System.out.println(classloader1);
        //获取到扩展类加载器
        //sun.misc.Launcher$ExtClassLoader@131245a
        System.out.println(classloader1.getParent());
        //获取到引导类加载器 null
        System.out.println(classloader1.getParent().getParent());
        //获取系统的ClassLoader
        ClassLoader classloader2 = Thread.currentThread().getContextClassLoader();
        //sun.misc.Launcher$AppClassLoader@dad5dc
        System.out.println(classloader2);

        String[]strArr=new String[10];
        ClassLoader classLoader3 = strArr.getClass().getClassLoader();
        //null,表示使用的是引导类加载器
        System.out.println(classLoader3);
        ClassLoaderDemo[]refArr=new ClassLoaderDemo[10];
        //sun.misc.Launcher$AppClassLoader@dad5dc
        System.out.println(refArr.getClass().getClassLoader());
        int[]intArr=new int[10];
        //null,如果数组的元素类型是基本数据类型,数组类是没有类加载器的
        System.out.println(intArr.getClass().getClassLoader());
    }
}

3、ClassLoader源码剖析

3.1、ClassLoader与现有类加载器的关系

在这里插入图片描述

3.2、抽象类ClassLoader的主要方法(内部没有抽象方法)

方法名描述
getParent()返回该类加载器的超类加载器
loadClass(String name)加载名称为name的类,返回结果为java.lang.Class类的实例。
findClass (String name)查找名称为name的类,返回结果为java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len)把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例。
resolveClass(Class<?> c)链接(Linking)指定的一个Java类。

一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass() 方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass() 方法生成类的Class对象。

Classloader源码loadClass方法源码

//resolve==true,加载class的同时需要进行解析操作,一般为false
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
	//同步操作,保证只能加载一次
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 在缓存中判断是否已经加载同名的类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
            	//获取当前类的父类加载器
                if (parent != null) {
                	//如果存在父类加载器,则调用父类加载器进行类的加载(双亲委派机制)
                    c = parent.loadClass(name, false);
                } else {
                	//parent==null 父类加载器是引导类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
			// 当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                // 调用当前classloader的findClass
                long t1 = System.nanoTime();
                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;
    }
}

3.3、SecureClassLoader与URLClassLoader

  1. SecureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联
  2. ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,无复杂需求,一般继承URLClassLoader类。

3.4、Class.forName()与ClassLoader.loadClass()对比

  1. Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。
    例如:数据库中的初始化驱动Class.forName(“com.mysql.jdbc.Driver”);
  2. ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。
    如:ClassLoader cl=…;
    cl.loadClass(“XXXX”);

4、双亲委派机制

双亲委派优势:安全、避免重复加载、保护程序、防止核心api被篡改。

4.1、工作原理

  1. 如果一个类加载收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
  3. 如果父类的加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

4.2、源码体现

 双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现,可查看3.2中的源码

  1. 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
  2. 判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name, false)接口进行加载
  3. 反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNull(name)接口,让引导类加载器进行加载
  4. 如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。
  5. 双亲委派的模型就隐藏在这第2和第3步中

如果在自定义的类加载器中重写java.lang.ClassLoader.loadClass(String)或者java.lang.ClassLoader.loadClass(String,boolean)方法,抹去其中的双亲委派机制,仅保留上面四步中的第1步和第4步,那么是不是就能加载核心类库了呢?
答:不行,因为JDK还为核心类库提供了一层保护机制。不管是自定义的类加载器,还是系统类加载器或扩展类加载器,最终都必须调用java,lang,ClassLoader.defineClass(String,byte[],int,int,ProtectionDomain)方法,而该方法会执行preDefineClass()接口,该接口中提供了对JDK核心类库的保护

5、沙箱安全机制

  1. Java安全模型的核心是Java沙箱(sandbox)。沙箱是一个限制程序运行的环境。
  2. 沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并严格限制代码对本地系统资源访问。
  3. 系统资源:CPU、内存、文件系统、网络等。
  4. JDK1.6引入域(Domain)的概念,虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责域关键资源进行交互,而各个应用域则通过系统域的部分代理来对各个需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,1.6安全模型如下:
    在这里插入图片描述

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

6、类相同判定、主动被动使用

  1. 在JVM中表示两个class对象是否为同一个类存在两个必要条件:
    ① 类的全限定名必须一致
    ②加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

6.1、主动适用

主动使用和被动使用的区别在于 是否执行类构造器方法Clinit<>())

①. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化
②. 当调用类的静态方法时,即当使用了字节码 invokestatic 指令。
③. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用 getstatic 或者 putstatic 指令。(对应访问变量、赋值变量操作)
④. 当使用 java.lang.reflect 包中的方法反射类的方法时。比如Class.forName(“XXXX”))
⑤. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
⑥. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类初始化之前,该接口要先被初始化
⑦. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
⑧. 当初次使用MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类
    JDK7开始提供的动态语言支持(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)

//输出	CompareB的初始化  换行 子类初始化......
//注释default方法后,输出	子类初始化......
public class DemoB implements A{
    static{
        System.out.println("子类初始化......");
    }
    public static void main(String[] args) {

    }
}
interface A{
    public static final Thread t = new Thread() {
        {
            System.out.println("CompareB的初始化");
        }
    };
    default void method1(){
        System.out.println("====");
    }
}

补充说明
在初始化一个类时,并不会先初始化它所实现的接口
在初始化一个接口时,并不会初始化它的父接口
只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

//如果输出语句执行了,表明进行了初始化
interface  CompareA{
    public static final Thread t=new Thread(){
        {
            System.out.println("CompareA的初始化");
        }
    };
    public static final int NUM=1;
}

6.2、被动使用

其他使用Java类的方式都被看做是对类的被动引用,都不会导致类的初始化。没有<clinit>()调用

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
    当通过子类引用父类的静态变量,不会导致子类初始化
  2. 通过数组定义类引用,不会触发此类的初始化
  3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了
  4. 调用ClassLoader类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
// 这里不会进行初始化,因为相当于parent只开辟了空间,没赋值
Parent[] parent=new Parent[10];

7、类的卸载

7.1、类、类的加载器、类的实例之间的引用关系

  1. 在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系
  2. 一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象
    在这里插入图片描述

7.2、举例

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

如上图所示,loader1变量和obj变量 间接引用代表Sample类的Class对象,而objClass变量则直接引用它。

如果程序运行过程中,将上图左侧三个引用变量都置为 null,此时Sample对象结束生命周期。MyclassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。

资料方法区的垃圾回收——不再使用的类型,查看文中7.4

当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接引用,不再重新加载;如果不存在Sample类,会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可通过哈希码查看是否是同一个实例)

7.3、类的卸载

  1. 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
  2. 被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小
  3. 开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。

8、Java9新特性

  1. JDK9前的双亲委派模式:只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
  2. 在委派给父加载器加载前,先判断该类是否能归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器加载。
  3. 扩展类加载器(Ext ClassLoader)替换为 平台类加载器(Platfrom ClassLoader)
  4. 平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader。
    现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader(往上SecureClassLoader,再上ClassLoader)。
    在这里插入图片描述
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值