类加载和初始化
class cycle
首先一个class文件在硬盘里面
然后JVM去对它进行以下行为:
- Loading,把class文件load到内存,双亲委派(安全)
 Linking,分三小步:
verification,校验文件是否复合JVM规定的class格式.<br /> preparation,静态变量赋默认值,比如int-0,double-0.0,boolean-false<br /> resolution,解析,将类、方法、属性等符号引用解析为直接引用<br /> 如常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
Initializing,初始化,这时候静态变量赋初始值(代码指定的值),调用静态代码
一般我们只要记住,静态变量的初始化分为两步,Linking时是默认值,Initializing后才是初始化的值
成员变量的初始化其实也是分两步,第一步申请内存空间时是默认值,第二步调用构造方法时才是初始化的值
类加载器ClassLoader
一个class文件的Loading,load出两个东西:
- class文件的二进制编码加载到内存
 - 生成一个与之对应的Class对象,改对象指向内存中的class编码文件
 
如果打印一下String的ClassLoader.会发现结果为null:
System.out.println(String.class.getClassLoader());
这是因为:
最顶层的加载器Bootstrap是用C++来实现的,在JAVA中没有与之对应的类.
所以Bootstrap加载出来的类,比如String,获取到的ClassLoader是null.
类加载器的分层关系:
注意
1.这个上层加载器,即父加载器,是逻辑上的关系,其实就是一个成员变量
2.不是类的继承关系,那是另一种维度的关系
3.加载器也是一个对象,也要由另一个加载器加载,但并不一定是由他的parent加载,是谁不一定.最终都是由Bootstrap加载的
设一个加载器a的上层加载器是b,那么 a不一定是被b加载的
求证一下BootStrap,Ext,App都加载哪些类
其实AppClassLoader和ExtClassLoader都是sun.misc.Launcher的内部类
(JDK11把Launcher换成了ClassLoaders)
Launcher里面可以看到这三个内部类,和他们各自加载的path
BootClassPathHolder:System.getProperty(“sun.boot.class.path”)
AppClassLoader:java.class.path
ExtClassLoader:java.ext.dirs
我们写个方法打印一下这些东西就看出来了:
public static void main(String[] args) {System.out.println("boot-------------------");System.out.println(System.getProperty("sun.boot.class.path").replaceAll(":", System.lineSeparator()));System.out.println("ext-------------------");System.out.println(System.getProperty("java.ext.dirs").replaceAll(":", System.lineSeparator()));System.out.println("classpath-------------------");System.out.println(System.getProperty("java.class.path").replaceAll(":", System.lineSeparator()));}
双亲委派
是什么
双亲委派并不是指父母双方,而是指”查找类时从子到父,加载类时从父到子”的这么一个机制.
具体含义:
众所周知,ClassLoader加载完一个类后,会放入一个ClassCache,下次再用时就不需重复加载了.每个ClassLoader有自己的ClassCache
- 当我们需要找一个类时,会先交给最下层的ClassLoader,在ClassCache找,如果找到了就返回结果,如果找不到就交给上层加载器,上层加载器进行同样的操作,直到Bootstrap.
 - 真正去加载这个类的时候,会自上到下开始加载.
 每个ClassLoader先看自己管辖的类里面有没有需要加载的class,如果有就加载返回,如果没有就交给下一层去加载.
如果都没有就抛异常CLassNotFoundException.
为什么
为啥不直接放到一个ClassCache里面,这样就不用层层查找了啊?
这里主要是出于安全考虑.
假设黑客小明自定义了一个java.lang.String对象,里面做了些非法操作;如果不分层查找的话,用户就会用到他自定义的String,阴谋得逞;
双亲委任机制下,使用String时,先看看父加载器是否已加载,直到找到Bootstrap后直接返回String类.
可以打破双亲委派机制吗?
可以,自定义一个classLoader,重写loadClass方法就可以打破.
热加载/热部署的时候,可能会重写loadClass(),打破双亲委派机制
从源码角度去理解ClassLoader
继承ClassLoader(这里用到了模板方法设计模式)
重写模板方法findClass,调用defineClass
自定义类加载器 加载 加密的class,防止反编译和篡改
ClassLoader简单读源码
别的方法ClassLoader类已经写好啦(模板设计模式),
关键点就是下面的findClass方法,该方法直接抛异常,是个必须被子类重写的方法.(模板方法,钩子函数)
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded// 这个查找调用了native方法,具体可能要看HotSpot或者其他JVM源码了,可以理解为一个"缓存"Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();// 然后调用parent的loadClass,parent也是先检查下是否已加载// 然后调用parent.parent.loadClass或者findBootstrapClassOrNull// 这里体现了双亲委派的第一步,查找类时从子到父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) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();// 这里需要各ClassLoader实现,需要去实现双亲委派的第二步,加载类时从父到子c = findClass(name);// this is the defining class loader; record the statsPerfCounter.getParentDelegationTime().addTime(t1 - t0);PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
查实自定义一个简单的Class Loader
如果Load的class本项目空间已经有啦,那么就不会走自定义的findClass方法了,而是直接由Launcher$AppClassLoader加载出来了
所以我们自定义一个CLassLoader,一般是加载一个其他地方的class,比如从RPC服务中获取
远程传输class文件一般会对对class加密,拿到class文件的字节数组后再解密;
最简单的是对方发送时对一个token做异或(xor,^)运算,我们拿到后再对那个token做异或即可解密.
我这里com.example.springboot2.test.Hello是另一个项目空间的类,test.Hi是自己的项目空间的类
public class MyClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {System.out.println("MyClassLoader.findClass");String parent = "/Users/liweizhi/IdeaProjects/wzdemo/springboot2/target/classes/";// String parent = "/Users/liweizhi/IdeaProjects/wzdemo/demo1/target/classes";File f = new File(parent, name.replace(".", "/").concat(".class"));FileInputStream fis = null;ByteArrayOutputStream bao = null;try {fis = new FileInputStream(f);bao = new ByteArrayOutputStream();/*int b = 0;while ((b = fis.read()) != -1) {bao.write(b);}*/byte[] buffer = new byte[1024];for (int len = 0; (len = fis.read(buffer)) != -1; ) {bao.write(buffer, 0, len);}byte[] bytes = bao.toByteArray();return defineClass(name, bytes, 0, bytes.length);} catch (Exception e) {e.printStackTrace();} finally {if (bao != null) {try {bao.close();} catch (IOException e) {e.printStackTrace();}}if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}return super.findClass(name);}public static void main(String[] args) throws ClassNotFoundException {MyClassLoader classLoader = new MyClassLoader();// String className = "com.example.springboot2.test.Hello";String className = "test.Hi";Class<?> aClass = classLoader.loadClass(className);System.out.println(aClass.getClassLoader());}}
LazyLoading & 父子类的构造器加载顺序
Loading,把class文件load到内存;Initializing初始化
JVM规范并没有规定什么时候加载
但是规定了什么时候必须初始化:
- new getstatic putstatic invokestatic指令,访问final变量除外
 - java.lang.reflect对类进行反射调用时
 - 初始化子类的时候,父类首先初始化
 - 虚拟机启动时,被执行的主类必须初始化
 - 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
 
这里涉及到一个加载顺序的问题
加载顺序的问题:
- final static修饰的变量,在类加载前就初始化好了,访问它不需要初始化类.
 - 访问静态变量时需要加载类,先加载父类执行父类的静态代码块,再加载自己执行自己的静态代码块.
 如果有非静态代码块或者构造器的内的代码,整体的顺序是:
3.1 Parent static block<br /> 3.2Child static block(这时子类加载完毕,先加载父类)<br /> 3.3Patent block<br /> 3.4 Patent constructor<br /> 3.5 Child block<br /> 3.6 Child constructor(这时子类对象创建完毕,先调用父类的代码块和构造器)
```java public class InitializingTest { public static void main(String[] args) throws ClassNotFoundException { // System.out.println(Parent.a); // 先加载Parent,然后打印a // System.out.println(Parent.s); // System.out.println(Child.q); // 先加载Parent, 后加载Child, 然后打印q // System.out.println(Child.w); // Class.forName(“com.example.demo.jvm.InitializingTest$Parent”);
// 这个和打印Child.q时一样, 其实就是当第一次打印Child.q,去加载了
// Class.forName(“com.example.demo.jvm.InitializingTest$Child”);
Child child = new Child();
}
static class Parent {
static int a = 1;static final int s = 2;static {System.out.println("Parent static block");}{System.out.println("Patent block");}public Parent() {System.out.println("Patent constructor");}
}
static class Child extends Parent {
static int q = 3;static final int w = 4;static {System.out.println("Child static block");}
{System.out.println("Child block");}public Child() {System.out.println("Child constructor");}}
}
<a name="DwRvh"></a>### java是解释型还是编译型语言?答:默认是混合模式,解释器+JIT,当某个方法调用很频繁时就走JIT<br />也可以指定为单纯的解释性/编译型1. 解释:众所周知,Java是跨平台的语言,JVM在运行时讲class字节码解释为操作系统认识的本地代码去执行1. 编译:这里编译是指,直接编译成操作系统认识的本地代码,不用JVM在运行时解释了1. 解释器: bytecode interpreter1. JIT: Just In-Time compiler1. 混合模式:混合使用解释器 + 热点代码编译器5.1 其实阶段是解释执行<br /> 5.2 当一个方法或者循环指令的调用频率很高时,这部分代码由JIT进行编译,下次执行就不用解释了,提高速度6. 可以通过JVM参指定模式:-Xmixed 混合模式,默认<br /> -Xint 解释模式,启动快,执行慢<br /> -Xcomp 编译模式,启动慢,执行快<br />搞个测试类,求证一下:```javapublic class T009_WayToRun {static int count = 10_0000;public static void main(String[] args) {for (int i = 0; i < count; i++) {m();}long start = System.currentTimeMillis();for (int i = 0; i < count; i++) {m();}long end = System.currentTimeMillis();System.out.println(end - start);}public static void m() {for (long i = 0; i < count; i++) {long j = i % 3;}}}
