Class Loading Linking Initializing (类编译-加载-初始化)
类加载器(classloader):
①把二进制码存入内存
②创建一个class对象指向这块区域
以后使用的都是这个class对象然后指向的这块区域,这个class对象不在栈中,在metaspace(1.8之后出现的)
上图中从下往上是父加载器,但不是继承的关系,而且仅仅是语法上才是继承!(易混淆点)
这样可以取到类的classloader,如果这个返回值是null则代表是最顶级的Bootstrap加载器,因为是c++实现的所以是null
加载时目标的class先看自定义classloader是否加载了,如果没加载委托上一级app加载器去看看加载了没,如果没加载就继续向上挨个找,如果最顶层bootstrap也没加载,则从最顶层依次向下委派让子加载器加载这个class,如果这个class不是属于这个子加载器可以加载的范围则继续向下委派,如果最后都不能加载则抛出classNotFoundException,以上过程只要有一个找到了就直接返回结果。
面试:为什么类加载器要用双亲委派?
答:主要安全,比如说有一个java.lang.string的包要加载,如果没有双亲委派,直接拿来最低级classloader直接加载到内存使用,就把sun公司的String覆盖了,如果客户在输密码的时候我在包里做了个记录密码的手脚就很危险,而双亲委派发现string要加载那一直找到bootstrap发现sun有了,那直接把sun的string返回;次要是之前已经加载过了就不用了再加载了。
类加载器中有个parent成员变量是classloader指向谁谁就是parent。
上图中$后代表前面这个类的内部类
Class clazz = T005_LoadClassByHand.class.getClassLoader().loadClass("com.testJvm.jvm.c2_classloader.T002_ClassLoaderLevel");
上为加载类,T005…为当前类名,最后参数是你想要加载的包名.类名。
T005_LoadClassByHand.class.getClassLoader().getResourceAsStream("");
上为利用类加载器可以加载静态资源。
classLoader源码:
loadClass()方法(内部已经写好了双亲委派的过程)
上图类似递归调用,找不到就父类调用loadClass(本方法),最后也找不到就会调用findClass(),findClass方法源码中只有抛异常的一句话,是受保护的关键字修饰,具体的实现是子类中才能看到,这是钩子函数、模板函数的设计模式(写到一半,提供模板,具体子类实现)(面试设计模式可以引到classloader的源码中,加分项)!【有趣的题外话:历史上有两次jdk自己破坏了双亲委派,可以百度搜一下具体发生了啥】
自定义类加载器:
public class T006_MSBClassLoader extends ClassLoader {
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
File f = new File("c:/test/", name.replace(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b=fis.read()) !=0) {
baos.write(b);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();//可以写的更加严谨
return defineClass(name, bytes, 0, bytes.length);//将流转换为class对象,name:类名;bytes:内存中哪部分字节数组;off:字节数组起始位置;bytes.length:字节数组结束位置。
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}
public static void main(String[] args) throws Exception {
ClassLoader l = new T006_MSBClassLoader();
Class clazz = l.loadClass("com.testJvm.jvm.Hello");
Class clazz1 = l.loadClass("com.testJvm.jvm.Hello");
System.out.println(clazz == clazz1);
Hello h = (Hello)clazz.newInstance();//反射new一个对象
h.m();
System.out.println(l.getClass().getClassLoader());
System.out.println(l.getParent());
System.out.println(getSystemClassLoader());
}
}
defineClass在堆中
自定义classloader的过程:继承classloader,重写findClass()方法,在方法内找到要load进来的二进制的内容,load进内存,将这块内存转换为class对象(用defindClass()方法)。
compiler API 可以直接把class load到内存
懒加载,什么时候用什么时候加载。
严格来讲应该叫lazyInitializing,jvm规范没有规定何时加载。
五种情况:
new getstatic putstatic invokestatic指令,访问final变量除外
java.lang.reflect对类进行反射调用时
初始化子类的时候,父类首先初始化
虚拟机启动时,被执行的主类必须初始化
动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
三种执行方式:混合执行、编译执行、解释执行。
java是解释和编译混合模式执行的。
intepreter:解释器。
JIT:编译器,有些代码会被编译成本地代码(相当于是windows下exe的,linux中的elf),热点代码编译成本地的,下次执行直接找本地的,效率提升。
检测热点代码:-XX:CompileThreshold=10000 (也就是一万次执行是热点)
设置不同模式的参数在edit中的vm options中写入,默认混合模式。
问题:parent是如何指定的?
答:在自定义classLoader时会调用父亲默认的classLoader()方法,源码中是通过 ClassLoader.getSystemClassLoader() 获取默认的classLoader当做parent(在没有手动指定的情况下)。
问题:如何打破双亲委派?
答:重写loadClass(),而不是重写findClass(),因为loadClass()方法写好了双亲委派的过程。
tomcat中每一个webApplication中都有不同的classLoader,因为不同上下文中可能加载了同名的类的不同版本。
jsp等问什么能热加载,因为重写了loadClass(),打破了双亲委派,每次给了需要加载的类名就把已有的自定义classLoader整个干掉,重新new 一个classLoader然后加载,这样每次加载的就不一样了,若补充协议loadClass()打破双亲委派,则第二次加载会找加载过的内容直接返回。
load的类都放在metaspace方法区里,原来load的老的类,如果没有指向他的引用就会被GC回收。