类加载运行全过程
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
package cn.pencilso.study.classloader;
public class TestRun {
private static int int1 = 1;
private static final int int2 = 2;
private static User user = new User();
public int add() {
return int1 + int2;
}
public static void main(String[] args) throws ClassNotFoundException {
TestRun testRun = new TestRun();
int add = testRun.add();
System.out.println("add-> "+add);
}
}
通过Java命令执行代码的大体流程如下:
其中classloader.loadClass的类加载过程有如下几步:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
-
加载 :读入class字节码文件,使用到类时才会加载,例如调用类的静态方法,new对象等。
-
验证:校验字节码文件的正确性。
-
准备:对静态变量分配内存,并赋予默认值,例如 int1的默认值是0,而user是null,至于int2则是2,因为int2 使用了final修饰词,所以int2是常量,不再是静态变量。
-
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main() 方法)替换为指向数据所存内存的指针或句柄等 (直接引用) ,这是所谓的静态链接过程 (类加载期间完成)。
动态链接是在程序运行期间完成的将符号引用替换为直接引用。
-
初始化:对类的静态变量初始化为指定的值,int1赋值为1 ,user 则会加载User类,创建User实例后赋值给user, 然后执行静态代码块。
主类在运行过程中如果使用到其他类,会逐步加载这些类。
jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
类加载器和双亲委派机制
Java中有多种类加载器
- 引导类加载器:负责加载jre/lib目录下的jvm的核心类库,rt.jar、charsets.jar 等。
- 扩展类加载器:负责加载jre/ext目录下的jvm扩展类库。
- 应用程序类加载器:负责加载ClassPath路径下的类,主要就是加载自己编写的那些类。
- 自定义加载器:负责加载指定目录下的类。
注意,引导类加载器是由C++所创建的,所以在Java代码中是获取不到引导类加载器的对象。
查看对应的几种类加载器
public static void main(String[] args) {
ClassLoader stringClassLoader = String.class.getClassLoader();
ClassLoader deskeyFactoryClassloader = DESKeyFactory.class.getClassLoader();
ClassLoader testJdkClassloader = TestJDKClassloader.class.getClassLoader();
System.out.println("stringClassLoader-> "+stringClassLoader);
System.out.println("fileClassloader-> "+deskeyFactoryClassloader);
System.out.println("testJdkClassloader-> "+testJdkClassloader);
}
-----------
stringClassLoader-> null
fileClassloader-> sun.misc.Launcher$ExtClassLoader@1b28cdfa
testJdkClassloader-> sun.misc.Launcher$AppClassLoader@18b4aac2
// String是Java核心类库,是由C++所编写的引导式加载器 bootstrapLoader,所以获取到的是null
// DESKeyFactory 是Java的扩展类库,所以获取到的是 ExtClassLoader
// testJdkClassloader 是自己编写的文件,所以类加载器是AppClassLoader
在创建Launcher时就创建好了ExtClassLoader、AppClassLoader两种加载器。
查看源码Launcher,分析ExtClassLoader、AppClassLoader是如何创建的。
// 单例 Launcher
private static Launcher launcher = new Launcher();
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
//创建扩展的类加载器
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
//创建应用的类加载器 , 并且将扩展库加载器传递到参数
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
......
}
ExtClassLoader 创建过程
// 1、单例模式 ,在createExtClassLoader方法进行创建
public static ExtClassLoader getExtClassLoader() throws IOException
{
if (instance == null) {
synchronized(ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
// 2、可以看到new了一个ExtClassLoader,并且把扩展类库的路径传给了构造参数
private static ExtClassLoader createExtClassLoader() throws IOException {
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
final File[] dirs = getExtDirs();
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
// 3、调用了supper方法,将类库路径传给了supper构造的参数中,并且将parent参数传递的是null,至此extClassLoader则创建出来了
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
SharedSecrets.getJavaNetAccess().
getURLClassPath(this).initLookupCache(this);
}
AppClassLoader 创建过程
// 1、获取classpath下的类路径,并且new了一个AppClassLoader,将路径与扩展库加载器传递到构造参数。
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
// 2、AppClassLoader构造方法,将类路径数组与扩展库类加载器传到了supper
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}
// 3、最后跟踪supper构造方法,将parent 保存在了一个成员变量里面。而对于AppClassLoader来说
// 这个parent就是扩展库的ClassLoader
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
....
}
类加载器初始化过程
类运行加载过程中会创建JVM启动器实例sun.misc.Launcher 并且使用了单例设计模式。
在Launcher构造方法内部,创建了两个类加载器,分别是:
- ExtClassLoader 扩展类加载器
- AppClassLoader 应用加载器 并且其parent是ExtClassLoader扩展类加载器
- JVM默认使用Launcher的getClassLoader方法返回的类加载器实例,加载应用程序,而这个getClassLoader方法返回的就是AppClassLoader应用加载器。
双亲委派机制流程
每个类加载器都会维护一个已加载过的类数组,当需要加载某一个类时,类加载器执行顺序是:自定义加载器(如果有的话)-> 应用类加载器 -> 扩展类加载器 -> 引导类加载器
假设现在需要加载一个User.class类,会从自定义加载器开始,先检查自身已加载类列表是否存在,如果存在则直接返回,如果不存在,调用parent类加载器进行加载,如果parent已加载列表也没有,则调用parent的parent类加载器,如此层层向上委托,直到没有parent为止。
如果最顶级的parent在它自己的已加载列表中找不到,则在它的类加载路径寻找文件,如果还找不到,则层层往下退回加载请求,每一层都执行同样的操作,只要有一层类加载器找到了,就会放到该加载器的已加载列表,并且返回该类的信息,如果所有的类加载器都没有找到,则抛出ClassNotFoundException。
双亲委派机制源码实现
源码在ClassLoader.loadCLass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 第一步,检查已经加载过的class,如果已加载过了,则直接return了。
Class<?> 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
}
if (c == null) {
// 如果所有的parent都加载不到的话,则去自己的类路径寻找文件
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;
}
}
为什么要设计双亲委派机制
- 沙箱安全机制 : 自己写的核心内库不会被加载,这样可以防止核心API库被随意篡改。
- 避免类的重复加载:当父加载器已经加载了该类时,就没有必要子CLassLoader再加载一次,保证被加载类的唯一性
尝试自定义String类如下,替换Java的核心类库String ,运行错误,提示找不到这个main方法。
原因String是Java核心类库,由引导类加载器最先加载的。在执行下列代码时,会委托给父加载器进行加载String,最后委托到引导类加载器的时候,会从引导类加载器返回回来,而引导类加载器所加载的String,是Java的核心类库,它的匹配规则是依据包名+类名的匹配规则来匹配。
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("---------My String ----------");
}
}
运行结果
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
全盘负责委托机制
“全盘负责” 是指当一个ClassLoader装在一个类时,除非显示的使用另外一个CLassLoader,该类所依赖及引用的类也由这个CLassLoader载入。
自定义类加载器
自定义类加载器,加载Class文件
实现一个简单的类加载器,只需要继承CLassLoader,并重写findClass 方法即可。
先准备一个示例类 : User
package cn.pencilso.study.classloader;
public class User {
public void sout(){
System.out.println("-------this is User Class --------");
}
}
对代码进行编译,编译之后class文件会在项目的target 目录下。
准备文件夹,将User这个类的class文件复制出来。
我把class文件放在了 "/Users/pencilso/Desktop/classloader/" + "cn/pencilso/study/classloader"
需要注意的是,后面的 "cn/pencilso/study/classloader" 对应的是这个类的包名。
pencilso@MacBook-Pro ~ % ls ~/Desktop/classloader/cn/pencilso/study/classloader
User.class
简单的类加载器实现
package cn.pencilso.study.classloader;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/Users/pencilso/Desktop/classloader/");
Class<?> userCLass = myClassLoader.loadClass("cn.pencilso.study.classloader.User");
Object userInstance = userCLass.newInstance();
Method soutMethod = userCLass.getDeclaredMethod("sout", null);
soutMethod.invoke(userInstance, null);
System.out.println("classLoader:" + userCLass.getClassLoader().getClass());
}
@AllArgsConstructor
@Data
public static class MyClassLoader extends ClassLoader {
/**
* class 文件所在的文件夹
*/
private String folder;
/**
* 从指定目录中寻找class文件
*
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException {
try {
//将包名中的 "." 替换为文件路径分隔符
String classPath = name.replace(".", File.separator);
FileInputStream fileInputStream = new FileInputStream(folder + classPath + ".class");
int len = fileInputStream.available();
byte[] classByte = new byte[len];
fileInputStream.read(classByte);
fileInputStream.close();
//解释class文件
return defineClass(name, classByte, 0, classByte.length);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
}
}
运行后,结果如下,可以看到已经调用到了User对象里面的"sout"方法,输出了 "this is User Class" 。
但是classLoader为什么还是AppClassLoader?
这是因为双亲委托机制的结果,自定义ClassLoader加载器也会向上层级委托。
-------this is User Class --------
classLoader:class sun.misc.Launcher$AppClassLoader
源码分析:
我们的自定义类加载器是继承自CLassLoader,那么在初始化时会执行CLassLoader的构造方法,如下:
// 1、ClassLoader的无参构造方法,调用了getSystemClassLoader,并且传入了二参构造
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
// 2、getSystemClassLoader方法是获取一个CLassLoader对象,其初始化方法是 initSystemClassLoader
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
// 3、初始化SystemClassLoader
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 调用了getClassLoader方法,而这个getClassLoader方法获取到的就是appClassLoader对象
scl = l.getClassLoader();
......
}
// 4、查看二参的ClassLoader构造方法,可以得知,它最后将AppClassLoader对象存在了parent字段中。
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
....
}
通过上面源码分析,我们自定义的类加载器,加载User.class,执行双亲委派机制时,委托到了AppClassLoader,后又委托到ExtClassLoader,后又委托到 BootstrapLoader ,而 BootstrapLoader 是肯定找不到这个类的,ExtClassLoader也找不到,委托退到AppClassLoader的时候找到了。所以上面的代码运行输出的ClassLoader是 AppClassLoader。
即使我们的User.class文件在项目的target目录下,和我们自己创建的外部文件夹都有一份,但是只要有一个类加载器加载到了文件,其他的类加载器则不再会加载。而AppClassLoader的层级比自定义ClassLoader的层级要高 。
那怎样让它用自定义加载器加载User.class ?
接下来,把源码中的User.java 删除,编译、重新运行,这次的结果是自定义的CLassLoader了,因为这时候target目录下已经没有了User.class ,所以AppClassLoader找不到这个类,委托会退到自定义CLassLoader,而自定义CLassLoader会去我们自定义的文件夹下面去找这个类文件。
-------this is User Class --------
classLoader:class cn.pencilso.study.classloader.MyClassLoaderTest$MyClassLoader
打破双亲委派机制
我们得知在加载类文件时,需要经过双亲委派机制,层级向上委托,层级向下退回。
那么如何指定让自定义加载器直接加载,而不向上委托?
之前提到过双亲委派机制的源码实现在 ClassLoader.loadCLass 中,那么自定义类加载器继承后,在MyClassLoader类中,重写loadClass方法即可。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
long t1 = System.nanoTime();
if (name.equals("cn.pencilso.study.classloader.User")) {
//当加载指定的类时,不委托给父加载器,自行加载。
c = findClass(name);
} else {
//默认委托给父加载器
c = getParent().loadClass(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;
}
}
接下来把源码中的User.java 文件添加回来,再次执行,最后输出的是我们自定义的类加载器了。
-------this is User Class --------
classLoader:class cn.pencilso.study.classloader.MyClassLoaderTest$MyClassLoader