JVM类的加载机制

 

 

在学习之前,我们现象几个问题:

1.JVM的类加载机制是什么?

2.它是如何实现的其功能的?

3.在什么情况下在使用类加载器?

带着这几个问题,我们一步一步深入学习一下。

一、什么是类加载机制?

  虚拟机把描述类的数据,从class文件(即一组以8位字节为基础单位的二进制流)加载到内存中,并对数据进行各种处理,最终生成能够直接被虚拟机识别的Java类型;

它分为以下几个步骤:

1.加载

加载是第一个阶段,主要完成了三件事:

1)通过类的全名,获取类的二进制数据流;其实就是class文件;

2)将获取到的class文件解析为方法区内的数据结构;

3)创建Java.lClass类的实例,表示该类型;

2.连接

这个过程包含了:验证、准备、解析三个过程;

1.验证:

验证被加载的类是否有正确的内部结构,它包含:

1.文件格式的验证

主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

2.元数据验证

对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

3.字节码验证

最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

4.符号引用验证

主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

2.准备

类准备阶段负责为类的静态变量分配内存,并设置默认初始值。这些变量所使用的内存都是在方法区中分配的,但是要注意,准备阶段所分配内存的变量都是被static修饰的,如果是实例变量或者其他局部变量,那会随着对象的实例化在堆内存中分配

3.解析

解析就是把class文件中的符号引用转换成直接引用,符号引用就是类的全限定名,可以是任意的字面量,引用的目标不一定已经加载到内存中,而直接引用就是直接指向目标的指针,引用对象一定需要已经被加载到内存中;Java中的多态(动态绑定)其实就是跟类的解析有关,类的解析可能发生在程序运行期间(类初始化之后),因为对于多态来说在类的加载,验证,准备过程中并不知道实际要调用哪一个对象的方法,只有在执行代码的时候才知道实际需要执行哪一个对象的方法

3.初始化

1.类初始化是类加载过程的最后一步了,初始化其实就是执行构造器的过程,构造器是JVM自动生成的,它是去自动搜集类的变量,静态代码块中的语句合并产生的;

2.<clinit>()和类的构造函数不同,JVM会保证子类执行<clinit>()方法之前会先执行父类的<clinit>()方法,不需要想构造函数一样需要显式的调用父类的构造函数,所以Object的<clinit>()一定是最先执行的

3.<clinit>()不是必须的,如果类中没有对变量进行赋值操作,也没有静态代码块,那么就没有<clinit>()方法

4.虚拟机会保证<clinit>()方法在多线程的环境下同步执行,所以如果多线程同时去初始化一个类,那么同一个时刻只有一个线程去执行<clinit>()方法,其他线程都会等待,如果在一个类<clinit>()方法中有耗时人物,可能造成多线程阻塞,例如在静态代码块中执行耗时操作

public class LoadTest {

	public static String name;
	public static void main(String[] args) {
		System.out.println("我是一个测试类");
	}
}

 其对应的构造函数如下

 public com.example.demo.web.LoadTest();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>
:()V
        4: return
     LineNumberTable:
       line 3: 0

 public static void main(java.lang.String[]);

扩展:

<clinit>和<init>方法的区别?

<clinit>是在类的初始化进行调用的方法,用于加载类的静态变量和静态块;

<init>是在创建对象时候调用的,即new的时候调用的方法。

那么<clinit>在什么情况下调用呢?

即在类加载的初始化过程中调用,那什么时候class会被初始化?

Java虚拟机规定:一个类和接口在初次使用前,必须要进行初始化,这里的‘使用’是指主动使用。有一下6种情况:

1.创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、序列化;

2.当调用类的静态方法时,即使用了字节码invokestatic指令;

3.当时用类或接口的静态字段时(final修饰的除外),比如:getstatic和putstatic指令;

4.当时用Java.lang.reflect包中的反射类的方法时;

5.当初始化子类时,先初始化父类;

6.启动虚拟机,含有main方法的类

除了这六种情况是主动使用外,其他都属于被动使用,被动使用不会引起类的初始化;

 

在这一阶段,主要使用了类加载器来实现的,那我们也顺带学习一下类加载器:

 

先说一下JVM启动时都做了什么?

1.在启动JVM的同时将加载Bootstrap ClassLoader(启动类加载器,使用C/C++编写,属于JVM的一部分);

2.通过Bootstrap ClassLoader加载sun.misc.Launcher类(ExtClassLoader和AppClassLoader是它的内部类);

3. sun.misc.Launcher类在执行初始化阶段时,会创建一个自己的实例,在创建过程中会创建一个ExtClassLoader(扩展类加载器)实例、一个AppClassLoader(系统类加载器)实例,并将AppClassLoader实例设置为主线程的ThreadContextClassLoader(线程上下文类加载器)。

4. 然后AppClassLoader实例就开始加载Main.class及其所依赖的类库了。

既然我们都知道了加载器的作用,那么我们来看一下它们加载的路径:

1.首先我们来看一下启动类加载器:

public class LoadTest {

	private static URL[] urLs;

	public static void main(String[] args) {
		urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
		for(URL url:urLs) {
			System.out.println(url);
		}
	}

输出:

file:/D:/JavaEnvironment/JDK-1.8/jre/lib/resources.jar
file:/D:/JavaEnvironment/JDK-1.8/jre/lib/rt.jar
file:/D:/JavaEnvironment/JDK-1.8/jre/lib/sunrsasign.jar
file:/D:/JavaEnvironment/JDK-1.8/jre/lib/jsse.jar
file:/D:/JavaEnvironment/JDK-1.8/jre/lib/jce.jar
file:/D:/JavaEnvironment/JDK-1.8/jre/lib/charsets.jar
file:/D:/JavaEnvironment/JDK-1.8/jre/lib/jfr.jar
file:/D:/JavaEnvironment/JDK-1.8/jre/classes

上面的结果,跟我们在加载器开始的图上的路径对应起来了,那么我们再来验证一下扩展类加载的过程:

package com.example.demo.test;

import java.net.URL;
import java.net.URLClassLoader;

public class Test {
    private static URL[] urLs;
    public static void main(String[] args) {
        //urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        URLClassLoader extClassLoader=(URLClassLoader)ClassLoader.getSystemClassLoader().getParent();
        urLs=extClassLoader.getURLs();
        for(URL url:urLs) {
            System.out.println(url);
        }
    }
}

它的输出结果:

file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/access-bridge-64.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/cldrdata.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/dnsns.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/jaccess.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/jfxrt.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/localedata.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/nashorn.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/sunec.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/sunjce_provider.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/sunmscapi.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/sunpkcs11.jar
file:/D:/jdk-1.8/jdk1.8/jre/lib/ext/zipfs.jar

也跟图中的路径一样,这样我们就验证了每个加载器中的加载位置;

对于应用类加载器,我们也以另一种方式试一下,如下图:

package com.example.demo.test;

public class Test {
    public static void main(String[] args) {
        ClassLoader classLoader = Test.class.getClassLoader();
       while(classLoader!=null){
           System.out.println(classLoader);
           classLoader=classLoader.getParent();
       }
    }
}

输出结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@19469ea2

可以看到Test类的加载器是AppClassLoader类加载器;它的双亲类加载器是ExtClassLoader;这也验证了我们开发者写的类是被应用类加载器给加载的;

我们从代码从面分析一下类加载器的工作方式:

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, 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 {
                        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();
                    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.synchronized (getClassLoadingLock(name))进行了加锁,保证同一个类由一个线程进行加载;

2.Class<?> c = findLoadedClass(name);先进行查询这个类是否已经被加载过;

3.c = parent.loadClass(name, false);没加载过就会去调用双亲加载器,然后使用递归再进行处理,直到找到需要加载的类或者直到启动类加载器为止;

当走到启动类加载器时也没有找到,则会尝试自己加载;这里的过程是:

启动类加载器加载失败,但它不是通知子类去加载class,而是通知往上传的加载器自己加载失败,让其子类进行加载,直到加载的类被加载器加载为止;

通过整个过程的查询双亲是否被加载==》通知子类加载失败的整个过程就是一个双亲委派过程;

那么双亲委派的优点是什么?

Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系

举例说明:

Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值