JAVA虚拟机学习总结01-类加载机制

JVM虚拟机

java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。比如kotlin、scala等。

JVM的基本结构

JVM由三个主要的子系统构成

  • 类加载子系统
  • 运行时数据区(内存结构)
  • 执行引擎

jvm-architecture.png

类加载机制

类的生命周期

类加载.jpg

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:

1.加载

找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

类的加载方式:

  • 根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;
  • 从jar文件中读取;
  • 从网络中获取:比如10年前十分流行的Applet。
  • 根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。

2.连接

2.1 验证

验证字节码文件的正确性,保证加载的类是能够被jvm所运行。

2.2 准备

给类的静态变量分配内存,并赋予默认值;

  1. 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
  2. 引用类型的默认值为null。
  3. 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。

2.3 解析

把常量池中的符号引用转换为直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

3.初始化

如果一个类被直接引用,就会触发类的初始化。初始化阶段是执行类构造器 < clinit > 方法的过程。

类构造器方法有如下特点:

  • 保证父类的 < clinit > 方法执行完毕,再执行子类的 < clinit > 方法。
  • 由于父类的 < clinit > 方法先执行,所以父类的静态代码块也优于子类执行。
  • 如果类中没有静态代码块,也没有为变量赋值,则可以不生成 < clinit > 方法。
  • 执行接口的 < clinit > 方法时,不需要先执行父接口的 < clinit > 方法。只有父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也不执行接口的 < clinit > 方法。
  • 虚拟机会保证在多线程环境下 < clinit > 方法能被正确的加锁、同步。如果有多个线程同时请求加载一个类,那么只会有一个线程去执行这个类的 < clinit > 方法,其他线程都会阻塞,直到方法执行完毕。同时,其他线程也不会再去执行 < clinit > 方法了。这就保证了同一个类加载器下,一个类只会初始化一次。(这也是为什么说饿汉式单例模式是线程安全的,因为类只会加载一次。)

在java中,直接引用的情况有:

  • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
  • 初始化子类的时候,会触发父类的初始化
  • 作为程序入口直接运行时(也就是直接调用main方法)。
  • 通过反射方式执行以上三种行为。

初始化顺序:

对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)>构造器。

 在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。

 被动引用:

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
  • 定义类数组,不会引起类的初始化。
  • 引用类的常量,不会引起类的初始化。

4.使用

5.卸载

在类使用完之后,如果满足下面的情况,类就会被卸载:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

类加载器的种类

  • 启动类加载器(Bootstrap ClassLoader)

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

  • 扩展类加载器(Extension ClassLoader)

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

  • 系统类加载器(Application ClassLoader)

也称应用程序加载器。是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

  • 自定义加载器:负责加载用户自定义路径下的类

Java 执行代码的大致流程如下:

在这里插入图片描述

类加载机制

  1. 全盘负责委托机制

当一个classloader加载一个Class的时候,这个Class所依赖的和引用的其它Class通常也由这个classloader负责载入。除非显示的使用另一个ClassLoader。

     2. 双亲委派机制

在这里插入图片描述

当需要加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。


 

 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) {
                     
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    
                    //调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                    c = findClass(name);

双亲委派模式的优势

  • 沙箱安全机制:保护核心类,防止打破双亲委派机制,防篡改,如果重名的话就报异常,这里的重名指包名加类名都重复。
  • 避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次

打破双亲委派机制

为什么要打破双亲委派机制:父类加载器无法加载子类加载器路径中的类。

在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

打破双亲委派机制实例----SPI机制

SPI机制(Service Provider Interface)其实源自服务提供者框架(Service Provider Framework,参考【EffectiveJava】page6),是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔。

以JDBC为例:

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的「SPI」扩展机制来实现。
jdbc的获取连接的方式
(1)接口定义

JDBC在java.sql.Driver只定义了接口。

(2)厂商实现

这里以MySQL为例,在mysql-connector-java-5.1.39.jar包里的META-INF/services目录下可以找到一个java.sql.Driver文件,文件内容是一个类名,这个名叫com.mysql.cj.jdbc.Driver的类就是MySQL针对JDBC中定义的接口的实现。
(3)使用

Connection conn = DriverManager.getConnection(url,username,password);
显然语句并没有加载实现类,这里就涉及到使用「SPI」扩展机制来查找相关驱动了,接下来,我们结合源码探究一下这是如何实现的。

(4)源码解析
关于驱动的查找其实都在DriverManager中,DriverManager位于java.sql包里,用来获取数据库连接,在DriverManager中有一个静态代码块如下:

static {
loadInitialDrivers();
println(“JDBC DriverManager initialized”);
}

loadInitialDrivers方法用于实例化驱动,由3部分构成:
(1)获取有关驱动的名称,保存在drivers中

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction() {
public String run() {
return System.getProperty(“jdbc.drivers”);
}
});
} catch (Exception ex) {
drivers = null;
}

drivers保存驱动的定义
(2)加载并实例化驱动(run方法)

比较关键的地方是ServiceLoader.load
其中ServiceLoader.load(Driver.class)方法源码:
 

public static ServiceLoader load(Class service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

   java的双亲委托类加载机制(ClassLoaderA -> System class loader -> Extension class loader -> Bootstrap class loader)可以保证核心类的正常安全加载。但是右边的 Bootstrap class loader 所加载的代码需要反过来去找委派链靠左边的 ClassLoader A 去加载东西的时候,就需要委派链左边的 ClassLoader 设置为线程的上下文加载器即可。

每一个线程都有自己的ContextClassLoader,默认以SystemClassLoader为ContextClassLoader。通过Thread.currentThread().getContextClassLoader(),可以把一个ClassLoader置于一个线程的实例之中,使该ClassLoader成为一个相对共享的实例,这样即使是启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了。

  3. cache机制

如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是修改了Class但是必须重新启动JVM才能生效,并且类只加载一次的原因

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值