我们为什么要学习类加载机制?在实际编程中有什么用?

说到类加载机制,又不得不提Java代码执行过程,源码(.java)文件被编译成字节码(.class)文件,再由Jvm进行后续处理。其实这个后续处理过程,就是JVM的类加载机制,简单来说,就是把.class文件装载到内存,进行校验、解析、转换和初始化,最终形成可以被虚拟机直接使用的Java类型。
这一个过程就是类加载的生命周期,类加载的生命周期总共分为7个阶段:

加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备、解析三个步骤又可统称为连接。类加载机制包括了前五个阶段,即加载、验证、准备、解析、初始化。

1)加载(Loading)
从字面意思就可以理解,该步的主要目的就是将字节码从不同的数据源(class文件、jar包、war包,甚至网络、其它文件生成[比如将JSP文件转换成对应的Class类])转化为二进制文件加载到内存中,并生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的入口。

2)验证(Verification)
这一阶段的主要目的就是对二进制文件进行校验,校验是否符合Jvm字节码规范,校验是否会危害Jvm安全,简单来说,就是做各种检查,主要包括:

确保二进制字节流格式是否规范;

是否所有方法都遵守访问控制关键字的限定;

方法调用的参数个数和类型是否正确;

确保变量在使用之前是否被正确初始化;

检查变量是否被赋予恰当类型的值。

3)准备(Preparation)
JVM会在这个阶段对类变量(静态变量,即static修饰)分配内存并设置类变量的初始值(对应数据类型的默认初始值,如0、0L、null、false等),即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

public int aa = 10;

public static int bb = 66;

public static final int cc = 88;

aa不会被分配内存,而bb会分配内存,但bb的初始值是0而不是66,需要注意的是,static修饰的量是类变量,也叫做静态变量,而static final修饰的被称作为常量,所以cc的初始值是88。

4)解析(Resolution)
该阶段将常量池中的符号引用转化为直接引用。

符号引用:就是class文件中的CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等类型的常量。

在编译时,Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如我们有一个com.test类引用了阿里巴巴的com.aliyun类,编译时Test类并不知道阿里巴巴类的实际内存地址,因此只能使用符号com.aliyun。

直接引用:通过对符号引用进行解析,找到引用的实际内存地址。

5)初始化(Initialization)
初始化阶段是执行类构造器方法的过程,即真正执行类中定义的Java程序代码。

比如:String xiaoming = new String("need a Girlfriend");
使用new实例化一个String对象,此时就会调用String类的构造方法对xiaoming进行实例化。

说完类加载过程,就不得不说类加载器,什么是类加载器?

还记得使用JdbcTemplate连接数据库过程吗?我们把数据库连接封装为工具类DruidUtils方法,该方法就用到了类加载器:

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

/**
 * 提供连接
 */
public class DruidUtils {

	private static DataSource dataSource = null;

	static { // 必须优先执行 只执行一次

		try {
			// 需要一个文件流
			InputStream is = DruidUtils.class.getClassLoader().getResourceAsStream("db.properties");
			// 创建配置文件对象
			Properties props = new Properties();
			props.load(is);
			// 核心类
			dataSource = DruidDataSourceFactory.createDataSource(props);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 返回数据源方法
	 * 
	 * @return
	 */
	public static DataSource getDataSource() {
		return dataSource;
	}

	/**
	 * 提供连接的方法
	 * 
	 * @return
	 */
	public static Connection getConnection() throws SQLException {

		return dataSource.getConnection();
	}
}

其中,class.getClassLoader()就是类加载器的标志了。Jvm设计之初就把加载动作放到外部实现,以便让应用程序决定如何获取所需的类,所以提供了3种类加载器:
1)启动类加载器(Bootstrap ClassLoader):加载jre/lib包下面的jar文件,比如说常见的rt.jar。
(java.time.*、java.util.*、java.nio.*、java.lang.*、java.text.*、java.sql.*、java.math.*等等都在rt.jar包下)

import java.net.URL;

/**
 * 该程序可以获得根类加载器所加载的核心类库,
 * 并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径
 *
 */
public class ClassLoaderTest {

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

2)扩展类加载器(Extension or Ext ClassLoader):加载jre/lib/ext包下面的jar文件。由Java语言实现,父类加载器为null。

3)应用类加载器(Application or App ClasLoader):根据程序的类路径(classpath)来加载Java类。由Java语言实现,父类加载器为ExtClassLoader。

类加载器加载Class大致要经过如下8个步骤:

1、检测此Class是否曾载入过(缓冲区中是否有此Class),如果有直接进入第8步,否则进入第2步。
2、如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
3、请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
4、请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
5、当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
6、从文件中载入Class,成功后跳至第8步。
7、抛出ClassNotFountException异常。
8、返回对应的java.lang.Class对象。

如果以上3种类加载器不能满足要求,我们还可以自定义类加载器(继承 java.lang.ClassLoader 类),它们的层级关系如图:

这种层次关系就叫作双亲委派模型:如果一个类加载器收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,父类再委派父类,一直到最顶层的类加载器,因此所有的加载请求都应该传送到启动类加载器。只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,保证Java程序的稳定性。比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object 对象。

所以,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。

发布了224 篇原创文章 · 获赞 34 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/weixin_39309402/article/details/105017847