类加载器就是寻找类的字节码文件并构造出类在JVM内部表示对象的组件。
类加载工作由ClassLoader及其子类负责。ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入Class字节码文件。
类加载器
负责将.class文件加载到内存中,并为之生成对应的 Class 对象。
类加载器层次结构
- Bootstrap ClassLoader 根类加载器
- 也被称为引导类加载器,负责Java核心类的加载。此加载器是
C写的,在Java是看不到的。 - 加载Java JDK核心类
JRE/lib/rt.jar,如 System, String类。
- 也被称为引导类加载器,负责Java核心类的加载。此加载器是
- Extension ClassLoader 扩展类加载器
- sun.misc.Launcher$ExtClassLoader
- 负责加载JRE/lib/下的扩展目录ext中的jar类包。
- App ClassLoader 应用类加载器
- 也称为系统类加载器,sun.misc.Launcher$AppClassLoader。
- 负责在 JVM 启动时加载来自 java 命令的 class 文件,以及 Classpath 环境变量所指定的 jar 包和类路径。
类加载器的流程
在Java中,类加载器把一个类装入JVM,需要经过加载、链接(校验,准备,解析)、初始化步骤。
加载
把类的.class文件的字节码加载到内存中,有两种方式:
- 隐式加载:不通过代码里调用
ClassLoader来加载需要的类。而是通进JVM来自动加载需要的类到内存。 - 显式加载:调用
ClassLoader类来加载一个类,可以使用以下方法:- this.getClass.getClassLoader.loadClass(String name)
- Class.forName(String className)
- 或自己实现ClassLoader的 findClass()方法等。
连接
把类的二进制合到到JRE中,分三个阶段
- 验证:检验加载的类内部结构是否正确,并和其他类协调一致。
- 准备:为类静态变量分配内存,并设置默认值(仅包含类静态变量,不包含实例变量)。
- 解析:虚拟机将常量池中的符号引用替换为直接引用。
初始化
类静态变量初正确的初始值,执行静态代码块,执行构造方法等。
- 类变量赋初始值:
- 声明类变量时指定初始化值。
- 使用静态初始化块为类变量指定初始值。(静态初始化块都将被当成类的初始化语句)
- JVM初始化步骤:
- 若类没加载和连接,则先加载并连接该类。
- 若类的直接父类没有被初始化,则先初始化其直接父类。
- 若有初始化语句,则依次执行这些初始化语句
- 类初始化时机
- 创建类的实例。
- 调用某个类的静态方法。
- 访问某个类或接口的类变量,或为该类变量赋值。
- 使用反身来强制创建该类或接口对应的java.lang.Class对象。
- 初始化某个类的子类,该子类的直接父类和所有间接父类都被初始化。
- 直接使用java.exe来运行某个主类。
- final修饰的类变量的初始化:
- 如果类变量(静态变量)使用
final修饰,并且它的值在编译时就确定下来,该变量相当于常量, 不会被初始化。static final String name = "Admin"; 。 - 如果使用
final修饰类变量的值在不能在编译时确定下来,必须等到运行时才可以确定该类变量的值,则会被初始化。static final String nowtime = System.currentTimeMillis() + ""; 。
- 如果类变量(静态变量)使用
使用ClassLoader类的loadClass()方法来加载某个类时,该方法只加载该类;使用Class的forName()静态方法会强制初始化该类。
类加载器机制
Java中,一个类用其全限定类名(包括包名和类名)作为唯一标识。
JVM中,一个类用其全限定类名和其类加载器作为唯一标识(类名、包名、加载器的实例)。
- 全盘负责制:该类所有依赖的和引用的其他类将由该类的加载器负责载入,除非显式使用另外的加载器载入。
- 父类委托:先委托父类加载器加载目标类,找不到时再从自己的类路径中查找并加载目标类,确保类只被加载过一次。
- 缓存机制:保证所有加载过的类都会被缓存,当需要使用时,先从缓存区中搜索,缓存不存在该类时,系统才会读取该类对应的二进制数据,将期转换成
Class对象,放入缓存区中。
类加载器执行步骤
类加载器加载Class大致经过如下8个步骤:
- 检测此类是否载入过(即在缓存区是否存),有进入第8步,没有执行第2步。
- 如果父类加载器存在,执行第3步;否则跳到第4步执行。
- 请求父类加载器载入目标类,如果成功跳到第8步,否则执行第5步。
- 请求根类加载器载入目标类,如果成功跳到第8步,否则执行第7步。
- 当前类加载器尝试寻找Class文件,如果找到则执行第6步,否则跳到第7步。
- 从文件中载入Class,成功载入后跳到第8步。
- 抛出ClassNotFoundException异常。
- 返回对应的java.lang.class对象
自定义的类加载器
- 通过扩展
ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。- loadClass(String name,boolean resolve):ClassLoader方法入口,根据指定名称为加载类。
- findClass(String name)根据指定定名称来查找类。建议使用
- Class defineClass(String name,byte[] b, int off, int len)
- defineClass()
- findSystemClass(String name)
- static getSystemClassLoader()
- getParent()
- findLoadedClass(String name)
使用自定义类加载器实现如下常见功能:
- 执行代码前自动验证数字签名。
- 根据用户提供的密码解密代码,从而可以实现代码混淆来避免反编译
*.class文件。 - 根据用户需求来动态在加载类。
- 根据应用需求把其他数据以字节码的形式加载到应用中。
URLClassLoader类
该类也是系统类加载器和扩展类加载器的父类。功能比较强大,可以从本地文件或远程主机获取二进制文件来加载类。- URLClassLoader(URL[] URLS):使用默认的父类加载器创建一个
ClassLoader,该对象将从urls所指定的系列路径来查询并加载类。 - URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建一个
ClassLoader对象,其他功能与个构造器相同。
- URLClassLoader(URL[] URLS):使用默认的父类加载器创建一个
一旦得到URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。例如:可以直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接,就无须将驱动添加到CLASSPATH环境变量中。
示例代码
示例一:获取加载器
public class Demo2 {
@Test
public void ClassLoaderTest() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("loader = " + loader);
System.out.println("loader parent = " + loader.getParent());
System.out.println("loader parent`s parent = " + loader.getParent().getParent());
}
}
//----------结果------------------------
loader = sun.misc.Launcher$AppClassLoader@18b4aac2
loader parent = sun.misc.Launcher$ExtClassLoader@2038ae61
loader parent`s parent = null
示例二:URLClassLoader
package com.demo;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.util.Properties;
public class Test6 {
private static Connection conn;
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://localhost:3306/vending?useUnicode=true&characterEncoding=utf-8&useSSL=false";
String account = "root";
String key = "123456";
System.out.println(getConn(url,account,key));
}
// 定义一个获取数据库连接的方法
public static Connection getConn(String url, String user, String pass) throws Exception {
if (conn == null) {
// 创建一个URL数组
URL[] urls = { new URL("file:mysql-connector-java-5.1.44-bin.jar") };
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
@SuppressWarnings("resource")
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载MySQL的JDBC驱动,并创建默认实例
Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();
// 创建一个设置JDBC连接属性的Properties对象
Properties props = new Properties();
// 至少需要为该对象传入user和password两个属性
props.setProperty("user", user);
props.setProperty("password", pass);
// 调用Driver对象的connect方法来取得数据库连接
conn = driver.connect(url, props);
}
return conn;
}
}
示例三:类加载顺序与赋值
public class MainTest {
public static void main(String[] args) {
Singleton s = Singleton.getSingleton();
System.out.println("counter1 = " + s.counter1);// = 1
System.out.println("counter2 = " + s.counter2);// = 0
}
}
public class Singleton {
/**
* 1. 被调用:加载,连接
* 1.1 验证
* 1.2 准备:给静态变量分配内存,设默认值,singleton=null,counter1=0,counter2=0
* 1.3 解析:符号引用替换成直接引用
* 2. 初始化:
* 2.1 执行构造方法:counter1=1,counter2=1,singleton=对象(特别注意)
* 2.2 给类变量赋正确的初始值:counter1没有赋初始值,还是默认值;counter2=0 (特别注意)
*/
private static Singleton singleton = new Singleton();
public static int counter1;
//然后初始化,赋值覆盖了默认值
public static int counter2 = 0;
public Singleton() {
counter1++; //counter1 = 1
counter2++; //counter2 = 1
}
public static Singleton getSingleton() {
return singleton;
}
}
相关参考
更多内容请访问:IT源点
注意:本文归作者所有,未经作者允许,不得转载