Java类加载器和加载机制

类加载器负责在运行时将Java类动态加载到JVM(Java虚拟机)。此外,它们是JRE(Java运行时环境)的一部分。因此,由于类加载器,JVM不需要知道底层文件或文件系统以运行Java程序。

此外,这些Java类不会同时加载到内存中,而是在应用程序需要时。这就是类加载器的用武之地,他们负责将类加载到内存中。

1. 类加载器的层级结构

让我们首先学习如何使用各种类加载器使用,一个简单示例加载不同的类:

public class TestMain {

    public static void main(String[] args) {
        System.out.println("1: " + People.class.getClassLoader());
        System.out.println("2: " + Logging.class.getClassLoader());
        System.out.println("3: "+new AppletEvent("w",1,"3").getClass().getClassLoader());
    }
}

输出:
1: sun.misc.Launcher$AppClassLoader@135fbaa4
2:sun.misc.Launcher$ExtClassLoader@cc34f4d
3: null

我们可以看到,这里有三种不同的类加载器:AppClassLoader,ExtClassLoader,还有一个显示为空的 BootstrapClassloader。

对于AppletEvent,它在输出中显示为null。**这是因为引导类加载器是用C++而不是Java编写的,因此它不会显示为Java类。**由于这个原因,引导类加载器的行为在不同的JVM实现之间会有所不同。

  • BootstrapClassloader:它主要负责加载JDK内部类,通常是rt.jar和位于*$ JAVA_HOME / jre / lib* 目录中的其他核心库。此外,Bootstrap类加载器充当所有其他ClassLoader实例的父级
  • ExtClassLoader:扩展类加载器从JDK扩展目录加载,通常是*$ JAVA_HOME / lib / ext目录或java.ext.dirs*系统属性中提到的任何其他目录。
  • AppClassLoader:系统或应用程序类加载器负责将所有应用程序级别类加载到JVM中。它加载在类路径环境变量-classpath或-cp命令行选项中找到的文件。此外,它是Extensions类加载器的下属级别。
2. 类加载器如何工作?

类加载器是Java运行时环境的一部分。当JVM请求类时,类加载器会尝试使用 完全限定的类名 来定位类并将类定义加载到运行时。java.lang.ClassLoader.loadClass() 方法是负责将类加载为运行时的方法,它尝试基于完全限定名称加载类。

如果当前类加载器尚未加载该类,它会将请求委托给父类加载器。此过程以递归方式发生。

最终,如果父类加载器没有找到该类,则子类将调用 java.net.URLClassLoader.findClass() 方法来查找文件系统本身中的类。如果最后一个子类加载器也无法加载该类,则会抛出 java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException

在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 {
                        //如果这个类没有上级类加载器,那么则使用BootstrapClassLoader来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 如果仍然为空,则调用 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) {
                 // 解析class文件,就是将符号引用替换为直接引用的过程
                resolveClass(c);
            }
            return c;
        }
    }

总结上面的加载步骤:

  1. 首先会检查当前传入的类全路径名是否在缓存中存在,即已经被加载;
  2. 如果没有被加载,检查当前类加载器的上级类加载器是否存在,如果存在,则使用上级类加载器继续调用loadClass()方法继续该过程;如果不存在,那么则使用BootstrapClassLoader来加载;
  3. 经过上面的类加载器过程,如果还没有能够加载该类,下面会再查询一下缓存中是否有该类。
  4. 如果resolve参数为true,则 寻找是否存在符号引用,如果有就解析class文件,将符号引用替换为直接引用。

类加载原理:
在这里插入图片描述

类加载有3个原则:

  1. 委托模型

    Java虚拟机和Java ClassLoader使用称为 委派层次结构算法的算法 将类加载到Java文件中。

    ClassLoader基于委托模型给出的一组操作来工作。他们是:

    • ClassLoader始终遵循委托层次结构原则
    • 每当JVM遇到一个类时,它会检查该类是否已经加载;
    • 如果已经在方法区域中加载了Class,则JVM继续执行;
    • 如果该方法区域中不存在该类,则JVM要求Java ClassLoader子系统加载该特定类,然后ClassLoader子系统将控件移交给Application ClassLoader
    • Application ClassLoader将请求委托给Extension ClassLoader,Extension ClassLoader依次将请求委托给Bootstrap ClassLoader
    • Bootstrap ClassLoader将在Bootstrap类路径(JDK / JRE / LIB)中进行搜索。如果该类可用则加载它,否则请求被委托给Extension ClassLoader;
    • Extension ClassLoader在Extension Classpath(JDK / JRE / LIB / EXT)中搜索类。如果该类可用则加载它,否则请求被委托给Application ClassLoader;
    • Application ClassLoader在Application Classpath中搜索类。如果该类可用则会加载它,否则会生成ClassNotFoundException异常。

    委托模型被我们翻译为双亲委派模型,这是正好 AppClassLoader 上面有两个类加载器,如果有三个或者四个,这种叫法就不合适。个人认为翻译为双亲不是太合适,因为这两个类加载器并不是平级的关系。

  2. 可见性原则

    可见性原则声明由父类ClassLoader加载的类对子类ClassLoader可见,但子类ClassLoader加载的类对子类ClassLoader不可见。假设扩展ClassLoader已加载类Person.class,那么该类仅对Extension ClassLoader和Application ClassLoader可见,但对Bootstrap ClassLoader不可见。如果再次尝试使用Bootstrap ClassLoader加载该类,则会给出异常 java.lang.ClassNotFoundException

  3. 唯一性属性

    Uniquesness属性确保类是唯一的,并且不会重复类。这也确保子类加载器不加载由父类加载器加载的类。如果父类加载器无法找到该类,则只有当前实例本身才会尝试这样做。

在JVM请求类之后,要遵循几个步骤来加载类。根据委托模型加载类,但有一些重要的方法或函数在加载类时起着至关重要的作用。

  1. loadClass(String name,boolean resolve):此方法用于加载JVM引用的类,它将类的名称作为参数;
  2. defineClass():该方法是一个final方法,不能被覆盖。此方法用于将字节数组定义为类的实例。如果该类无效,则抛出ClassFormatError
  3. findClass(String name):此方法用于查找指定的类,此方法仅查找但不加载类;
  4. findLoadedClass(String name):此方法用于验证JVM引用的Class是否先前已加载;
  5. Class.forName(String name,boolean initialize,ClassLoader loader):此方法用于加载类以及初始化类。此方法还提供了选择任何一个ClassLoader的选项。如果ClassLoader参数为NULL,则使用Bootstrap ClassLoader。
3. ClassLoader 如何加载出类加载器的层级结构

Java类由java.lang.ClassLoader的实例加载。在Classloader中并没有看到关于类加载器层级结构的相关代码,那么这一部分是怎么实现的呢?

我们接着看ClassLoader的一个构造方法:

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

主要看getSystemClassLoader()方法:
在这里插入图片描述
在Classloader初始化的时候,会初始化系统类加载器。通过sclSet变量来控制只初始化一次,并且如果在进入initSystemClassLoader方法时发现sclSet如果为true则抛出异常。接着调用了Launcher类:

sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

点击进入Launcher的构造方法,因为sun.misc包中的源码在src.zip中默认是没有的,我是下载了openJDK源码查看到的。

先看一下Launcher是如何初始化的:
在这里插入图片描述

通过getLauncher()方法获取 launcher对象,launcher通过构造方法初始化,并且launcher对象是static类型的,意味着Launcher类是单例的。下面来看Launcher的构造方法:

在这里插入图片描述
从上面的初始化过程可以看出:

  1. 首先会创造扩展类加载器。扩展类加载器会去加载哪些类呢?在getExtClassLoader() ---> getExtDirs()方法中告诉了我们:

    String s = System.getProperty("java.ext.dirs");
    

    获取的是java.ext.dirs目录下的所有类文件。

  2. 第二步是创建用于启动应用程序的类加载器。同样方式与第一步大同小异,加载的路径如下:

    final String s = System.getProperty("java.class.path");
    
  3. 第三步为当前线程设置上下文类加载器。

  4. 最后,根据请求安装安全管理器。

在ExtClassLoader初始化的过程中,将 父类加载器设置成了null,因为BootstrapClassLoader是C++编写,对于Java本身来说它是不存在的。所以在此处将ExtClassLoader的父加载器设置为空,表示他的父类加载器就是启动类加载器。
在这里插入图片描述

而下面的AppClassLoader在初始化设置父类加载器的时候,将 上面获取到的 ExtClassLoader作为他的父类加载器,最终的加载器层级状态就如我们开头所描述的这样,构成了一个层级结构。

而到这里我们也可以解释从 ClassLoader 到 Launcher, 类的加载器从初始化层级结构到使用的过程。最终从 Launcher中返回来的是AppClassLoader,即应用层级的类加载器。
在这里插入图片描述

4. 自定义ClassLoader

正常情况下我们都会按照预设的目录使用JDK自带 的类加载器进行加载,比如Spring搭建的工程中会扫描指定包下所有的类,那么什么时候会使用到自定义类加载器呢?

  • 当我们的类文件不是按照指定目录存放的时候,即该目录未配置到可以被AppClassLoader扫描的路径中;
  • 当你从网络中读取字节流转成类文件的时候,如何处理这个类的逻辑也应该是自己实现;
  • 实现简易的热部署模式,自定义类加载器,定时加载新的类文件。

以上这些场景下都可以用到自定义的类加载器。实现自定义类加载器需要继承ClassLoader类,重写其中的关键方法。

关键方法
  • findClass(String name):实际执行加载二进制流的具体行为方法,这个方法顾名思义负责查找一个类并返回它。对我们自定义而言,这是我们最需要关注的。
  • loadClass(String name):这个方法中主要负责协调加载类,通常它的逻辑比较固定,我们可以不去重写。这是类加载器执行加载类逻辑的方法,包括检查是否已经加载,调用父类加载,失败则自己尝试使用 findClass方法加载。
  • defineClass(String name, byte[] b, int off, int len):负责定义类,这个方法我们主要调用就好了。

所以我们需要实现的步骤就是:

  1. 实现findClass方法,从你指定的位置加载类文件;
  2. 然后调用defineClass将类文件字节流转为对应的类对象即可。

实现代码如下:

package com.rickiyang.learn;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

/**
 * @author rickiyang
 * @date 2019-09-05
 * @Desc 加载自定义类文件
 * 读取编译好的.class文件,使用自定义的classLoader进行加载
 */
public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
        super(CustomClassLoader.class.getClassLoader());
        this.classPath = classPath;
    }

    @Override
    public Class<?> findClass(String name) {
        //检查路径是否可用
        if (classPath == null) {
            throw new IllegalArgumentException("Please set class path.");
        }
        //加载class文件数据
        byte[] classData = loadClassData(classPath);
        if (classData == null) {
            throw new NullPointerException(" Please check class file path.");
        }
        // 将class的字节数组解码为Class实例
        return defineClass(name, classData, 0, classData.length);
    }

    /**
     * 读取Class文件
     */
    private byte[] loadClassData(String path) {
        byte[] bytes = new byte[1024];
        int length = 0;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        File classFile = new File(path);
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(classFile);
            while ((length = fis.read(bytes)) != -1) {
                byteStream.write(bytes, 0, length);
                byteStream.flush();
            }
            return byteStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis == null) {
                    throw new NullPointerException("null");
                }
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public void setClassPath(String classPath) {
        this.classPath = classPath;
    }
}


/**
 * 具体使用方式
 */
class MainClass {
    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader("");
        try {
            classLoader.setClassPath("D:\\workspace\\springboot-learn\\single-database\\target\\classes\\com\\rickiyang\\learn\\TestClass.class");
            Class clazz = classLoader.loadClass("com.rickiyang.learn.TestClass");
            //反射来调用方法
            Method method = clazz.getDeclaredMethod("doSomething");
            System.out.println(clazz.getSimpleName());
            System.out.println("result = " + method.invoke(clazz.newInstance()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class TestClass {
    public int doSomething() {
        return 1;
    }
}

上面这个案例的正确使用方式是你需要提前将TestClass.java 编译成.class文件,然后才能使用。

热部署实现

既然可以手动的通过使用自定义类加载器的方式来加载类对象,那么我们是否可以想到如何实现一个简单的热加载呢?原理其实很简单:通过上面的类加载器方法定时的扫描指定文件夹的所有.class文件即可。

定义类加载器:

package com.rickiyang.learn.utils;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Set;

/**
 * @author rickiyang
 * @date 2019-09-06
 * @Desc 自建一个类加载器, 可以用来加载自己的类
 */
public class SelfClassLoder extends ClassLoader {
    //用于读取.Class文件的路径
    private String swapPath;
    //用于标记这些name的类是先由自身加载的
    private Set<String> useMyClassLoaderLoad;

    public SelfClassLoder(String swapPath, Set<String> useMyClassLoaderLoad) {
        this.swapPath = swapPath;
        this.useMyClassLoaderLoad = useMyClassLoaderLoad;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c == null && useMyClassLoaderLoad.contains(name)) {
            //特殊的类让我自己加载
            c = findClass(name);
            if (c != null) {
                return c;
            }
        }
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) {
        //根据文件系统路径加载class文件,并返回byte数组
        byte[] classBytes = getClassByte(name);
        //调用ClassLoader提供的方法,将二进制数组转换成Class类的实例
        return defineClass(name, classBytes, 0, classBytes.length);
    }

    private byte[] getClassByte(String name) {
        String className = name.substring(name.lastIndexOf('.') + 1) + ".class";
        try {
            FileInputStream fileInputStream = new FileInputStream(swapPath + className);
            byte[] buffer = new byte[1024];
            int length = 0;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while ((length = fileInputStream.read(buffer)) > 0) {
                byteArrayOutputStream.write(buffer, 0, length);
            }
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[]{};
    }
}

创建一个定时器程序,2s同步使用自定义类加载器加载指定类:

package com.rickiyang.learn;

import com.google.common.collect.Sets;
import com.rickiyang.learn.utils.SelfClassLoder;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @author rickiyang
 * @date 2019-09-06
 * @Desc 测试自己的类加载器
 * 注意这里的SelfClassLoder必须要和Test类在同一包下,不然找不到Test类的路径
 * <p>
 * 测试方式:
 * 启动定时任务首先会打印:当前版本是2哦
 * 然后你手动将 2 改为1 编译一下Test类,或者提前编译好,替换掉Test.class,
 * 就可以看到自动加载了这个新的类文件了
 * 这种方式可以用于我们热更新某些类文件
 */
public class SelfClassLoderTest {

    public static void main(String[] args) {
        //创建一个2s执行一次的定时任务
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                String swapPath = SelfClassLoder.class.getResource("").getPath();
                String className = "com.rickiyang.learn.Test";

                //每次都实例化一个ClassLoader,这里传入swap路径,和需要特殊加载的类名
                SelfClassLoder myClassLoader = new SelfClassLoder(swapPath, Sets.newHashSet(className));
                try {
                    //使用自定义的ClassLoader加载类,并调用printVersion方法。
                    Object o = myClassLoader.loadClass(className).newInstance();
                    o.getClass().getMethod("printVersion").invoke(o);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, 0, 2000);
    }
}

5. 类加载的阶段

类从被加载到内存中开始,直到被从内存中卸载为止,它的整个生命周期包括:验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如下图所示:

在这里插入图片描述

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

实际上,什么时候开始执行类加载的第一个阶段:加载,这点Java虚拟机并没与强制约束,但是对于初始化阶段,虚拟机规范则是严格规定了 有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)。这里的5种情况,通常被称为类的主动引用。除此之外,所有类引用的方式都不会触发类的初始化,称为被动引用。

主动引用的5种类型
  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

下面详细的看一下类加载的几个过程:

1. 加载

这里的加载只是类加载中的一个过程,更好理解的说法应该叫做装载。如果要加载一个类型,虚拟机必须要完成3件事情。

  1. 通过一个类的全限定名获取定义此类的二进制字节流。
  2. 解析这个二进制流为方法区的运行时数据结构。
  3. 创建一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

由于虚拟机并没有指明二进制字节流要从一个Class文件中获取,准确的说没有规定从哪里获取怎样获取,以下都是可能获取的方式之一:

  • 从本地文件系统加载一个Java class 文件。
  • 通过网络下载一个Java class文件。
  • 从ZIP、JAR、EAR、WAR格式的压缩文件中获取。
  • 运行时生成,JDK提供的动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为*$Proxy的代理类的二进制字节流。
  • 由数据库中读取或者其它文件生成,如JSP等等。

有了这些二进制的字节流文件后,就可以使用类加载器进行加载了。在加载阶段我们既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。Java有关类加载器的相关知识点在本文中不做过多叙述,后续另起一篇博文再做介绍。

加载阶段完成之后,JVM就会把二进制字节流就按照自己所需的格式存储在方法区之中。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面)。

加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。如Java类加载器在加载一个二进制文件时会执行一个checkName()的方法,用于校验文件是否为空或者是否是有效的二进制名称。

2. 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。一般验证阶段会完成4个阶段的验证工作:

  1. 文件格式验证;
  2. 元数据验证;
  3. 字节码验证;
  4. 符号引用验证。

文件格式验证主要是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型等等。该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

元数据验证是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。如这个类是否有父类、是否继承了不允许被继承的类,类中的字段、方法是否与父类产生矛盾等等。该阶段主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

字节码验证是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。如保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。

符号引用验证主要是在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证的目的是确保解析动作能正常执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3. 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int v = 1;

那变量v在准备阶段过后的初始值为0而不是1。因为这时候尚未开始执行任何java方法,而把v赋值为1的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把v赋值为1的动作将在初始化阶段才会执行。

至于“特殊情况”是指:

public static final int v=1;

即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为1而非0。

4. 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

  • 符号引用(Symbol References): 符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须一致,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
  • 直接引用(Direct References): 直接引用可以是直接目标的指针相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局有关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。

我们通过一个例子来实际看一下在解析的过程中各个部分的代码块是采用什么样的先后顺序来加载的,

我使用的开发工具是IDEA,在工具栏:run-edit configutations-vm options 菜单中配置如下参数:

-XX:+TraceClassLoading,这样在代码调用时会跟踪类的加载情况。

案例:

一个父类,一个子类,一个并行的类。测试子类和父类的加载顺序,静态代码块和非静态代码块的加载时机。

package com.rickiyang.learn;

/**
 * @author rickiyang
 * @date 2019-09-10
 * @Desc TODO
 */
public class TestClassLoadSequence extends BaseClass {
    {
        System.out.println("这里是子类的普通代码块");
    }

    public TestClassLoadSequence() {
        System.out.println("这里是子类的构造方法");
    }

    @Override
    public void print() {
        System.out.println("这里是子类的普通方法");
    }

    public static void print1() {
        System.out.println("这里是子类的静态方法");
    }

    static {
        System.out.println("这里是子类的静态代码块");
    }

    public static void main(String[] args) {
        BaseClass bcb = new TestClassLoadSequence();
        bcb.print();
    }

    Test2 o = new Test2();
}

class BaseClass {

    public BaseClass() {
        System.out.println("这里是父类的构造方法");
    }

    public void print() {
        System.out.println("这里是父类的普通方法");
    }

    public static void prin1t() {
        System.out.println("这里是父类的静态方法");
    }

    static {
        System.out.println("这里是父类的静态代码块");
    }

    Other2 o2 = new Other2();

    {
        System.out.println("这里是父类的普通代码块");
    }
}

class Test2 {
    Test2() {
        System.out.println("初始化子类的属性值");
    }
}

class Other2 {
    Other2() {
        System.out.println("初始化父类的属性值");
    }
}

这个例子比较简单,在运行代码之前分析一下:带有static关键字的代码块应该是最先执行,其次是非static关键字的代码块以及类的属性(Fields),最后是构造方法。带上父子类的关系后,上面的运行结果为:

这里是父类的静态代码块
这里是子类的静态代码块
初始化父类的属性值
这里是父类的普通代码块
这里是父类的构造方法
这里是子类的普通代码块
初始化子类的属性值
这里是子类的构造方法
这里是子类的普通方法

Process finished with exit code 0

注意的是类的属性与非静态代码块的执行级别是一样的,谁先执行取决于书写的先后顺序。
结论:父类的静态代码块->子类的静态代码块->初始化父类的属性值/父类的普通代码块(自上而下的顺序排列)->父类的构造方法->初始化子类的属性值/子类的普通代码块(自上而下的顺序排列)->子类的构造方法。
注:构造函数最后执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值