最近在准备一个数据库框架的专题,想从driver一路讲到上层orm框架。在准备JDBC这层时发现其中有不少可讲的java知识点。因而舍远求近,先讲讲框架里的java。java
本文选取JDBC获取connection这个切入点,但愿类的加载、SPI模式和一些涉及到的其余知识点。因为篇幅缘由本文先讲解类的加载过程。 经过学习本文能够学习如下内容:mysql
从下面这个代码片断能够发现JDBC得到一个可以使用的connection仅仅须要调用一个静态方法。c++
// 实验脚本
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以后才执行这段关键代码。sql
//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。bootstrap
这里的加载和类的加载容易引发歧义,加载在不一样的语境能够指整个类的加载过程(包括加载、链接和初始化),也可能指类的加载中的第一步loading。安全
下面看一下loading作什么,不作什么:bash
也有人翻译称为经过名字找到字节流,这种说法有一点不严谨可是适用于大部分场景。好比Class.forName每每是经过提供一个名字来定位类的文件的。数据结构
找到,这个信息就很是模糊了,在哪儿找到彻底不限制。这也给了classloader极大的创造空间,好比能够经过DynamicProxy中动态生成字节流的方法体会自定义类的生成与加载过程,具体参考java.lang.reflect.Proxy/sun.misc.ProxyGenerator生成二进制字节流的源代码。关于loading下文的类加载器章节会继续探讨。多线程
这句话的信息量就很是大了:这里涉及JVM中的两个空间(1)方法区method area, (2)动态常量区run-time constant pool;同时涉及两个类C和D。
假设C是咱们要加载的类,D就是触发C加载的类。首先D在本身的run-time constant中得到C的引用,把C的二进制字节流中所表明的静态存储结构转化为method area/方法区的数据结构并建立class对象做为这个类的访问入口。这里具体的数据结构和内存分区和jvm的不一样实现相关。
讲到link一会儿想到了咱们林克老师,塞尔达的创造者曾解释以林克为主角的游戏为何命名为塞尔达:塞尔达表明了这个充满无穷魅力的世界,而林克就是链接咱们进入这个世界的人。回到正题,linking过程,其实也是要把这个进入jvm method area的类和运行时联系在一块儿,从而类/接口进入可执行状态。
链接过程包括三个过程:验证(verification),准备(preparation)和解析(resolvation)。
其实java编译器能够确认编译过的字节码是安全的,可是正如loading中讲到的,二进制字节能够来自各个地方。来自各类不明途径的二进制字节码是否是有错误,是否是有危险,就值得怀疑了。因此在类投入使用以前须要进行验证。
验证的内容包括文件格式,语言规范,数据流控制流等。这里就不展开了。
准备是为static fields分配内存并赋予默认值的过程。 若是一个类的内容没有异常,那么就进入了准备阶段。这一阶段jvm会为类的静态变量分配内存并赋予默认值,注意准备阶段并不会执行任何static fields的初始化和static代码块。
java代码中各类引用在编译时统一用规范的符号引用(symbolic references)来表示,并存储在class文件里。符号引用是一种逻辑定位符,追求定位无歧义,但并不表示实际的内存地址。但当真正执行jvm指令时,须要具体能找到数据的内存地址的方法,因而就有了直接引用(direct references)。直接引用可使指针、偏移量等具体不一样实现。
JVM规范规定了在执行一系列指令( anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface等)时须要直接引用,而解析就是将符号引用替换为直接引用的过程。
从验证字节流、分配内存到引用解析能够看出这一切都是为执行类中定义的代码作准备。
一切准备就绪以后,就开始执行类/接口的初始化方法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中定义的初始化条件比较经常使用到的有:
下面有例子请参考注释和输出:
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以后。
这里就涉及到另外一个常见的问题,单例模式怎么写(没法理解什么懒汉恶汉命名法就直接分析写法了)。 有两种利用类的加载模式来创造单例的方法。
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;
}
}
复制代码
讲类的加载器每每会讲三大加载器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在两个类及类的实例之间都不能成立。
刚刚提到一个词叫作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掉,但这个模型是一个推荐的类加载通用模型。
那么为何通常讲类的加载器都会讲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的用法:)