网上有很多的Java类加载机制的介绍, 但是对于初学者而言看起来都太过于深疏, 因此在本文用图解和例子的方式为本文的读者介绍Java的类加载机制。
委派模型介绍:
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
通俗的理解就是:
String.class
:
String.class
。String.class
出来。代码理解:
URLClassLoader loader = (URLClassLoader) Init.class.getClassLoader();
while (loader != null) {
System.out.println(loader.getClass().getName() + " 加载的路径:");
URL[] urls = loader.getURLs();
for (URL url : urls)
System.out.println(url);
System.out.println("----------------------------");
loader = (URLClassLoader)loader.getParent();
}
System.out.println("BootstrapClassLoader加载路径: ");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url);
}
输出(有删减):
sun.misc.Launcher$AppClassLoader
file:${JAVA_HOME}/Contents/Home/jre/lib/charsets.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/deploy.jar
file:/Users/baidu/workspace/qyp/job/target/classes/
file:${M2_HOME}/org/apache/zookeeper/zookeeper/3.3.6/zookeeper-3.3.6.jar
file:${M2_HOME}/com/alibaba/fastjson/1.2.7/fastjson-1.2.7.jar
----------------------------
sun.misc.Launcher$ExtClassLoader
file:${JAVA_HOME}/Contents/Home/jre/lib/ext/cldrdata.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/ext/dnsns.jar
----------------------------
BootstrapClassLoader加载路径:
file:${JAVA_HOME}/Contents/Home/jre/lib/resources.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/rt.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/sunrsasign.jar
可以看出:
${JAVA_HOME}/jre/lib
下面的部分jar包。比如java.*、sun.*
${JAVA_HOME}/jre/lib/ext
下面的jar包。比如javax.*
WEB-INF/class
和WEB-INF/lib
.通过不同的 完整类名 和 classloader, 可以区分两个类。好处为内存隔离(最常见的就是静态变量)。
针对最后一点:类Foo.class, 如果
ClassLoader loader1 = new URLClassLoader();
和ClassLoader loader2 = new URLClassLoader();
loader1和loader2去加载类Foo.class, 得到的Class也不是一个类。
且看问题:在web应用中假如部署了多个webapp. 为了方便共享就预先在Tomcat lib里面内置了部分类比如Spring、JDBC。而用户自备也有类似的Jar包。 这样会引起什么样的冲突?
答案是不会冲突。
Tomcat提供了一个Child优先
的类加载机制:首先由子类去加载, 加载不到再由父类加载。就很好的规避了这个问题。WEB-INF/lib
目录下的类的加载优先级是优于Tomcat lib的。(配置文件在server.xml里面的
default false)上。 可见代码片段:
WebappClassLoaderBase#loadClass
boolean delegateLoad = delegate || filter(name, true);
针对Tomcat, 做一个加载路径的介绍:
java org.apache.catalina.startup.Bootstrap start
${JAVA_HOME}/jre/lib
部分jar包${JAVA_HOME}/jre/lib/ext
下面的jar包bootstrap.jar
和tomcat-juli.jar
(只显示的指定了这两个jar包)catalina.properties#common.loader
配置)Filter、Listener、Servlet
等入口都是被WebappClassLoaderBase加载的,而一般开发者不会主动指定ClassLoader。那么除非指定了ClassLoader,所有的webapp都是它加载的(刚好它的加载空间包含了这些类)
Thread.currentThread().getContextClassLoader()
一般有两个用处:给SPI用, 找配置文件用。
UserClassLoader -> AppClassLoader->ExtClassLoader -> Bootstrap
委派链左边的ClassLoader就可以很自然的使用右边的ClassLoader所加载的类。
情况反过来,右边的ClassLoader所加载的代码需要反过来去找委派链靠左边的ClassLoader去加载东西怎么办呢?没辙,双亲委托机制是单向的,没办法反过来从右边找左边。
ServiceLoader.load(Class.class);
在加载类的时候, ServiceLoader由BootStrap加载,而一般的SPI都是在用户的classpath下。鉴于方法调用默认是使用的调用类的ClassLoader去加载, 显然BootStrap是加载不了没在它的路径下的Class的, 这个时候就可以传入一个Thread.currentThread().getContextClassLoader()
, 就可以很轻松的找到资源文件.
这个跟上诉的SPI机制其实也差不多, 都是每个ClassLoader负责一定的区域, 如果当前区域找不到再使用线程的Loader去找。
比如在Tomcat中执行一个 new File(), 会不会发现文件到${catalina.home}/bin
里面去了?
当需要用一个类的时候, 必须先加载它。
老生常谈:
解读(Useless.class为例):
public class Useless {
public Serializable s1 = new Serializable() {
{
System.out.println("域变量");
}
};
public static Serializable s2 = new Serializable() {
{
System.out.println("静态域变量");
}
};
public static int num = 3;
static {
System.out.println("静态代码块");
}
{
System.out.println("代码块");
}
}
可以看到, 类加载的整个过程跟域变量和代码块都是没什么关系的
方式一:
Class.forName
方式二
ClassLoader.loadClass
见代码片段:
Class z;
z = Class.forName("Useless"); // 1
z = Class.forName("Useless", true, MainFather.class.getClassLoader()); // 2
z = Class.forName("Useless", false, MainFather.class.getClassLoader()); // 3
z = MainFather.class.getClassLoader().loadClass("Useless"); // 4
一般理解为 1, 3, 4 等价。2会初始化类里面的静态元素和静态代码块(即类加载的初始化步骤)。
摘自http://www.cnblogs.com/ITtangtang/p/3978102.html
(1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
(2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
类只有在加载进入JVM之后才能被使用, 但是一般情况下还需要把类做实例化操作后来用。 一般区分为显示的实例化和隐式的示例化。
类的实例化的目的是为了得到一个类的对象。
2019年04月11日
闲来无事翻了翻老文章。 发现忘了介绍Java类加载如何保证在内存里面只有这一个类对象的。
声明 这个题目是个伪命题,如果自研ClassLoader, 然后还不符合规范去实现, JVM里面肯定是会有多个Class的对象的。
本小节只讲多线程环境下的Case。
BootStrap ClassLoader
:
既然是类加载,在双亲委派模型下, 类似于 ”rt.jar“ 一类的类文件, BootStrapClassLoader由 加载。
具体源码没有翻过, 不过main() 函数里面一定会触发加载Object.class, String.class。
此时不存在多线程的情况(`执行多个java命令那叫多进程,不是一个JVM`)。
URLClassLoader
:
ExtClassLoader 是它的实现类。
最终的加载是委托给 ClassLoader#loadClass(String, boolean)。
它使用了一个同步块,同步块的对象锁锁的是 #getClassLoadingLock(String)。
使用了ConcurrentHashMap的一个特性: putIfAbsent。
因此,多线程环境中:
putIfAbsent 保证了只有一个线程能往ConcurrentHashMap里面塞对象,且他们GET的对象一定是同一个。
synchronized 保证了多线程环境下,只有一个类能够被加载。 之后的类加载都是获取加载好的类。
AppClassLoader
:
AppClassLoader继承于URLClassLoader, 但是重写了ClassLoader#loadClass(String, boolean)。
这个逻辑很简单,先找是否加载了这个类knownToNotExist(String), 方法是同步的。
否则沿用ClassLoader#loadClass(String, boolean)逻辑。
如上, 至少在JDK原生的ClassLoader环境下, JDK通过synchronized
/ConcurrentHashMap
等机制保证了各种环境下Class对象的唯一性。
至于BootStrapClassLoader, 没有翻源码。String
和 Object
肯定是独一份的。
见类:Useless.java
import java.io.Serializable;
public class Useless extends UselessParent {
public Serializable s1 = new Serializable() {
{
System.out.println("域变量");
}
};
public static Serializable s2 = new Serializable() {
{
System.out.println("静态域变量");
}
};
static {
System.out.println("静态代码块");
}
{
System.out.println("代码块");
}
}
见类:UselessParent.java
import java.io.Serializable;
public class UselessParent {
public Serializable s1 = new Serializable() {
{
System.out.println(getClass() + "域变量");
}
};
public static Serializable s2 = new Serializable() {
{
System.out.println(getClass() + "静态域变量");
}
};
static {
System.out.println("静态代码块");
}
{
System.out.println(getClass() + "代码块");
}
}
和执行类:MainFather.java
public class MainFather {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException {
Useless u;
// u = new Useless();
System.out.println(Useless.s2);
System.out.println("-----------------------------------------");
Class z;
z = Class.forName("com.baidu.qyp.job.clazz.Useless");
System.out.println("-----------------------------------------");
z = Class.forName("com.baidu.qyp.job.clazz.Useless", true, MainFather.class.getClassLoader());
System.out.println("-----------------------------------------");
z = Class.forName("com.baidu.qyp.job.clazz.Useless", false, MainFather.class.getClassLoader());
System.out.println("-----------------------------------------");
z = MainFather.class.getClassLoader().loadClass("com.baidu.qyp.job.clazz.Useless");
System.out.println("-----------------------------------------");
u = (Useless) z.newInstance();
System.out.println("-----------------------------------------");
u = new Useless();
}
}
执行结果:
class UselessParent$2静态域变量
静态代码块
class Useless$2静态域变量
静态代码块
Useless$2@378bf509
-----------------------------------------
-----------------------------------------
-----------------------------------------
-----------------------------------------
-----------------------------------------
class UselessParent$1域变量
class Useless代码块
class Useless$1域变量
class Useless代码块
-----------------------------------------
class UselessParent$1域变量
class Useless代码块
class Useless$1域变量
class Useless代码块
执行附图:
这也很好的解释了一个问题: 为什么静态元素和静态代码块在一个虚拟机里面只会执行一次:
- 默认习惯都是不会指定ClassLoader的,所属类也就只有一次初始化过程。
- 赋值静态域,或者执行静态代码块,是在类加载的流程中执行的。而这样的操作只会有一次。
- 赋值域,或者执行代码块,是在类实例化的流程中执行的,这样的操作根据程序需求可能有多次。
这里的Web应用指的就是Tomcat Web应用。 其中的Tomcat启动模块跟普通Java应用并无区别。 附上一张Spring Web流程图。
参考文章:
http://www.cnblogs.com/ityouknow/p/5603287.html
http://www.cnblogs.com/ITtangtang/p/3978102.html
双亲委托加载的序列图 ↩︎