JAVA类加载机制和类加载器

当虚拟机遇到一条new指令时,

1)检查符号引用即检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查该引用是否被加载、解析和初始化过。若没有则必须先执行相应的类加载过程

2)在检查通过后,虚拟机将为新生的对象分配内存。

*为对象分配空间的任务相当于从JAVA堆中划出一片内存,假设JAVA堆是规整的,那么就是移动相应指针划出与对象大小相等的内存,这种分配方式称为指针碰撞。如果JAVA堆不是规整的,已使用和未使用的内存片段交错,则没办法简单进行指针碰撞,则必须维护一个列表,记录哪些内存可用,这种分配方式称为空闲列表。分配方式的选择由JVM决定,具体以垃圾收集算法为依据。因此在使用Serial、ParNew等等Compact过程的收集器时,则采用指针碰撞,而使用CMS这种基于Mark-Sweep的通常采用空闲列表。为了解决多线程问题,还可以通过设置使用TLAB(本地线程分配缓冲)。

3)内存分配完成后,JVM需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB则在TLAB中进行。这一步操作保证了对象的实例字段在JAVA代码中可以不赋予初始值就可以直接使用,程序能访问这些字段的数据类型所对应的零值。

4)接下来JVM要对对象进行必要设置,对对象头需要存储的信息,例如对象属于哪个类的实例,类的元数据信息地址,对象哈希值,GC分带年龄,锁状态等。

5)到了这一步,从JVM的角度来说对象已经产生,但从JAVA程序的视角,对象创建才刚刚开始,即<init>方法还没有被执行,所有的字段都为零值,所以new之后会紧跟<init>方法,将对象按照程序员的意愿初始化。


JVM的类加载机制

首先要弄清楚JVM如何装载class文件

class可能是类可能是接口,接下来同一用类代表

class文件并非具体的磁盘文件,而是一串二进制字节流,无论它以何种方式存在

一、类加载的时机

类从被加载到内存到卸载出内存,整个生命周期包括:

加载,验证,准备,解析,初始化,使用,卸载,七个阶段

其中验证准备解析三个阶段合称为连接



在七个阶段中,加载,验证,准备,初始化,卸载这五个阶段的顺序是确定的,是有先后关系的。

而解析阶段则不确定,它在某些情况可以在初始化之后开始,这是为了支持JAVA的语言的运行时绑定(例如多态)

需要注意的是,这里的先后顺序指的是开始的顺序,并不是进行或完成的顺序,因为这些阶段通常都是相互交叉混合式进行的。

对于初始化阶段,JVM硬性规定了五种情况必须进行类的初始化(而加载验证准备自然必须发生在这之前):

1)遇到new/getstatic/putstatic/invokestatic这4条字节码指令时,若类没有初始化,类必须初始化。这四条指令常见的场景依次对应的是:使用new实例化对象/读取类的静态字段(final修饰的除外,因为其在编译器已经把结果放入常量池中)/设置类的静态字段/调用一个类的静态方法时。

2)使用java.lang.reflect的方法对类进行反射时。

3)当初始化一个类的时候,若父类没有初始化,需要先进行父类的初始化。

4)当JVM启动时,用户需要指定一个主类(包含main方法的那个类),JVM会先初始化这个类。

5)当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例经过解析使用1)的指令时,触发类的初始化

以上均为主动使用,若被动使用不会触发类的初始化,即加载了类却没有初始化。

1)即使用子类调用父类的静态字段,父类会被加载,但不会触发父类的初始化。

2)引用类的常量也不会触发相应类的初始化


介绍完情况后接下来看各个阶段都具体做了什么事情:

一、加载:

在加载阶段,JVM主要做三件事情

1)通过一个类的全限定名来获取定义此类的二进制字节流(网络获取,数据库获取,运行时计算生成等)

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3)在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(Class虽然是对象但存放在方法区里)

二、连接(验证、准备、解析)

验证:

该阶段的目的:确保Class文件的字节流中包含的信息符合JVM的要求,并且不会危害虚拟机自身的安全。


验证阶段是重要的但不是必须的,因为对程序运行期没有影响,可以通过设置参数关闭该阶段,以缩短JVM类加载的时间。

准备:

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段的内存分配仅包括类变量(被static修饰的变量)而不包括实例变量,实例变量将会通过对象实例化时随着对象一起分配在堆中。这里的初始值指的是通常情况下的数据类型的零值即若public static i = 123; 在这个阶段i被设置为int的零值0,即i=0;把123赋予i的动作需要到初始化阶段执行,而不是现在

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

*如果类中有属于常量的字段,那么常量字段也会在准备阶段被附上正确的值,这个赋值属于java虚拟机的行为,属于变量的初始化。在准备阶段,不会有任何java代码被执行。

解析:

在准备阶段完成后,就可以进入解析阶段。

解析阶段的目的:将类、接口、字段、方法的符号引用转为直接引用。

符号引用:一些字面量的引用,即以一组符号来描述所引用的目标,字面量形式由java虚拟机明确规范。

直接引用:可以是直接指向目标的指针,相对偏移量,间接定位到目标的句柄。和虚拟机实现的内存布局相关,若已经有了直接引用,则引用的目标一定存在内存当中。

三、初始化

这是类加载过程的最后一步。前面的阶段除了加载阶段用户可以自定义类的类加载器外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才是真正执行类中定义的JAVA程序代码。

可以说,初始化阶段是类执行构造器<clinit>()方法的过程。

1)<clinit>()方法是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的类变量,定义在其之后的类变量,只能被赋值,不能被访问

2)<clinit>()方法与类的构造器函数<init>()方法不同,它不需要显示的调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。故父类的静态语句块会先于子类的静态语句块执行。

*父类的<clinit>总是在子类<clinit>之前被调用。

*java编译器并不是为所有的类都产生<clinit>初始化函数,如果一个类既没有类变量赋值语句,也没有static语句块,那么生成的<clinit>函数就应该为空,因此,编译器就不会为该类插入<clinit>函数。

重要:虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步,如果多个线程同时去初始化一个类,只有一个线程去执行这个类的<clinit>()方法,其他线程都会被阻塞,直到指定线程执行完<clinit>()方法。


到这里类的加载就结束了。

补充:

1)父类加载初始化先于子类,父类的<clinit>优先于子类的<clinit>函数执行。即父类静态变量与静态块>子类,最优先。

2)如果创建一个子类对象,父类构造函数<init>调用先于子类构造器<init>函数调用。即父构造>子构造

3)在执行构造器<init>函数首先会初始化类中成员变量或者执行非静态代码块(这二者执行的先后顺序依赖于在源文件中出现的顺序),然后再调用构造函数。成员变量和非静态代码块>构造函数。

*这里父类优先于子类包括父类构造函数>子类成员变量和非静态代码块。



类加载器:


在前面的类加载机制的加载阶段中,总共做了三件事,其中第一件事

“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到JAVA虚拟机外部去实现。

实现这个动作的代码模块称为类加载器。

类加载器只用于实现类的加载动作,但不限于类的加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在JAVA虚拟机中的唯一性,通俗来讲,比较两个类是否相等,只有在两个类由同一个类加载器加载的前提下才有意义。否则,两个类来源同一Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,两个类就不相等。

*这里的相等包括代表类的Class对象的equals()方法和instanceof关键字做对象所属关系判断的结果。

JAVA的类加载器之间关系模型采取的双亲委派模型。

双亲委派模型:

      从JVM的角度来说,JAVA只存在两种不同的类加载器,一种是启动类加载器Bootstrap ClassLoader,这个类加载使用C++语言实现,是虚拟机自身的一部分;另一种就是其他类加载器,这些加载器都由JAVA语言实现,独立于虚拟机外部并且全部基础自java.lang.Classloader。

       从程序员的角度,类加载器可以更细致划分。

1)Bootstrap Classloader:启动类加载器

2)Extension Classloader:扩展类加载器

3)Application Classloader:应用程序类加载器,程序中默认的类加载器


图中的类加载器层次关系,就是所谓的双亲委派模型。即除了顶层的启动类加载器之外,其余的类加载器必须有自己的父类,这里的父子关系一般不会以继承实现,而是都使用组合关系来复用父加载器的代码。

在JDK1.2期间被引入,并不强制使用,是推荐模型。

双亲委派模型工作过程:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的加载器都是如此,因此所有的请求最终都应该传送到顶层的启动类加载器中,只有当父类反馈自己无法完成这个请求时(它的搜索范围没有找到所需的类)时,子类加载器才尝试自己加载。

优点:

1)具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个加载器想要加载该类都会把请求委托到顶层的启动类加载器中加载,因此Object类在程序的各种加载器环境中都是同一个类,保证了程序的稳定性。

代码实现逻辑:

调用自身loadClass()方法,方法内:先检查是否被加载过,若没有则调用父加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFindException异常后,再调用自己的findClass()方法进行加载。

双亲委派模型的破坏者-线程上下文类加载器

    在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。 

 线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例


从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。为了进一步证实这种场景,不妨看看DriverManager类的源码,DriverManager是Java核心rt.jar包中的类,该类用来管理不同数据库的实现驱动即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver,这里主要看看如何加载外部实现类,在DriverManager初始化时会执行如下代码:

//DriverManager是Java核心包rt.jar的类
public class DriverManager {
    //省略不必要的代码
    static {
        loadInitialDrivers();//执行该方法
        println("JDBC DriverManager initialized");
    }

//loadInitialDrivers方法
 private static void loadInitialDrivers() {
     sun.misc.Providers()
     AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //加载外部的Driver的实现类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
              //省略不必要的代码......
            }
        });
    }

在DriverManager类初始化时执行了loadInitialDrivers()方法,在该方法中通过ServiceLoader.load(Driver.class);去加载外部实现的驱动类,ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容,如下所示


public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
        super();
    }

    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
                + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

从注释可以看出平常我们使用com.mysql.jdbc.Driver已被丢弃了,取而代之的是com.mysql.cj.jdbc.Driver,也就是说官方不再建议我们使用如下代码注册mysql驱动

//不建议使用该方式注册驱动类
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

而是直接去掉注册步骤,如下即可

String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

这样ServiceLoader会帮助我们处理一切,并最终通过load()方法加载,看看load()方法实现

public static <S> ServiceLoader<S> load(Class<S> service) {
     //通过线程上下文类加载器加载
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
  }

很明显了确实通过线程上下文类加载器加载的,实际上核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,通过这种方式实现了Java核心代码内部去调用外部实现类。我们知道线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为虽然默认使用的是AppClassLoader但具体实现可以由程序员设置,若不同会导致出很多问题。


参考:《深入理解JAVA虚拟机》

部分引自:http://blog.csdn.net/javazejian/article/details/73413292


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值