类加载机制
整理自《疯狂java讲义》及个人理解
一、类加载、连接和初始化
注:同一个类的所有实例的静态变量共享一块内存区。但是如果两次运行Java程序处于两个不同的JVM进程中,两个JVM之间并不能共享数据。
1.类的加载
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。
类加载:类加载是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是所有程序运行的基础,JVM提供的这些类加载器通常也被称为系统类加载器,除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载二进制数据,比如:
- 从本地系统加载class文件。
- 从JAR包加载class文件,这种方式其实也就是我们导jar包的方式。
- 通过网络加载class文件。
- 把一个java源文件动态编译,并执行加载。
注:类加载器无需等到“首次使用”该类时才加载该类,Java虚拟机规范允许预先加载某些类。
2.类的连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责吧类的二进制数据合并到 JRE中。类连接的三个阶段:
(1) 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
(2) 准备:类准备阶段负责该类的类变量分配内存,并设置默认初始值。
(3) 解析:将类的二进制数据中的符号引用替换为直接引用。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。通俗来讲,就是我们一般将常量定义为一个全大写的字符串。
符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中(如果编译时就可以将这个类变量的值确定下就不用加载到内存中,程序中所有使用到这个常量的地方在编译的时直接替换成它的值),如果不能在编译时确定这个常量的值,就需要加载到内存。如:static final String XXX=System.currentTimeMiles()+"";
3.类的初始化
JVM初始化一个类包含如下几个步骤:
(1)假如这个类还没有被加载和连接,则程序先加载并连接该类。
(2)假如该类的直接父类还没有初始化,则先初始化其直接父类。
(3)假如类中有初始化语句,则系统依次执行这些初始化语句。
4.类初始化的时机
当java程序首次通过下面6种方式使用某个类或者接口时,系统就会初始化该类或接口。
- 创建类的实例。(包括new、通过反射来创建实例、通过反序列化方式创建实例)
- 调用某个类的类方法。
- 调用某个类的或接口的类变量,或为该类赋值。
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如Class.forName(“Person”).
- 初始化某个类的子类。(所有父类都会先初始化)。
- 直接使用java.exe命令运行某个类的主类。
使用ClassLoader类的loadClas()方法来加载某个类时,该方法知识加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。如下例:
package org.jcut.day01;
class Tester {
static {
System.out.println("Tester类的静态初始化块...");
}
}
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader c1 = ClassLoader.getSystemClassLoader();
c1.loadClass("org.jcut.day01.Tester");
System.out.println("系统加载了Teacher类");
Class.forName("org.jcut.day01.Tester");
}
}
运行结果:
系统加载了Teacher类
Teacher类的静态初始化块...
其中,c1.loadClass()只是加载了Tester类,,并不会初始化Tester类。必须等到Class.forName(“Tester”)时才会完成对Tester类的初始化。
二、类加载器
类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。
1.类加载机制
一旦一个类被载入到 JVM中,同一个类就不会被再次载入了。当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。
- Bootstrap ClassLoader:根类加载器
- Extension ClassLoader:扩展类加载器
- System ClassLoader: 系统类加载器
类加载器之间的父子关系并不是类继承上的父子关系,而是类加载器实例之间的关系。
JVM类加载器示例:
public class ClassLoaderParent {
public static void main(String[] args) throws IOException {
ClassLoader userClassLoader = ClassLoaderParent.class.getClassLoader();
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器"+userClassLoader);
System.out.println("系统类加载器:"+systemLoader);
Enumeration<URL> em1=systemLoader.getResources("");
while (em1.hasMoreElements()) {
System.out.println(em1.nextElement());
}
//获取系统类加载器的父类加载器,得到扩展类加载器
ClassLoader extendsionLoader = systemLoader.getParent();
System.out.println("扩展类加载器:"+extendsionLoader);
System.out.println("扩展类加载器的加载路径:"+System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent:"+extendsionLoader.getParent());
}
}
运行结果:
系统类加载器sun.misc.Launcher$AppClassLoader@73d16e93
系统类加载器:sun.misc.Launcher$AppClassLoader@73d16e93
file:/D:/javaworkspace/%e7%b1%bb%e5%8a%a0%e8%bd%bd%e6%9c%ba%e5%88%b6%e5%92%8c%e5%8f%8d%e5%b0%84/bin/
扩展类加载器:sun.misc.Launcher$ExtClassLoader@15db9742
扩展类加载器的加载路径:C:\Program Files\Java\jre1.8.0_171\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
扩展类加载器的parent:null
由上可知,系统类加载器加载路径是程序运行的当前路径,扩展类加载器的加载路径是jre路径。
扩展类加载器的父类加载器是根类加载器,这是根类加载器并不是java实现的。
类加载器加载Class大致要经过如下8个步骤:
- 检测此Class是否载入过(即缓存区是否有此Class),如果有则直接进入第8步,否则进入第2步。
- 如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步;如果父类加载器存在,就接着执行第3步。
- 请求父类加载器去载入目标类,如果成功载入则跳到第8步,如果否则执行第5步。
- 请求使用根类加载器来载入目标类,如果成功就跳到第8步,否则跳到第7步。
- 当前类加载器尝试去寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,否则跳到第7步。
- 从文件中载入Class,成功载入后跳到第8步。
- 抛出ClassNotFoundException异常。
- 返回对应的java.lang.Class对象。
上述过程看似繁杂,重要的是需要理清思路,以单个情况往下走流程就能清楚一点:需要注意的是第3步跳到第5步:如果父类类加载器没有载入成功,那么就由当前类去尝试载入。而不是根类加载器,这一步容易犯错误。
2.创建并使用自定义类加载器
再来回顾一下类加载顺序(个人总结):首先是判断父类加载器是否存在,如果存在就去请求父类加载器去载入目标类,如果不存在说明这个类加载器的父类加载器就是根类加载器或者本身就是根加载器,这种情况下就用根加载器去载入class,如果上述两种都没有载入成功,那么就用当前类加载器进行载入。(理解大于一切)
看源码来加深一下印象:
//1.loadClass(String)方法实现加载过程
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//2.内部调用了这个类加载方法(核心)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查这个class是否已经存在,如果存在直接返回。
Class<?> c = findLoadedClass(name);
//不存在,考虑如下情况:
if (c == null) {
long t0 = System.nanoTime();
try {
//父类类构造器是否为空,如果不为空,调用父类类加载器去加载Class
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;
}
}
自定义类加载器
Java允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。但是通常推荐实现findClass()方法,因为实现逻辑更加简单
遇到具体使用情况研究细节,先不贴代码了。
3.URLClassLoader类
此类是系统类加载器和扩展类加载器的父类(注意此处的父类,就是指类与类之间的继承关系,不同于前面几个类加载器之间的关系,前面的实例之间的父子关系),URL既可在本地文件系统获取二进制文件,又可以在远程主机获取而今之光文件来加载类。
构造方法:
- URLClassLoader(URL[] urls)
- URLClassLoader(URL[],ClassLoader parent)
一旦得到URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类,下面演示了如何之间从文件系统中加载MySQL驱动,并使用该驱动获取数据库连接。通过这个方式,无需将MySQL驱动添加到CLASSPATH环境变量中。
public class URLClassLoaderTest
{
private static Connection conn;
// 定义一个获取数据库连接方法
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.30-bin.jar")};
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载MySQL的JDBC驱动,并创建默认实例
Driver driver = (Driver)myClassLoader.
loadClass("com.mysql.jdbc.Driver").getConstructor().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 static void main(String[] args)throws Exception
{
System.out.println(getConn("jdbc:mysql://localhost:3306/mysql"
, "root" , "32147"));
}
}