JVM:类加载 & 类加载器
System virtual machine: a.k.a. full virtualization VM. Provides a substitute for a real machine (functionality needed to execute entire OS). Allows for multiple environments which are isolated from one another yet exist on the same physical machine.
Process Virtual Machine: a.k.a. application virtual machine or Managed Runtime Environment.
Process VM runs as a normal application inside a host OS and support a single process, is created when that process is started and destroyed when it exits.
Process VM’s purpose is to provide a platform-independent programming environment that abstracts away details of the underlying hardware and operating system and allows a program to execute in the same way on any platform.
e.g. JVM; Parrot virtual machine; .Net Framework.
*使用工具:
Source Insight - 查看openjdk源码
-> new project->选择openjdk所在目录->add project files时选择add all->add to project中“include top level sub-dirs”和“recursively add lower sub-dirs"全勾选
clion - 编写底层程序
idea / netbeans - 单步调试jdk
-> 新建项目->C++/基于现有源代码的C++项目->选定openjdk所在目录>
hsdb
*注:使用jclasslib时,如果源代码有改动,在idea中先build再在jclasslib中点刷新以更新字节码。
jps, jstack, jconsole等。
写JVM的步骤顺序:内存池 -> 垃圾回收器 -> 让JVM能跑单线程的Java程序 -> 让JVM能跑多线程的Java程序
类加载
Klass
Klass: (存在于元空间,)Java的每个类的对象在JVM中都有一个对应的Klass类实例,用于存储类的元信息(e.g. 常量池,属性信息,方法信息)。
*元信息包含字面量 (e.g. 类名、方属性名、属性签名、方法名等)。
Klass的继承结构
MasterspaceObj
|- Metadata
|- Klass
|- InstanceKlass
|- InstanceMirrorKlass
|- InstanceRefKlass
|- InstanceClassLoaderKlass
|- ArrayKlass
|- TypeArrayKlass
|- ObjArrayKlass
InstanceKlass: 表示普通(非数组)Java类。类加载器将.class文件加载进系统,将.class文件解析生成类的元信息,存储在InstanceKlass中。子类包括InstanceMirrorKlass、InstanceRefKlass和InstanceClassLoaderKlass。
InstanceMirrorKlass:镜像类,表示Java代码中的java.lang.Class类,存储在堆区。
* 因为静态变量存储在InstanceMirrorKlass中,所以静态变量也存储在堆区。
InstanceRefKlass: 表示java.lang.ref.Reference类的子类。
InstanceClassLoaeder: 用于遍历某个加载器加载的类。
Java中的数组不是静态数据类型(e.g. JVM内置的8种数据类型),是动态数据类型(i.e. 在运行期生成的)。
ArrayKlass: 存储数组类的元信息。
TypeArrayKlass: 表示基本类型的数组。
ObjArrayKlass: 表示引用类型的数组。
实验
证明java数组是动态数据类型
-> java main方法中new一个int/对象数组,编译运行
-> IDEA使用插件jclasslib查看字节码(idea->view->show bytecode with Jclasslib),main中显示newarray/anewarray,对应字节码手册中含义:“创建一个原始类型/引用型数组并将其引用至压入栈顶”。
查看java类对应的klass
-> HSDB -> Tools/Class Browser->找到目标类名即可找到对应内存地址
-> HSDB->Tools/Inspector->输入内存地址->可查看java类在内存中对应的klass类
or
-> while true维持程序运行,terminal输入jps –l获取当前运行进程id;
-> HSDB->file/attach to hotspot process->输入目标进程ID
-> 选中main线程,工具栏第二个按钮查看线程堆栈->可查看java对象底层内存地址
-> 复制内存地址->HSBD->tool/inspector->可查看java对象底层的实现类
HSDB attach后记得detach。
e.g.在main县城堆栈中查看数组对应的klass
类加载的过程
* openjdk源码中ClassState记录class的状态(分配内存、加载、链接、正在初始化、完全初始化、初始化出错)。
/openjdk/hotspot/src/share/vm/oops/instanceKlass.hpp
enum ClassState { allocated, // allocated (but not yet linked) loaded, // loaded and inserted in class hierarchy (but not linked yet) linked, // successfully linked/verified (but not initialized yet) being_initialized, // currently running class initializer fully_initialized, // initialized (successfull final state) initialization_error // error happened during initialization };
加载 -- 可以随便使用任何语言实现类加载器,只要能够达到这三个效果。
-> 通过类的全限定名获取存储该类的class文件(没有指明必须从哪获取);
-> 解析成运行时数据(instanceKlass实例),存放在方法区;
-> 在堆区生成该类的Class对象(instanceMirrorKlass实例)。
*方法区包含了ExtClassLoader区域和AppClassLoader区域,因为一般是AppClassLoader加载,类加载器解析时将得到的类的元信息存放在方法区的AppClassLoader区域。
JVM加载类是懒加载模式。-- 根加载器加载jar文件时并没有把其中所有的类都进行加载,而是只加载了一部分(预加载模式,只先加载常用的String,Thread,Integer等类)。
类加载的时机? --主动使用时
1) new, getstatic, putstatic, invokestatic字节码 i.e. java代码中使用new关键字实例化对象、读取或设置类的静态字段(final修饰的常量除外)、调用类的静态方法
2) 反射
3) 初始化子类时会去加载其父类
4) 启动类(main函数所在类)
5) 当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
从哪加载? - 因为没有指明从哪获取class文件,可采用的思路:
1) 从压缩包中读取 e.g. jar, war
2) 从网络中获取 e.g. Web Applet
3) 动态生成 e.g. 动态代理、CGLIB
4) 由其他文件生成 e.g. JSP
5) 从数据库读取
6) 从加密文件读取
验证 – 检查klass文件是否符合规范,判断版本, jvm能否正常运行klass,…etc.
1. 文件格式验证
2. 元数据验证
3. 字节码验证
4. 符号引用验证
//参考《深入理解java虚拟机》
准备
为静态变量分配内存和赋初值。实例变量没有赋初值一说,而是在创建对象时完成赋值。
例外:如果被Final修饰,编译时会给添加ConstantValue属性,准备阶段直接完成赋值,没有赋初值步骤。
不同数据类型对应不同的初值:
解析 – 间接引用转为直接引用
*常量池:包括静态常量池(class文件常量池)、运行时常量池(可在HSDB中查看)和字符串常量池(StringTable)。
* 字符串存储在StringTable,在堆区。
间接引用a.k.a. 符号引用:指向运行时常量池的引用
直接引用:内存地址
1. 类或接口的解析
2. 字段解析
3. 方法解析
4. 接口方法解析
解析后的信息存储在ConstantPoolCache类实例中。
何时解析? -- 思路有:
1) 加载阶段解析常量池时
2) 用时 // openjdk采用,在执行特定字节码指令(e.g. anewarray, checkcast, getfield, …)前进行解析。
初始化 – 执行静态代码段,完成静态变量的赋值。
java代码中定义一个static属性(静态字段、静态代码段),编译时字节码层面就会自动生成clint方法(只有一个)。
clint方法(i.e.字节码中生成的静态块)中语句顺序跟定义静态属性的java代码的编写顺序是保持一致的。
e.g.
实验
证明final成员没有赋初值而是直接赋值
-> 演示类中声明成员static final int a=10, static int b=10;
-> 查看字节码,a中有属性ConstantValue,代表没有赋初值,准备阶段就完成赋值。(b在准备阶段被赋初值0)
查看常量池
-> idea terminal切换到classes目录下, javap –verbose 对象全限定名(全限定名可在ide中选中类名右键copy reference得),输出的Constant pool:部分表示静态常量池
e.g. 静态常量池中Class对应#25,翻阅下方对应当前类名字符串。’#25’即为一个符号引用(指向常量池的引用)。
-> 运行程序,HSDB attach到目标进程,class browser点击目标对象,下方可查看动态常量池
e.g. 动态常量池中Class不再指向常量池,而是指向内存地址@0x000….,即为一个直接引用。//解析后符号引用转为直接引用
初始化实验1
public class Test { public static void main(String[] args) { TestA obj=TestA.getInstance(); System.out.println(TestA.val1); System.out.println(TestA.val2); } } class TestA { public static int val1; public static int val2=1; public static TestA instance=new TestA(); TestA() { val1++; val2++; } public static TestA getInstance() { return instance; } }
输出:1 2
原因:初始化时执行静态代码段,val1赋初值为0,val1赋值为1;执行构造函数后都+1;输出1 2.
初始化实验2 //将static val2定义移到构造方法后
public class Test { public static void main(String[] args) { TestA obj=TestA.getInstance(); System.out.println(TestA.val1); System.out.println(TestA.val2); } } class TestA { public static int val1; public static TestA instance=new TestA(); TestA() { val1++; val2++; } public static int val2=1; public static TestA getInstance() { return instance; } }
输出:1 1
原因:生成的静态块中语句顺序跟定义静态属性的java代码的编写顺序是保持一致的,所以val2经过构造函数后被定义语句覆盖回1;
程序的执行顺序: 1) clint方法 2) 默认构造方法(执行完++后val1=1,val2=1);静态块(val2又被赋值为1)。
加载实验1
public class Test { public static void main(String[] args) { System.out.printf(TestB.str); } } class TestA { public static String str="A str"; static { System.out.println("A Static Block"); } } class TestB extends TestA { static { System.out.println("B Static Block"); } }
输出:
A Static Block
A str
原因:A是B的父类,会被主动加载;B没有被使用,不会被加载
加载实验2
public class Test { public static void main(String[] args) { System.out.printf(new TestB().str); //new了B对象 } } class TestA { public String str="A str";//去掉static static { System.out.println("A Static Block"); } } class TestB extends TestA { static { System.out.println("B Static Block"); } }
输出:
A Static Block
B Static Block
A str
原因:AB都被使用,都会被加载(主动使用子类,就是间接在主动使用父类)
加载实验3
public class Test { public static void main(String[] args) { System.out.printf(new TestB().str); } } class TestA { static { System.out.println("A Static Block"); } } class TestB extends TestA { public String str="A str";//str从A移到子类B static { System.out.println("B Static Block"); } }
输出:
A Static Block
B Static Block
A str
原因:同上
加载实验4
public class Test { public static void main(String[] args) { System.out.printf(TestB.str); //没有new } } class TestA { static { System.out.println("A Static Block"); } } class TestB extends TestA { public static String str="B str"; //static成员 static { System.out.println("B Static Block"); } }
输出:
A Static Block
B Static Block
B str
原因:静态字段在子类里,子类会被加载
加载实验5
public class Test { public static void main(String[] args) { TestA arrs[] = new TestA[1]; } } class TestA { static { System.out.println("A Static Block"); } }
输出:无
原因:main中只是定义了一个数据类型,没有使用。
加载实验6
public class Test { public static void main(String[] args) { System.out.println(TestA.str); } } class TestA { public static final String str="A Str"; static { System.out.println("A Static Block"); } }
输出:A Str
原因:A没有被加载。虽然AStr在A类里,但是是被final修饰的常量,此常量被写入到Test类的常量池中 。(javap -verbose查看Test类的静态常量池可验证:)
加载实验7
import java.util.UUID; public class Test { public static void main(String[] args) { System.out.println(TestA.uuid); } } class TestA { public static final String uuid=UUID.randomUUID().toString(); static { System.out.println("A Static Block"); } }
输出:
A Static Block
838e8119-2f9c-4529-a0cf-3059d4a43027 //uuid
原因: 虽然uuid是final修饰,但randomUUID().toString()是动态运行的,uuid需要动态生成,不能写入到Test类的常量池。所以类A会被加载。
加载实验8
public class Test_2 { static { System.out.println("Test2 Static Block"); } public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz=Class.forName("com.xxx.TestA"); //defined in Test_1.java } }
输出:
Test2 Static Block
A Static Block
原因:因为反射,类12都会被加载
加载实验9
public class Test { public static void main(String[] args) { System.out.printf(B.str); } } class A { public static String str="str"; static { System.out.println("A Static Block"); } } class B extends A { static { str+="###"; System.out.println("B Static Block"); } }
输出:
A Static Block
str
原因:JVM先判断是否加载,后面才会有初始化动作发生。案例中B的内部没有任何东西被使用,所以没有加载B,B的静态块不会被执行。
读取静态变量的底层实现 涉及InstanceKlass, instanceMirrorKlass, ConstantPoolCache
实验 证明静态属性存储在镜像类中
public class Test { public static void main(String[] args) { System.out.printf(TestB.str); while (true) {} } } class TestA { public static String str="A str"; static { System.out.println("A Static Block"); } } class TestB extends TestA { static { System.out.println("B Static Block"); } }
输出:
A Static Block
A str
-> 运行,查出进程ID,在HSDB中attach
-> HSDB classbrowser找到类A的内存地址,输入到inspector
-> 可以在inspector类A中找到静态属性str是存储在oop Klass: java.mirror中。说明jdk8静态属性是存储在镜像类(instanceMirrorKlass)中的。(而不是存储在instanceKlass, jdk6之前是)
-> 同样操作在inspector类B的oop Klass:java.mirror中并没有找到静态属性str,说明静态属性str只存放在父类A。
- 既然静态属性str存放在父类A中,main中调用B.str是怎么找到它的?
- 两种实现思路:
1) 先从子类B的镜像类中取,如果有直接返回,没有则沿着继承链往上找;-- O(n)
2) 借助另外的数据结构,使用K-V格式存储。-- O(1)。
Hotspot采用的就是思路2,借助另外的数据结构ConstantPoolCache,常量池类ConstantPool有属性_cache指向该结构,每条数据对应一个类ConstantPoolCacheEntry。
* ConstantPoolCache: 用于存储某些字节码指令所需的解析(resolve)好的常量项,例如给[get|put]static, [get|put]field, invoke[static|special|virtual|interface|dynamic]等指令对应的常量池项用。
ConstantPoolCacheEntry的获取?
参考\openjdk\hotspot\src\share\vm\oop\cpCache.hpp,通过ConstantPoolCache的地址加上偏移量(ConstantPoolCache对象的内存大小,因为ConstantPoolEntry存放在ConstantPoolCache对象后面)
ConstantPoolCacheEntry* base() const { return (ConstantPoolCacheEntry*)((address)this + in_bytes(base_offset())); }
类加载器
JVM的类加载器包含两种类型:
1) 由C++编写的;-- 启动类加载器(Bootstrap Class Loader)
2) 由Java编写的 --其他继承java.lang.ClassLoader的类加载器
JVM也支持自定义类加载器。
各种类加载器之间逻辑上的父子关系不是真正的父子关系,没有直接从属关系。
启动类加载器
启动类加载器:JVM将C++处理类的一套逻辑定义为启动类加载器。启动类加载器没有实体。
因为由C++编写,无法被Java程序调用,在Java程序中显示null。
启动类加载器的加载路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); for (URL urL : urLs) { System.out.println(urL); }
启动类加载器、扩展类加载器和应用类加载器的父子关系链是如何建立的
JavaMain中调用了LoadMainClass,启动类加载器就是在这时加载的。
Openjdk/jdk/src/share/bin/java.c / JavaMain()
JavaMain(void * _args) { … mainClass = LoadMainClass(env, mode, what); … }
LoadMainClass中需要先找到Launcherhelper类。启动类加载器所做的事情就是加载类”sun/launcher/LauncherHelper”,checkAndLoadMain就是在LauncherHelper类里面。
Openjdk/jdk/src/share/java.c / GetLauncherHelperClass()
GetLauncherHelperClass(JNIEnv *env) { … NULL_CHECK0(helperClass = FindBootStrapClass(env, "sun/launcher/LauncherHelper")); … }
GetLauncherHelperClass主要调用FindBootStrapClass。FindBootStrapClass中用GetProcessAddress调用JVM动态链接库的JVM_FindClassFromBootLoader方法。
openjdk/jdk/src/windows/bin/java_md.c / FindBootStrapClass()
jclass FindBootStrapClass(JNIEnv *env, const char *classname) { … findBootClass = (FindClassFromBootLoader_t *)GetProcAddress(hJvm, "JVM_FindClassFromBootLoader"); … }
找到LauncherHelper类后,通过JNI执行LauncherHelper类的checkAndLoadMain方法。
用于加载main class(main方法所在的类),三种类加载器的父子链(->启动扩展类加载器->应用类加载器)也是在这次调用中完成的。
LoadMainClass返回的result其实就是调用checkAndLoadMain的结果 – main class。
Openjdk/jdk/src/share/bin/java.c / LoadMainClass()
LoadMainClass(JNIEnv *env, int mode, char *name) { … jclass cls = GetLauncherHelperClass(env); … NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls, "checkAndLoadMain", "(ZILjava/lang/String;)Ljava/lang/Class;")); … }
从checkAndLoadeMain开始都是java代码。checkAndLoadMain方法检查了运行模式,如果是class就直接命名,如果是jar包则从jar包中找,其他则报错。然后调用scloader.loadClass方法获得main class来返回。
openjdk/jdk/src/share/classes/sun/launcher/LauncherHelper.java / checkAndLoadMain()
public static Class<?> checkAndLoadMain(boolean printToStderr, int mode, String what) { … switch (mode) { case LM_CLASS: cn = what; break; case LM_JAR: cn = getMainClassFromJar(what); break; default: // should never happen throw new InternalError("" + mode + ": Unknown launch mode"); } … mainClass = scloader.loadClass(cn); … }
scloader是由ClassLoader.getSystemClassLoader()得到的,其中调用了initSystemClassLoader方法。
Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / getSystemClassLoader()
public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); … }
initSystemClassLoader中调用了Launcher.getLauncher方法。
Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / initSystemClassLoader
private static synchronized void initSystemClassLoader() { … sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); … }
Launcher构造函数中初始化了ExtClassLoader,再以ext为参数初始化appClassLoader。Ext其实就是parent。
线程上下文类加载器contextClassLoader也是在这时赋值的。
openjdk/jdk/src/share/classes/sun/misc/Launcher.java
public class Launcher { … private static Launcher launcher = new Launcher(); … public static Launcher getLauncher() { return launcher; } public Launcher() { … // Create the extension class loader extcl = ExtClassLoader.getExtClassLoader(); … // Now create the class loader to use to launch the application loader = AppClassLoader.getAppClassLoader(extcl); … Thread.currentThread().setContextClassLoader(loader); } … }
从getAppClassLoader()和其中调用的AppClassLoader构造函数可看出传入的extcl参数为parent。
openjdk/jdk/src/share/classes/sun/misc/Launcher.java /AppClassLoader
static class AppClassLoader extends URLClassLoader { public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException { … return new AppClassLoader(urls, extcl); } AppClassLoader(URL[] urls, ClassLoader parent) {…} }
为什么Ext的parent是null?
从ExtClassLoader构造函数看出其super构造函数传入的就是null,而ExtClassLoader构造函数的super对应的形参就是parent。
openjdk/jdk/src/share/classes/sun/misc/Launcher.java /ExtClassLoader
static class ExtClassLoader extends URLClassLoader { public static ExtClassLoader getExtClassLoader() throws IOException { … return new ExtClassLoader(dirs); } public ExtClassLoader(File[] dirs) throws IOException { super(getExtURLs(dirs), null, factory); … } … }
openjdk/jdk/share/classes/java/net/URLClassLoader.java
URLClassLoader(URL[] urls, ClassLoader parent, AccessControlContext acc) {…}
顺序:jvm的目的是要去加载main所在类->启动boot加载器->启动ext-加载器>启动app加载器->通过app加载器去加载main class。
所以这不是继承上的父子关系,而是加载链逻辑上的父子关系。逻辑上的父子关系目的就是为了双亲委派。
扩展类加载器
扩展类加载器的加载路径是通过向System.getProperty()传入参数”java.ext.dirs”
String[] urls = System.getProperty(“java.ext.dirs”).split(“:”); for (String url : urls) System.out.println(url); ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent(); URLClassLoader urlClassLoader = (URLClassLoader) classLoader; URL[] urls = urlClassLoader.getURLs(); for (URL url : urls) System.out.println(url);
向System.getProperty()传入参数”java.ext.dirs”的做法也可以在openjdk源码中看到:
openjdk/jdk/src/share/classes/sun/misc/Launcher.java / ExtClassLoader
public ExtClassLoader(File[] dirs) throws IOException { super(getExtURLs(dirs), null, factory); … } private static File[] getExtDirs() { String s = System.getProperty("java.ext.dirs"); … }
不同的类加载器加载同一个类,相等吗?
不相等。方法区是按照类加载器进行分开存储的。每个类加载器在方法区里都有一块独立的区域,虽然加载的是同一份文件,但是不会在同一个空间里。
同一个类加载器加载同一个文件多次,实际上会加载几次?
一次。因为加载前会(根据全限定名)去判断空间里是否已经有这个类。
双亲委派
双亲委派:需要查找某个类时,先判断在当前类加载器是否已经加载(能在其空间中找到),如果已经加载则直接返回,没有则向上委托给其父类加载器。
*系统已加载的class信息存储在SystemDictionary类中。
e.g. 查找某个类
-> 判断当前最下层的自定义的类加载器是否已加载该类,是则直接返回,否则往上委托给父类AppClassLoader;
-> 判断在AppClassLoader的空间中是否已经加载,是则直接返回,否则再往上委托给父类ExtClassLoader;
-> … 委托给BootstrapClassLoader…
-> 如果BootstrapClassLoader也没有加载直接报错
局限性:无法做到不委派或向下委派
e.g. 数据库需要实现的driver接口是由启动类加载器加载。而第三方数据(如mysql)的相关实现类需要由应用类加载器加载,启动类加载器不能加载,需要向下委派。
什么叫打破双亲委派?
两种思路。a) 不委派 –只用当前类加载器去加载 –实现方式:自定义类加载器
或 b) 向下委派 (SPI机制中的一部分)
SPI: 一种服务发现机制,通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。(类似Spring boot里的服务发现)
e.g. SPI Demo(该案例不算打破双亲委派,因为所有类都是启动类加载器加载的)
PayService是自定义的一个接口,有两个实现AlipayService和WxpayService。
Public interface PayService { void pay(); }
main中使用线程上下文加载器加载PayService类。
public static void main(String[] args) { ServiceLoader<PayService> services=ServiceLoader.load(PayService.class); for (PayService service : services) service.pay(); }
通过接口调用的pay方法调用的是哪一个实现类是看pom.xml/<dependencies>/<artifactId>中指定加载的是哪一个模块。
Pom.xml (gateway-main)
<dependencies> <dependency> … <artifactId>pay-wx</artifactId> … </dependency> … </dependencies>
每个实现类底下的/src/main/resources/META-INF.services/目录下的文件中指定了实现类的全限定名。SPI从而找到需要加载的实现类。
/pay-wx/src/main/resources/META-INF.services/com.luban.common.service.PayService
com.luban.pay.WxpayService
实现向下委派?
需要使用ServiceLoader。
e.g. Driver中的SPI机制。JDBI的底层实现用到serviceloader,也是一种SPI。driver通过向下委派来打破双亲委派。
ServiceLoader<Driver> loadedDrivers=ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator=loadedDrivers.iterator();
线程上下文类加载器
ServiceLoader的底层是由线程上下文类加载器ContextClassLoader实现的,在SPI向下委派中有应用。
ContextClassLoader可通过Thread.currentThread().setContextClassLoader()进行设置。
在checkAndLoadMain时已经进行设置,默认为AppClassLoader。
openjdk/jdk/src/share/classes/sun/misc/Launcher.java
public Launcher() { … // Now create the class loader to use to launch the application loader = AppClassLoader.getAppClassLoader(extcl); … Thread.currentThread().setContextClassLoader(loader); }
自定义类加载器
- 如何实现自定义类加载器?
- extends ClassLoader类,重写findClass方法。
实验1:自定义加载器重写findClass时返回null,能否加载成功
public class Classloader1 extends ClassLoader { public static void main(String[] args) throws Exception { Classloader1 classloader1=new Classloader1(); Class<?> clazz1=classloader1.loadClass("com.xxx.Test_1"); System.out.println("Load: "+clazz1); System.out.println("clazz1 hashcode: "+clazz1.hashCode()); System.out.println(clazz1.getClassLoader()); } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { System.out.println("----Classloader1 findClass----"); return null; } }
结果:可以加载
原因:自定义类加载器判断自己空间中没有该类,向上(双亲)委派,最后该类由AppClassLoader找到并直接返回。clazz1.getClassLoader()打印出sun.misc.Launcher$AppClassLoader证明clazz1是由AppClassLoader加载的。
实验2
public class Classloader1 extends ClassLoader { public static void main(String[] args) throws Exception { Classloader1 classloader1=new Classloader1(), classloader2=new Classloader1(); Class<?> clazz1=classloader1.loadClass("com.xxx.Test_1"); Class<?> clazz2=classloader2.loadClass("com.xxx.Test_1"); System.out.println(clazz1==clazz2); } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { ... } }
结果:true
原因:用同一个classLoader类加载同一个类,得到的Class是同一个类。可通过System.out.println(clazz1.hashCode())证明clazz1和clazz2的hashCode是一样的。
自定义类加载器如何打破双亲委派
Classloader/loadClass(String)底层了调用loadClass(String, boolean)
/OpenJdk/jdk8/jdk/src/share/classes/java/lang/ClassLoader.java
public Class<?> loadClass(String name) throws ClassNotFoundException { return this.loadClass(name, false); }
loadClass(String, Boolean)中的双亲委派逻辑:
/OpenJdk/jdk8/jdk/src/share/classes/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { //name为全限定名 … Class<?> c = this.findLoadedClass(name); //首先在自己空间(已经加载过的类)中搜索该类 if (c == null) { //如果没有找到才去委派 try { if (this.parent! = null) c = this.parent.loadClass(name, false); //双亲委派(用parent去加载) else c = this.findBootstrapClassOrNull(name); //如果没有parent-> } catch (ClassNotFoundException) {} } if (c == null) c = this.findClass(name); //如果双亲委派仍未加载到,调用自己的findClass加载一次 //此后再不能加载到将直接报错 if (resolve)//判断有无解析,进行解析 this.resolveClass(c); return c; }
private Class<?> findBootstrapClassOrNull(String name) // <- { if (!checkName(name)) return null; return findBootstrapClass(name); }
可通过重写loadClass(String, Boolean)打破双亲委派: 对指定名字包下的类的加载做不委派处理
public class Classloader1 extends ClassLoader { ... @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (this.getClassLoadingLock(name)) { Class<?> c=this.findLoadedClass(name); if (c==null) { long t0=System.nanoTime(); if (name.startsWith("com.xxx")) {//对指定名称的类不委派 c=findClass(name); } else { c=this.getParent().loadClass(name);//其它类照常向上委派 } if (c==null) { long t1=System.nanoTime(); c=this.findClass(name); //如果委派没有find到,调用自己的findClass再加载一次 PerfCounter.getParentDelegationTime().addTime(t1=t0); PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); PerfCounter.getFindClasses().increment(); } } if (resolve) { this.resolveClass(c); } return c; } } }
沙箱安全
checkAndLoadMain底层有调用到initSystemClassLoader。initClassLoader中所做的AccessController.doPrivileged判断就是一种沙箱安全机制。
Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / initSystemClassLoader
private static synchronized void initSystemClassLoader() { … scl = AccessController.doPrivileged( new SystemClassLoaderAction(scl)); … }
实验: 沙箱安全
在自创java.lang包下定义String类,在main中调用String的方法会报错。
public class String { public static void main(String[] args) { String.show(); } public static void show() { System.out.println("String show function"); } }
结果:报错“在类中找不到main方法”
原因:沙箱安全防止打破双亲委派修改系统类,保护核心类库。