Java 静态块到底在什么时候被调用

(如何确定一个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