一、基础概念
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把该类加载到JVM。
其主要流程如下:
1.什么是类加载
那么什么是类加载?
Java的类加载,就是把字节码格式“.class”文件加载到JVM的方法区,并在JVM的堆区建立一 个java.lang.Class对象的实例,用来封装Java类相关的数据和方法。
那Class对象又是什么呢?
你可以 把它理解成业务类的模板,JVM根据这个模板来创建具体业务类对象实例。
2.运行jar文件的方式
执行下面命令就可以运行jar包
java -jar xxx.jar
但是上述方式如果关闭了窗口或者ctrc+c后进程就终止了。
所以一般建议以下方式进行启动
nohup java -jar myapp.jar &
以上命令会以nohup方式启动“myapp.jar”应用程序,并且不会因为控制台关闭或者SSH连接断开而停止。
现在,“myapp.jar”已被成功地转移到后台进程中。
3.懒加载
JVM并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加 载。
二、类加载机制
1.类加载整体流程
基本流程:class文件加载至内存,然后执行链接(验证、准备、解析),初始化;最终形成JVM可以直接使用的JAVA类型的过程。
加载:在方法区形成该类的运行时数据结构;
在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等。
在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接:class文件是否存在问题;一些符号引号替换成直接引用。具体如下:
校验:检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节码验证,符号引用验证) 准备:给类的静态变量分配并初始化存储空间;会赋予一个默认值但不是真正的代码定义的值。 解析:将常量池中的符号引用转成直接引用;
初始化:到了初始化阶段,才真正去执行Java里面的代码。主要为静态变量赋值和执行静态块,此时赋予的值才是真正代码中定义的值。初始化一个类时,会先初始化它的父类。
虚拟机会保证一个类的初始化在多线程环境中被正确加锁和同步。
注意:
1.类加载的过程中,是不会涉及到堆内存的。
2.主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
2. 类加载器
顾名思义就是来完成类加载这个过程的组件。
站在Java虚拟机的角度讲,只存在两种不同的类加载器:
一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分:
另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
1.类加载器层次结构
类加载器的层次结构如下
1.引导类加载器bootstrap classloader
加载JAVA核心库($JAVA_HOME/jre/lib/rt.jar),原生代码实现(C++),并不继承自java.lang.ClassLoader。
2.扩展类加载器extensions classloader
负责加载扩展目录($JAVA_HOME/jre/ext/*.jar)下的类包
由sun.misc.Launcher.ExtClassLoader实现
3.应用程序类加载器 application classloader(也称系统类加载器)
一般来说,JAVA应用的类由它加载,即加载路径是classpath下的路径。
主要就是加载你自己写的那些类,比如如下类:
由sun.misc.Launcher.AppClassLoader实现。应用程序默认用它来加载类。
4.自定义类加载器
开发人员继承java.lang.ClassLoader实现自己的类加载器。(自定义类加载器不会打破双亲委托机制)
这里请你注意,类加载器的父子关系不是通过继承来实现的,比如AppClassLoader并不是ExtClassLoader的 子类,而是说AppClassLoader的parent成员变量指向ExtClassLoader对象。
同样的道理,如果你要自定义类 加载器,不去继承AppClassLoader,而是继承ClassLoader抽象类,再重写findClass和loadClass方法即可。
2.ClassLoader类加载器
ClassLoader的基本职责就是:
第一,根据指定的类名称,找到或者生成对应的字节码,并根据字节码生成class对象
第二,加载JAVA应用所需的资源,如配置文件等。
JVM类加载是由类加载器来完成的,JDK提供一个抽象类ClassLoader,这个抽象类中定义了三个关键方法分别是:findClass,defineClass和loadClass,理解清楚它们的作用和关系非常重要。
java.lang.ClassLoader实现源码如下:
public abstract class ClassLoader {//每个类加载器都有个⽗加载器private final ClassLoader parent;protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);//如果没有加载过if (c == null) {long t0 = System.nanoTime();try {//先委托给⽗加载器去加载,注意这是个递归调⽤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}// 如果⽗加载器没加载成功,调⽤⾃⼰的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 statsPerfCounter.getParentDelegationTime().addTime(t1 - t0);PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}protected Class<?> findClass(String name){//1. 根据传⼊的类名name,到在特定⽬录下去寻找类⽂件,把.class⽂件读⼊内存...//2. 调⽤defineClass将字节数组转成Class对象return defineClass(buf, off, len);}// 将字节码数组解析成⼀个Class对象,⽤native⽅法实现protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)throws ClassFormatError{protectionDomain = preDefineClass(name, protectionDomain);String source = defineClassSourceLocation(protectionDomain);Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);postDefineClass(c, protectionDomain);return c;}}
从上面的代码我们可以得到几个关键信息:
- JVM的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个parent字段,指向父加载器
- findClass方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件 读到内存得到字节码数组,然后调用defineClass方法得到Class对象。
- defineClass是个工具方法,它的职责是调用native方法把Java类的字节码解析成一个Class对象,所谓的 native方法就是由C语言实现的方法,Java通过JNI机制调用。
- loadClass是个public方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不 是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用, 也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类时,会先委托父加载器去加 载,然后父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,才会交还 给子加载器加载,这就是双亲委托机制。
类加载器初始化过程
1.会创建JVM启动器实例sun.misc.Launcher。
2.sun.misc.Launcher初始化使用了单例模式设计(饿汉式),保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
3.在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。
4.JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。
其源码如下:
public Launcher() {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);}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);}}
2.2.2 双亲委托机制
JDK默认的类加载机制是双亲委派机制。
流程:
简单的说就是某个特定的类加载器接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,比如说从应用加载器委托给扩展类加载器,从扩展类加载器委托给引导类加载器。这种委托,直至委托到层次最高的类加载器,即引导类加载器,如果委托的父类加载器可以完成加载任务,那么成功返回;只有父类加载器无法完成时,才去依次交给下层加载直到自己加载。
加载具体流程如下:
例如:当jvm要加载Test.class的时候,
(1)首先会到自定义加载器中查找,看是否已经加载过,如果已经加载过,则返回字节码。
(2)如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class。
(3)如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。
(4)如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。
(5)如果BoopStrap ClassLoader依然没有加载过,则到自己指定类加载路径下(“sun.boot.class.path”)查看是否有Test.class字节码,有则加载返回,没有通知下一层加载器ExtClassLoader到自己指定的类加载路径下(java.ext.dirs)查看。
(6)依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException。
源码分析:
java.lang.ClassLoader类的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {//父加载器为空则,调用Bootstrap Classloaderc = 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();//父加载器没有找到,则调用findclassc = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
源码解读
-
首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
-
如果此类没有加载过,那么,再判断一下是否有父加载器;
如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。 -
如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
思考:
写一个自定义类加载器:
参考:https://gitee.com/daiwei-dave/core-java.git
2.2.3 为什么采用双亲委派模型?
1.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改.
2.类的全局唯一性:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性.
- 使得Java类随着类加载器不同而具备带优先级的层次关系,如java.lang.Object(位于rt.jar内),无论那个类加载器要加载该类,最终都委派给顶层引导类加载器,因此Object类在程序的各种类加载环境中都是同一个类。即能够有效确保一个类的全局唯一性。
- 相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,(比如引用了不同的jar包,里面都自己加载了一套Object类)Java类型体系中最基础的行为也就无从保证,应用程序也将会变得一片混乱。如果有兴趣的话,可以尝试去写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
- 保证了Java核心库的类型安全。核心库中所有系统都只会有一个类。
2.2.4 破坏双亲委派模型
使用JNDI服务、代码模块热部署OSGi等方式可以打破双亲委派模型。
1、继承ClassLoader并重写loadClass方法和findClass方法。
JDK1.2之前还没有引入双亲委派模式,为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。
JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
2、JNDI服务的代码有启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用有独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,单启动类加载器不可能“认识”这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这个行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。
Java中所有涉及SPI的加载动作基本上都是采用这种方式,例如:JNDI、JDBC、JCE、JAXB、和JBI等。
3、业界“事实上”Java模块化标准的OSGi,它实现模块化热部署的关键就是它自定义的类加载器机制的实现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。具体详见《深入理解虚拟机》第7章。
2.2.5 Tomcat类加载机制
Tomcat 如果使用默认的类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
- web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的类加载机制行不行?
答案是不行的。为什么?
为什么不行?
我们看,第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样,是不行的,tomcat的类是应用程序的。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热修改(楼主起的名字),jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。
那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
Tomcat 如何实现自己独特的类加载机制?
所以,Tomcat 是怎么实现的呢?牛逼的Tomcat团队已经设计好了。我们看看他们的设计图:
我们看到,前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。
其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp(web应用)访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
Tomcat类加载过程
tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等)。
各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。
具体的加载逻辑位于WebAppClassLoaderBase.loadClass()方法中,代码篇幅长,这里以文字描述加载一个类过程:
1.先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则 继续下一步。
2.让系统类加载器(AppClassLoader)尝试加载该类,此时还是用双亲委派模型,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
3.前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
4.最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。主要加载一些公用的如tomcat自带的jar包。
第3第4两个步骤的顺序已经违反了双亲委托机制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一样是违反了双亲委托。
原文链接:https://blog.csdn.net/qq_38182963/article/details/78660779
2.3 Java创建对象的过程
Java是一门面向对象的编程语言,在Java程序运行过程中每时每刻都有对象被创建出来。
在语言层面上,创建对象通常仅仅是一个new关键字而已,而在虚拟机中,对象的创建又是怎样一个过程呢?new一个对象可以分为两个过程:类加载和创建对象。
1.类加载
参考类加载机制,使用双亲委派模型
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况
划分内存的方法:
- “指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。(可以补一个脑图理解)
- “空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表, 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
解决并发问题的方法:
- CAS(compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。即在分配内存时,如果发现已有其他线程被占用,则进行重试
- TLAB本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。这样每个线程之间就相互隔离了。
通过XX:+UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。
3.初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。
这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈
希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
5.执行init方法
执行初始化方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法
2.4 SPI
SPI机制简介
SPI的全名为Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。
主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。
简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。
面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。
为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
参考:
1.深入理解SPI机制 https://www.jianshu.com/p/3a3edbcd8f24
2.阿里面试:什么地方违反了双亲委派模型
https://www.sohu.com/a/334000357_505800
2.5 JNDI
JNDI是 Java 命名与目录接口(Java Naming and Directory Interface),在J2EE规范中是重要的规范之一,不少专家认为,没有透彻理解JNDI的意义和作用,就没有真正掌握J2EE特别是EJB的知识。
那么,JNDI到底起什么作用?
要了解JNDI的作用,我们可以从“如果不用JNDI我们怎样做?用了JNDI后我们又将怎样做?”
没有JNDI的做法:
程序员开发时,知道要开发访问MySQL数据库的应用,于是将一个对 MySQL JDBC 驱动程序类的引用进行了编码,并通过使用适当的 JDBC URL 连接到数据库。
如下:
Connection conn=null;
try { Class.forName("com.mysql.jdbc.Driver", true, Thread.currentThread().getContextClassLoader()); conn=DriverManager.getConnection("jdbc:mysql://MyDBServer?user=xxx&password=xxx"); ...... conn.close();
} catch(Exception e) { e.printStackTrace();
} finally { if(conn!=null) { try { conn.close(); } catch(SQLException e) {} }
}
这是传统的做法,也是以前非Java程序员(如Delphi、VB等)常见的做法。这种做法一般在小规模的开发过程中不会产生问题,只要程序员熟悉Java语言、了解JDBC技术和MySQL,可以很快开发出相应的应用程序。
没有JNDI的做法存在的问题:
1、数据库服务器名称MyDBServer 、用户名和口令都可能需要改变,由此引发JDBC URL需要修改;
2、数据库可能改用别的产品,如改用DB2或者Oracle,引发JDBC驱动程序包和类名需要修改;如要修改:com.mysql.jdbc.Driver,可能会用Oracle的驱动。
3、随着实际使用终端的增加,原配置的连接池参数可能需要调整;
解决办法:
程序员应该不需要关心“具体的数据库后台是什么?JDBC驱动程序是什么?JDBC URL格式是什么?访问数据库的用户名和口令是什么?”等等这些问题。
程序员编写的程序应该没有对 JDBC 驱动程序的引用,没有服务器名称,没有用户名称或口令 —— 甚至没有数据库池或连接管理。
而是把这些问题交给J2EE容器来配置和管理,程序员只需要对这些配置和管理进行引用即可。即由硬编码转为软编码。
由此,就有了JNDI。
用了JNDI之后的做法:
使用spi机制,动态切换数据库驱动。
思考
1.在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么
代码测试:
package java.lang;/*** @author daiwei* @date 2019/2/11 15:42*/
public class String {public static void main(String[] args) {
// String string=new String();
// string.say();say();}public static void say(){System.out.println("hahah");}
}
结果如下:
原因分析:
更据Java的双亲委托机制,Java首先会自下而上查找java.lang.String是否已经被加载,结果发现bootstrap classloader类已经加载了,故不会再在重新加载java.lang.String类了,直接返回之前已经加载的java.lang.String类,而之前的类没有main方法。
故抛出找不到对应方法的错误。如果换成package.String等其他包名,则可以被加载
2.实战加载resource文件下的配置
使用spring提供的工具org.springframework.core.io.DefaultResourceLoader加载配置文件
3.JDBC中如何打破双亲委派模型
我们平时看到的mysql的加载是这个样子的:
DriverManager的初始化方法loadInitialDrivers,大家可以从中看到先是获取jdbc.drivers属性,得到类的路径。然后通过系统类加载器加载。
其源码如下:java.sql.DriverManager#loadInitialDrivers
private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// If the driver is packaged as a Service Provider, load it.// Get all the drivers through the classloader// exposed as a java.sql.Driver.class service.// ServiceLoader.load() replaces the sun.misc.Providers()AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();/* Load these drivers, so that they can be instantiated.* It may be the case that the driver class may not be there* i.e. there may be a packaged driver with the service class* as implementation of java.sql.Driver but the actual class* may be missing. In that case a java.util.ServiceConfigurationError* will be thrown at runtime by the VM trying to locate* and load the service.** Adding a try catch block to catch those runtime errors* if driver not available in classpath but it's* packaged as service and that service is there in classpath.*/try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}}
4.如何理解loadclass和findclass?
loadclass定义类加载的流程
findclass真正的去执行加载