最近在准备一个数据库框架的专题,想从driver一路讲到上层orm框架。在准备JDBC这层时发现其中有很多可讲的java知识点。于是舍远求近,先讲讲框架里的java。
本文选取JDBC获取connection这个切入点,希望类的加载、SPI模式和一些涉及到的其他知识点。由于篇幅原因本文先讲解类的加载过程。 通过学习本文可以学习以下内容:
- 类的加载过程:load,link,intialize;
- 类加载时间;
- 扩展知识点:利用jvm的实现单例;
- 类的加载器;
- 类的委派模型delegation model;
- launcher类中的ext/appClassloader;
问题的发现
从下面这个代码片段可以发现JDBC获得一个可使用的connection仅仅需要调用一个静态方法。
// 实验脚本
public class JdbcTest {
private String userName;
private String password;
private String serverName;
private String portNumber;
public JdbcTest(String userName, String password, String server, String port){
this.userName = userName;
this.password = password;
this.serverName = server;
this.portNumber = port;
}
public Connection getConnection() throws SQLException {
Connection conn = null;
Properties connectionProps = new Properties();
connectionProps.put("user", this.userName);
connectionProps.put("password", this.password);
conn = DriverManager.getConnection(
"jdbc:mysql://" +
this.serverName +
":" + this.portNumber + "/",
connectionProps);
System.out.println("Connected to database");
return conn;
}
public static void main(String[] args) throws SQLException {
JdbcTest jdbcTest = new JdbcTest("dal", "dal123", "localhost", "3306");
Connection connection = jdbcTest.getConnection();
}
}
单步执行可以发现从getConnection进入了DriverManager的static代码块,注意这里是在调用getConnection之后才执行这段关键代码。
//DriverManager类内部的static代码块
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
但这个过程是怎么实现的为什么发生在getConnection之后呢。这里就引出了第一个问题,类的加载过程是怎样的,发生在什么阶段。
类的加载过程?
类的加载过程主要分为3个阶段:loading,linking,initializing。同时linking阶段又有三个步骤:verifying,preparing和resolving。
loading
这里的加载和类的加载容易引起歧义,加载在不同的语境可以指整个类的加载过程(包括加载、连接和初始化),也可能指类的加载中的第一步loading。
下面看一下loading做什么,不做什么:
- loading要找到一个有具体名字的class/interface的二进制数据流(Loading is the process of finding the binary representation of a class or interface type with a particular name;)
也有人翻译称为通过名字找到字节流,这种说法有一点不严谨但是适用于大部分场景。比如Class.forName往往是通过提供一个名字来定位类的文件的。
找到,这个信息就非常模糊了,在哪儿找到完全不限制。这也给了classloader极大的创造空间,比如可以通过DynamicProxy中动态生成字节流的方法体会自定义类的生成与加载过程,具体参考java.lang.reflect.Proxy/sun.misc.ProxyGenerator生成二进制字节流的源代码。关于loading下文的类加载器章节会继续探讨。
- loading根据二进制数据创建类/接口(creating a class or interface from that binary representation);
这句话的信息量就非常大了:这里涉及JVM中的两个空间(1)方法区method area, (2)动态常量区run-time constant pool;同时涉及两个类C和D。
假设C是我们要加载的类,D就是触发C加载的类。首先D在自己的run-time constant中获得C的引用,把C的二进制字节流中所代表的静态存储结构转化为method area/方法区的数据结构并创建class对象作为这个类的访问入口。这里具体的数据结构和内存分区和jvm的不同实现相关。
- loading不做什么同样重要,JVM loading本身并不把生成的class和interface与运行态联系在一起,所以更不能执行类中的代码,类中的字段也没有初始化。换言之,各种static的代码块没有运行,static的fields也没有赋值。
连接linking: verify, prepare and resolve
讲到link一下子想到了我们林克老师,塞尔达的创造者曾解释以林克为主角的游戏为什么命名为塞尔达:塞尔达代表了这个充满无穷魅力的世界,而林克就是连接我们进入这个世界的人。回到正题,linking过程,其实也是要把这个进入jvm method area的类和运行时联系在一起,从而类/接口进入可执行状态。
连接过程包括三个过程:验证(verification),准备(preparation)和解析(resolvation)。
验证verifation
其实java编译器可以确认编译过的字节码是安全的,但是正如loading中讲到的,二进制字节可以来自各个地方。来自各种不明途径的二进制字节码是不是有错误,是不是有危险,就值得怀疑了。所以在类投入使用之前需要进行验证。
验证的内容包括文件格式,语言规范,数据流控制流等。这里就不展开了。
准备prepare
准备是为static fields分配内存并赋予默认值的过程。 如果一个类的内容没有异常,那么就进入了准备阶段。这一阶段jvm会为类的静态变量分配内存并赋予默认值,注意准备阶段并不会执行任何static fields的初始化和static代码块。
解析resolution
java代码中各种引用在编译时统一用规范的符号引用(symbolic references)来表示,并存储在class文件里。符号引用是一种逻辑定位符,追求定位无歧义,但并不表示实际的内存地址。但当真正执行jvm指令时,需要具体能找到数据的内存地址的方法,于是就有了直接引用(direct references)。直接引用可以使指针、偏移量等具体不同实现。
JVM规范规定了在执行一系列指令( anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface等)时需要直接引用,而解析就是将符号引用替换为直接引用的过程。
从验证字节流、分配内存到引用解析可以看出这一切都是为执行类中定义的代码做准备。
初始化initialization
一切准备就绪之后,就开始执行类/接口的初始化方法cinit了。cinit是编译器提供的方法,直接参与类的初始化过程。 编译器搜集了静态变量的赋值、static代码块编译进入cinit方法使之作为类的唯一构造器;同时一个类的cinit在调用前,jvm会保证其所有父类的cinit也都调用完成。这也解释了为什么一个类的初始化的原因是其子类被初始化。
java是天然支持多线程的语言,JVM为了保证类的初始化线程安全并且只发生一次,每个类都有初始化锁unique initialization lock和状态,类的初始化是“synchronize”的并在结束之后设置类的状态为“已初始化”来保证 cinit的执行过程是线程安全的,而且只会加载一次。
这也就是为什么static字段可以帮助实现单例。到这里,类的加载就完成了。
类的初始化发生在什么时候?
回到JDBC的例子,会发现用户代码脚本执行完getConnection之后,DriverManager的static代码块才开始执行。这里就引发了一个疑问:类的整个加载过程是什么时候发生的呢?
这里有两个关键认知:(1)类的整个加载环节是不需要一起执行的,一个类可能很早就加载了,但并没有初始化;(2)类的整个加载过程是什么时候发生是根据虚拟机实现的不同和一个jvm中类的使用方法不同都是不同的。
sun公司的JVM一般是lazy策略类的加载的全过程,在使用一个类时,才会对类进行loading;在需要调用类中的方法和字段时才进行初始化过程。初始化的具体条件,JVM specification中定义的初始化条件比较常用到的有:
- 在执行以下JVM指令时:new, getstatic, putstatic, invokestatic;这些方法都直接或者间接的使用了类中的field或者method。
- 子类初始化时,父类也需要进行初始化;
- 对类使用一些反射方法时候(class/reflect类的方法)会导致类的初始化;
- 用于launch java程序的类会直接被初始化;
下面有例子请参考注释和输出:
package lang;
import java.lang.reflect.Constructor;
public class Tester {
public static void main(String[] args) throws Exception {
System.out.println("step 1");
Class<TestClassLoader> theTestClass; # 这里不需要loadclass
System.out.println("step 2");
theTestClass = TestClassLoader.class; # 这里需要load class,但不需要初始化
Constructor<TestClassLoader> classLoaderConstructor = TestClassLoader.class.getDeclaredConstructor();
System.out.println("step 3");
classLoaderConstructor.newInstance(); # 执行类的代码时初始化
}
}
输出内容
step 1
step 2
step 3
static part is loaded
message field is loaded!
testclassloader instance is created
根据上面的分析,JDBC的实验就很容易理解了,import driverManager并不会触发static代码块来loadInitialDrivers,static执行发生在invokeStatic也就是getConnection之后。
单例模式
这里就涉及到另一个常见的问题,单例模式怎么写(无法理解什么懒汉恶汉命名法就直接分析写法了)。 有两种利用类的加载模式来创造单例的方法。
- 直接把单例写在static field里,这里就利用了类的初始化是lazy、线程安全且只会发生一次且static字段是类字段只有一个的特点;
- 把单例写在类的内部静态类,因为内部类并不跟随外部类一起初始化,只有当真正要使用内部类的方法和类属性时才会初始化,这样就可以更加lazy化;(当然为了一个单例实现一个类,只有一个static字段也可以)
public class Singleton {
static
{
System.out.println("singleton part is initialized");
}
// 第1种单例写法
private static TestClassLoader classLoader = new TestClassLoader();
public static TestClassLoader getClassLoader(){
return SingletonHolder.classLoader;
}
// 第2种单例写法
private static class SingletonHolder{
static {
System.out.println("holder initialized");
}
private static TestClassLoader classLoader = new TestClassLoader();
}
public static TestClassLoader getClassLoader2(){
return SingletonHolder.classLoader;
}
}
类的加载器
分类及namespace
讲类的加载器往往会讲三大加载器bootstrap、ext和app-classloader和双亲委派模型。但是从jvm角度讲,类的加载器分为两类:jvm直接提供的bootstrapClassloader,和其他用户实现的classloader。用户实现的cl都是抽象类ClassLoader的一个实例。除了array对象由jvm直接创建之外,所有的class和interface都由classloader来load。
类的加载器参与类的运行时命名空间(N,Li):JVM specification规定,运行时对于一个类的定义是(类的名字+类的defining loader)两者的组合,也就是如果你用classloader1 load了名字为N的类,会在运行时标记为NL1,用classloader2 load 的N是另一个运行时的类称为NL2,这两个类就是完全不同的类了。equal,instanceof在两个类及类的实例之间都不能成立。
parent delegation
刚刚提到一个词叫做defining loader,什么叫definingloader呢。这里就涉及类的委派模型classloader delegation model。一个classloader C可以直接创建class或者把它委派给另一个classloader D,直接创建这个class的loader D称为defining loader,发起创建动作C的称为initiating loader。当然C和D可以是一个loader- -。
最常听到的双亲委派总感觉翻译有点问题- -,英文parent delegation其实源自于类的层次关系,classloader中往往通过组合而设置一个逻辑上的parent Classloader来作为委派对象,而不用继承的实际父类。下面分析一下源码中涉及parent的部分。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
# 核心classLoader中设置parent字段,从而所有具体类都有一个parent,不需要树状的继承关系也可以通过parent来找到委派的对象;
private final ClassLoader parent;
public URLClassLoader(URL[] urls, ClassLoader parent) {
# URLclassloader是抽象类Classloader的一个实现,大家常常提到的ext/appClassloader都继承自这个类;
# 虽然ext/app都继承自这个类,但是他们的parent并不是这个类;
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}
static class AppClassLoader extends URLClassLoader {
具体的双亲委派过程核心就在于Classloader中的loadClass的定义了。Classloader完整的实现了loadClass的核心步骤:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
# (1)保证loading的线程安全设置lock
synchronized (getClassLoadingLock(name)) {
# (2)保证类只加载一次,所以获得锁之后再次检查是否已加载
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
# (3-1) 如果有parent delegation,则直接委派;
if (parent != null) {
c = parent.loadClass(name, false);
} else {
# (3-2)null代表parent是jvm中的bootstrap loader,直接委派bootstrap;
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
# (4)parent委派找不到类时,才自己尝试加载类,findClass是该抽象类扩展的核心
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();
}
}
# (5) 通过参数判断是否进行link过程;
if (resolve) {
resolveClass(c);
}
return c;
}
}
通过classloader中loadclass的代码可以发现双亲委派的核心过程是“啃老”- -!先让parent干,干不动了再自己干。同时可以发现,bootstrap被null代表,是默认的第一个parent。当然这个模型可以通过子类overriding掉,但这个模型是一个推荐的类加载通用模型。
launcher
那么为什么一般讲类的加载器都会讲ext和app-classloader呢,这里就要涉及java的Launcher类了。java Laucher涉及三个classloader:设置了bootstrap的路径,定义并使用内部静态类ExtClassLoader和AppClassloader。
bootstrapClassloader直接由jvm实现,加载java的lib中的最核心的类,这个加载过程还会根据文件名过滤比如rt.jar的类库都是由bootstrap直接加载的。
从laucher中的源码可以看到,Ext和App都是UrlClassloader的子类。ExtClassLoader设定parent是null,null代表jvm内部c++实现的bootstrapClassloader;Ext对应的路径是java.ext.dirs系统变量中的类目录; AppClassloader被launcher将其parent设置为ExtClassLoader,其对应的类加载路径是java.class.path也就指向用户实现的类的CLASSPATH。根据这两个类的设定就有了一个系统加载对类进行加载的委派路线,参考下图。
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
launcher所在rt.jar是由bootstrap进行加载的,初始化时生成了实例。具体分析请直接参考下面源码及注释。
private static Launcher launcher = new Launcher();
public Launcher() {
//(1)这里初始化extClassloader和appClassloader,并把ext设置为app的parent
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// (2)这里把app设置为线程上下文里的类加载器,从而建立起了系统的类的委派模型;
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
本文的内容就到此为止了,一下篇会讲完整个jdbc getConnection的过程,大致的内容有spi模式,doPrivilege,class.forName的用法:)