深入理解Java虚拟机(四)——类加载机制

前言

上一节《深入理解Java虚拟机(三)——Class文件结构》知道了 Class 文件的结构,那么拟机是如何加载这些 Class 文件的?

1 什么是类加载机制

  • 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

2 类加载的时机与过程

  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、解析称为连接。如下图所示:
    类的生命周期
2.1 加载
  • “加载”是“类加载”过程的一个阶段。

作用:

  • 通过一个类的全限名来获取定义此类的二进制流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的Class对象,作为这个类的各种数据的访问入口
2.2 验证
  • 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。

主要完成以下四个阶段的验证:

  • 文件格式验证:字节流是否符合 Class 文件格式规范,并且能被当前版本虚拟机处理。通常包括魔数验证、版本号验证、常量池中常量是否有不被支持的类型(tag标志)等。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。通常有判断类是否有父类(除了 java.lang.Object 之外,其他所有类都有父类),类中的字段、方法是否与父类产生矛盾。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如:保证跳转指令不会跳转到方法之外的字节码指令、保证方法体中的类型转换是有效的。
  • 符号引用验证:对类自身引用以外(常量池中的各种符号引用)的信息进行匹配性校验。通常有符号引用通过字符串描述的全限名是否能找到类、在指定类在是否存在符合方法的字段描述符以及简单名称所描述的方法和字段、符号引用种的类、字段、方法的访问下是否可以被当前类引用。

注:对于虚拟机的类加载机制来说,验证是非常重要的,但不是一点必要的阶段。如果所运行的代码都已经被反复使用和验证过,那么可以使用 -Xverify:none 参数来关闭大部分类的验证。

2.3 准备
  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

注:
(1)在准备阶段分配的内存进包括类变量(被static修饰的变量),实例变量还在在对象实例化时随对象分配在 Java 堆中。
(2)一般情况下初始化值是堆变量初始化默认值,并非赋值。如 public static int value =1; value在准备阶段的初始值为 0 而不是 1,因为这时候尚未开始执行任何 Java 方法,赋值的指令 putstatic 是程序被编译后,存放于类构造器 () 方法中,所以在赋值是在初始化阶段才会执行。而如果类字段被 ConstantValue 属性所指定,如:public static final int value =1; 编译时 javac 将会为 value 生成ConstantValue 属性,在准备阶段虚拟机就会赋值为 1.

2.4 解析
  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  • 解析主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
2.5 初始化
  • 初始化阶段是执行类构造器 <clinit>() 方法的过程。

<clinit>() 特点:

  • <clinit>() 方法是由编译器自动收集类中的所有类变量的复制动作和静态语句块中的语句合并产生,编译器收集的顺序是由语句在源文件在出现的顺序锁具顶,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他后的变量可以赋值。但不能访问。
public class ClinitTest {
   static {
   	i = 1; 	//变量赋值可通过编译
   	System.out.println(i); //引用变量会编译错误
   }
   static int i = 0;
}
  • <clinit>() 方法与类的构造函数(或者说实例构造器 <init>() 方法)不同,它不需要显示调用父类构造器,虚拟机会保证在子类的 <clinit>() 方法执行之前,父类的 <clinit>() 方法已经执行完毕。因此在虚拟机中第一个被执行的 <clinit>() 方法的类为 java.lang.Object。
  • 由于父类的 <clinit>() 先执行,也就亦为之父类中定义的静态语句块要优先于子类的变量赋值操作。
  • <clinit>() 方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法。
  • 接口中不能使用静态语句块,但仍有变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法,但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行夫接口 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
  • 虚拟机会保证一个类的 <clinit>() 方法在多线程环境在被正确地加锁、同步,<clinit>() 只会被一个线程执行,其它线程需要等待。 在这里插入图片描述

注:
1、类的初始化只会下以下几种情况:
(1)使用new关键字实例化对象的时候、
(2)读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)
(3)调用一个类的静态方法的时候
(4)使用java.lang.reflect包的方法对类进行反射调用的时候
(5)当初始化一个类时,如果父类还没初始化,则先初始化父类
(5)当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类
(6)当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
2、不会被初始化的情况(部分):
(1)通过子类引用父类的静态字段,子类不会被初始化

class Parent {
	static {
		System.out.println("父类init...");
	}
	static int i = 1;
}

class Sun extends Parent {
	static {
		System.out.println("子类init...");
	}

}

public class ClinitTest2 {

	public static void main(String[] args) {
		System.out.println(Sun.i);
	}
}
//执行结果:
父类init...
1

(2)通过数组定义引用类

class Parent {
	static {
		System.out.println("父类init...");
	}
	static int i = 1;
}

public class ClinitTest2 {

	public static void main(String[] args) {
		Parent[] p = new Parent[10];	
	}
}
//无输出

(3)调用类的常量(final修饰)

class Parent {

	final static int A = 1;
	static {
		System.out.println("父类init...");
	}
	static int i = 1;
}
/**
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用定义常量的类,因此不会出发定义常量的类的初始化
**/
public class ClinitTest2 {

	public static void main(String[] args) {
		System.out.println(Parent.A);
	}
}
//输出结果
1

3 类加载器

Java自身提供了三个类加载器:

  1. BootStrap ClassLoader(启动类加载器):负责将 %JavaHome%\lib 目录下,或 -Xbootclasspath 参数指定的路径中的,并且时虚拟机识别的类库加载到虚拟机中。启动类加载器无法被 Java 程序直接引用,用户在自定义类加载器时,如果需要把加载请求委托给引导类加载器,那么直接可以使用 null 代替。
// 查看 BootStrap ClassLoader 所加载的文件
public class ClassLoaderTest {

	public static void main(String[] args) {
		System.out.println(System.getProperty("sun.boot.class.path"));
	}
}
// 结果输出:
D:\soft\jdk\jdk1.8.0_25\jre\lib\resources.jar;
D:\soft\jdk\jdk1.8.0_25\jre\lib\rt.jar;
D:\soft\jdk\jdk1.8.0_25\jre\lib\sunrsasign.jar;
D:\soft\jdk\jdk1.8.0_25\jre\lib\jsse.jar;
D:\soft\jdk\jdk1.8.0_25\jre\lib\jce.jar;
D:\soft\jdk\jdk1.8.0_25\jre\lib\charsets.jar;
D:\soft\jdk\jdk1.8.0_25\jre\lib\jfr.jar;
D:\soft\jdk\jdk1.8.0_25\jre\classes
  1. Extension ClassLoader(扩展类加载器):负责加载%JavaHome%\lib\ext 目录下,或者被 java.ext.dirs 系统变量所指定路径中的所有类库。
// 查看 Extension ClassLoader 所加载文件
public class ClassLoaderTest {

	public static void main(String[] args) {
		System.out.println(System.getProperty("java.ext.dirs"));
	}
}
// 结果输出:
D:\soft\jdk\jdk1.8.0_25\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
  1. Application ClassLoader(应用程序类加载器):也称为系统类加载器,负责加载当前应用下 classpath 目录下的所有类。
// 查看 Application ClassLoader 所加载的文件
public class ClassLoaderTest {

	public static void main(String[] args) {
		System.out.println(System.getProperty("java.class.path"));
	}
}
// 输出结果为项目的目录

类加载器的源码分析 sun.misc.Launcher:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // 创建扩展类加载器
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // 创建应用程序类加载器
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        //设置AppClassLoader为线程上下文类加载器
        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}

Launcher初始化了ExtClassLoader和AppClassLoader,并将AppClassLoader设置为线程上下文类加载器。

ExtClassLoader源码:

/*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {

        static {
            ClassLoader.registerAsParallelCapable();
        }

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if (s != null) {
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }
 
......
    }

可以看到扩展类加载器加载的为 java.ext.dirs 路径下的类。

AppClassLoader源码 :

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {


        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

     
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

        ......
    }

可以看到应用程序类加载器加载的为java.class.path 路径下的类。

它们的继承关系如下:
在这里插入图片描述
从代码实例可以看到AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。

3.1 双亲委派
  • 一个类加载器查找 class 和 resource 时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
    在这里插入图片描述
    如图所示,如过一个类加载收到了类加载的请求,首先不会自己去加载这个类,而是将类委托给父类加载器完成,层层如此,直至最终都传至启动类加载器中,只有当父加载器未找到这个类时,才会让子加载器加载。

实现双亲委派的源码:java.lang.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) {
                // 如果父类加载器无法加载,自身才尝试加载
                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;
    }
}

通过源码可以清晰的看出双亲委派模式的加载机制,此加载方式最大的优点为 Java 类随着它的类加载器一起具备了带有优先级的层级关系。例如 java.lang.Object 存放在 rt.java 中,无论哪个类加载器要加载这个类,都会交给顶端的启动类加载器加载,因此 Object 类在各种类加载器环境中都为同一个类。

参考

一看你就懂,超详细java中的ClassLoader详解

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值