(如何确定一个class是否被classloader加载)
今天在项目的老代码里debug , 发现有一个获取ESB对象的初始化动作写在了静态块里
结果debug一直到使用这个类的时候这段代码才被执行到
这些年一直用spring, 主观上认为单例模式跟静态块在缓存这个层面上差不多,也没有深究过
一直觉得静态块跟spring的单例对象差不多, tomcat容器在启动时就应该被调用, 实际上并非如此
class只有在第一次被调用时才会被classloader加载, 当class被加载后static块才会被调用
为了证实这种猜测,我们使用一个简单例子做验证
我尝试使用jconsole查看classloader, 跟本地jvm加上
-Djava.rmi.server.hostname=10.10.8.57
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8011
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
然后发现不管是oracle的飞行器还是jconsole都不能提供相关信息
然后想到看能不能用反射直接调用一下classloader的相关方法
创建两个类
package testgc;
import java.util.Scanner;
public class TestStatic {
public static void main(String[] args) throws Exception {
Scanner sc=new Scanner(System.in);
String command = sc.nextLine();
System.out.println("worker has been loaded :" + isLoaded());
Worker.say();
Scanner sc2=new Scanner(System.in);
sc2.nextLine();
System.out.println("worker has been loaded :" + isLoaded());
}
private static boolean isLoaded() throws Exception{
java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[] { String.class });
ClassLoader cl = ClassLoader.getSystemClassLoader();
Object test = m.invoke(cl, "testgc.Worker");
if(test==null){
return false;
}else{
return true;
}
}
}
package testgc;
public class Worker {
static{
System.out.println("static block called");
}
public static void say(){
System.out.println("hello world");
}
}
然后发现了如下错误, 敢情protected方法不能直接这么调用
Exception in thread "main" java.lang.IllegalAccessException: Class testgc.TestStatic can not access a member of class java.lang.ClassLoader with modifiers "protected final"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Method.invoke(Method.java:490)
at testgc.TestStatic.isLoaded(TestStatic.java:21)
at testgc.TestStatic.main(TestStatic.java:11)
幸亏java.lang.reflect.Method 还提供了一个”硬刷”的方法 setAccessible(true), 什么private, protected统统搞定
修改之后代码如下
package testgc;
import java.util.Scanner;
public class TestStatic {
public static void main(String[] args) throws Exception {
Scanner sc=new Scanner(System.in);
String command = sc.nextLine();
System.out.println("worker has been loaded :" + isLoaded());
Worker.say();
Scanner sc2=new Scanner(System.in);
sc2.nextLine();
System.out.println("worker has been loaded :" + isLoaded());
}
private static boolean isLoaded() throws Exception{
java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[] { String.class });
m.setAccessible(true);
ClassLoader cl = ClassLoader.getSystemClassLoader();
Object test = m.invoke(cl, "testgc.Worker");
if(test==null){
return false;
}else{
return true;
}
}
}
这里的scanner没什么用,只是加一个阻塞方便查看结果,下面的1和2是我的输入
1
worker has been loaded :false
static block called
hello world
2
worker has been loaded :true
结论:
上面的例子证明如下两点
1. class只有在第一次被调用时才会被虚拟机加载
2. static block 只有在类被加载进classloader时才会被调用 (优先于你调用的方法,而且只能被调用一次)
偶然看到一哥们写认为static 块是在 newInstance 时才会被调用, 而不是类被装载时调用的,想想自己的例子里刚才用的好像都是静态方法,也没有newInstance啊, 好奇也一起试一下吧
修改代码为
package testgc;
import java.util.Scanner;
public class TestStatic {
public static void main(String[] args) throws Exception {
Scanner sc=new Scanner(System.in);
String command = sc.nextLine();
Class.forName("testgc.Worker");
Scanner sc2=new Scanner(System.in);
sc2.nextLine();
System.out.println("worker has been loaded :" + isLoaded());
Worker.say();
Scanner sc3=new Scanner(System.in);
sc3.nextLine();
Worker.say();
System.out.println("worker has been loaded :" + isLoaded());
}
private static boolean isLoaded() throws Exception{
java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[] { String.class });
m.setAccessible(true);
ClassLoader cl = ClassLoader.getSystemClassLoader();
Object test = m.invoke(cl, "testgc.Worker");
if(test==null){
return false;
}else{
return true;
}
}
}
运行结果是
1
static block called
2
worker has been loaded :true
hello world
3
hello world
worker has been loaded :true
可见只要装载, 静态块就会被调用, 不用创建对象 ( 静态类哪来创建对象 )
幸亏我幼儿园的时候小马过河学的好, 要不读书少真容易被误导 =。=
如果你再尝试一种极端情况, 把Worker的静态方法改成非静态的, 然后使用new Worker().say() 这种方式每次创建一个新对象调用say
在第一次Worker类被加载之后, 通过飞行器调用一次GC, 你会发现在下一次的say()方法被调用时,静态块没有被调用第二次, 我怀疑是类的元信息并没有被回收的关系
这里顺便多说两句
java默认提供3种classloader
1. Bootstrp loader
加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类
2. ExtClassLoader
加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库
3. AppClassLoader
加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器
getSystemClassLoader 返回的就是这个加载器
jdk2.0之后classloader就开始采用一种称为父类委托的机制来加载类
当一个classloader需要去加载类com.test.A时, 它首先会在自己的加载缓存中查找这个类,如果发现就直接返回,如果没有发现并不会直接加载,而是让自己的父类加载器去加载
等一下, 父类加载器会立即加载这个类么, 当然不是, 父类加载器也会采用同样的策略, 在自己的缓存中查找,如果没有找到, 就委托给自己的父类, 一直到Bootstrp loader
如果一直到Bootstrp loader 这一层仍然找不到这个类, classloader才会自己加载它
这种模式非常方便的定义了同包同名的class要如何被加载进jvm
假设有一个com.test.A.class文件同时被放进了 %JAVA_HOME%/jre/lib , %JAVA_HOME%/jre/lib/ext 和 classpath , 那么最终加载它的将会是Bootstrp loader
AppClassLoader 在缓存中找不到它, 会委托给ExtClassLoader , ExtClassLoader 在缓存中也找不到它会委托给 Bootstrp loader , Bootstrp loader 不会再委托了, 它找到了class A, 直接加载它
如果com.test.A.class 只在%JAVA_HOME%/jre/lib/ext 和 classpath 中存在, 加载它的将会是 ExtClassLoader
AppClassLoader 在缓存中找不到它, 会委托给ExtClassLoader , ExtClassLoader 在缓存中也找不到它会委托给 Bootstrp loader , Bootstrp loader 在缓存和路径下都找不到它, ExtClassLoader 就会将%JAVA_HOME%/jre/lib/ext 下的字节码文件加载进来
依次类推, 这就解释了为什么你大多数的自定义class是由AppClassLoader加载的,因为它们只存在于classpath中,上面两个父加载器都找不到它们, 所以AppClassLoader只好自己动手了
关于查看类是否被加载其实还可以通过参数 -verbose:class
会得到如下信息
1
[Loaded testgc.Worker from file:/C:/DEV/workspace_study/testgc/target/classes/]
static block called
2
[Loaded java.lang.ClassFormatError from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar]
[Loaded java.lang.AssertionStatusDirectives from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar]
worker has been loaded :true
hello world
3
hello world
worker has been loaded :true