类加载和初始化

class cycle

首先一个class文件在硬盘里面
然后JVM去对它进行以下行为:

  1. Loading,把class文件load到内存,双亲委派(安全)
  2. Linking,分三小步:

    1. verification,校验文件是否复合JVM规定的class格式.<br /> preparation,静态变量赋默认值,比如int-0,double-0.0,boolean-false<br /> resolution,解析,将类、方法、属性等符号引用解析为直接引用<br /> 如常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
  3. Initializing,初始化,这时候静态变量赋初始值(代码指定的值),调用静态代码

一般我们只要记住,静态变量的初始化分为两步,Linking时是默认值,Initializing后才是初始化的值
成员变量的初始化其实也是分两步,第一步申请内存空间时是默认值,第二步调用构造方法时才是初始化的值
image.png

类加载器ClassLoader

一个class文件的Loading,load出两个东西:

  1. class文件的二进制编码加载到内存
  2. 生成一个与之对应的Class对象,改对象指向内存中的class编码文件

如果打印一下String的ClassLoader.会发现结果为null:
System.out.println(String.class.getClassLoader());
这是因为:
最顶层的加载器Bootstrap是用C++来实现的,在JAVA中没有与之对应的类.
所以Bootstrap加载出来的类,比如String,获取到的ClassLoader是null.

类加载器的分层关系:
image.png
注意
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

我们写个方法打印一下这些东西就看出来了:

  1. public static void main(String[] args) {
  2. System.out.println("boot-------------------");
  3. System.out.println(System.getProperty("sun.boot.class.path").replaceAll(":", System.lineSeparator()));
  4. System.out.println("ext-------------------");
  5. System.out.println(System.getProperty("java.ext.dirs").replaceAll(":", System.lineSeparator()));
  6. System.out.println("classpath-------------------");
  7. System.out.println(System.getProperty("java.class.path").replaceAll(":", System.lineSeparator()));
  8. }

双亲委派

是什么
双亲委派并不是指父母双方,而是指”查找类时从子到父,加载类时从父到子”的这么一个机制.

具体含义:
众所周知,ClassLoader加载完一个类后,会放入一个ClassCache,下次再用时就不需重复加载了.每个ClassLoader有自己的ClassCache

  1. 当我们需要找一个类时,会先交给最下层的ClassLoader,在ClassCache找,如果找到了就返回结果,如果找不到就交给上层加载器,上层加载器进行同样的操作,直到Bootstrap.
  2. 真正去加载这个类的时候,会自上到下开始加载.
  3. 每个ClassLoader先看自己管辖的类里面有没有需要加载的class,如果有就加载返回,如果没有就交给下一层去加载.

    1. 如果都没有就抛异常CLassNotFoundException.

    为什么

    为啥不直接放到一个ClassCache里面,这样就不用层层查找了啊?
    这里主要是出于安全考虑.
    假设黑客小明自定义了一个java.lang.String对象,里面做了些非法操作;如果不分层查找的话,用户就会用到他自定义的String,阴谋得逞;
    双亲委任机制下,使用String时,先看看父加载器是否已加载,直到找到Bootstrap后直接返回String类.

可以打破双亲委派机制吗?

可以,自定义一个classLoader,重写loadClass方法就可以打破.
热加载/热部署的时候,可能会重写loadClass(),打破双亲委派机制

从源码角度去理解ClassLoader

继承ClassLoader(这里用到了模板方法设计模式)
重写模板方法findClass,调用defineClass
自定义类加载器 加载 加密的class,防止反编译和篡改

ClassLoader简单读源码

别的方法ClassLoader类已经写好啦(模板设计模式),
关键点就是下面的findClass方法,该方法直接抛异常,是个必须被子类重写的方法.(模板方法,钩子函数)

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. // 这个查找调用了native方法,具体可能要看HotSpot或者其他JVM源码了,可以理解为一个"缓存"
  7. Class<?> c = findLoadedClass(name);
  8. if (c == null) {
  9. long t0 = System.nanoTime();
  10. // 然后调用parent的loadClass,parent也是先检查下是否已加载
  11. // 然后调用parent.parent.loadClass或者findBootstrapClassOrNull
  12. // 这里体现了双亲委派的第一步,查找类时从子到父
  13. try {
  14. if (parent != null) {
  15. c = parent.loadClass(name, false);
  16. } else {
  17. c = findBootstrapClassOrNull(name);
  18. }
  19. } catch (ClassNotFoundException e) {
  20. // ClassNotFoundException thrown if class not found
  21. // from the non-null parent class loader
  22. }
  23. if (c == null) {
  24. // If still not found, then invoke findClass in order
  25. // to find the class.
  26. long t1 = System.nanoTime();
  27. // 这里需要各ClassLoader实现,需要去实现双亲委派的第二步,加载类时从父到子
  28. c = findClass(name);
  29. // this is the defining class loader; record the stats
  30. PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  31. PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  32. PerfCounter.getFindClasses().increment();
  33. }
  34. }
  35. if (resolve) {
  36. resolveClass(c);
  37. }
  38. return c;
  39. }
  40. }

查实自定义一个简单的Class Loader

如果Load的class本项目空间已经有啦,那么就不会走自定义的findClass方法了,而是直接由Launcher$AppClassLoader加载出来了
所以我们自定义一个CLassLoader,一般是加载一个其他地方的class,比如从RPC服务中获取
远程传输class文件一般会对对class加密,拿到class文件的字节数组后再解密;
最简单的是对方发送时对一个token做异或(xor,^)运算,我们拿到后再对那个token做异或即可解密.

我这里com.example.springboot2.test.Hello是另一个项目空间的类,test.Hi是自己的项目空间的类

  1. public class MyClassLoader extends ClassLoader {
  2. @Override
  3. protected Class<?> findClass(String name) throws ClassNotFoundException {
  4. System.out.println("MyClassLoader.findClass");
  5. String parent = "/Users/liweizhi/IdeaProjects/wzdemo/springboot2/target/classes/";
  6. // String parent = "/Users/liweizhi/IdeaProjects/wzdemo/demo1/target/classes";
  7. File f = new File(parent, name.replace(".", "/").concat(".class"));
  8. FileInputStream fis = null;
  9. ByteArrayOutputStream bao = null;
  10. try {
  11. fis = new FileInputStream(f);
  12. bao = new ByteArrayOutputStream();
  13. /*int b = 0;
  14. while ((b = fis.read()) != -1) {
  15. bao.write(b);
  16. }*/
  17. byte[] buffer = new byte[1024];
  18. for (int len = 0; (len = fis.read(buffer)) != -1; ) {
  19. bao.write(buffer, 0, len);
  20. }
  21. byte[] bytes = bao.toByteArray();
  22. return defineClass(name, bytes, 0, bytes.length);
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. } finally {
  26. if (bao != null) {
  27. try {
  28. bao.close();
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. if (fis != null) {
  34. try {
  35. fis.close();
  36. } catch (IOException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }
  41. return super.findClass(name);
  42. }
  43. public static void main(String[] args) throws ClassNotFoundException {
  44. MyClassLoader classLoader = new MyClassLoader();
  45. // String className = "com.example.springboot2.test.Hello";
  46. String className = "test.Hi";
  47. Class<?> aClass = classLoader.loadClass(className);
  48. System.out.println(aClass.getClassLoader());
  49. }
  50. }

LazyLoading & 父子类的构造器加载顺序

Loading,把class文件load到内存;Initializing初始化
JVM规范并没有规定什么时候加载
但是规定了什么时候必须初始化:

  1. new getstatic putstatic invokestatic指令,访问final变量除外
  2. java.lang.reflect对类进行反射调用时
  3. 初始化子类的时候,父类首先初始化
  4. 虚拟机启动时,被执行的主类必须初始化
  5. 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化

这里涉及到一个加载顺序的问题

加载顺序的问题:

  1. final static修饰的变量,在类加载前就初始化好了,访问它不需要初始化类.
  2. 访问静态变量时需要加载类,先加载父类执行父类的静态代码块,再加载自己执行自己的静态代码块.
  3. 如果有非静态代码块或者构造器的内的代码,整体的顺序是:

    1. 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”);

    1. // 这个和打印Child.q时一样, 其实就是当第一次打印Child.q,去加载了

    // Class.forName(“com.example.demo.jvm.InitializingTest$Child”);

    1. Child child = new Child();

    }

    static class Parent {

    1. static int a = 1;
    2. static final int s = 2;
    3. static {
    4. System.out.println("Parent static block");
    5. }
    6. {
    7. System.out.println("Patent block");
    8. }
    9. public Parent() {
    10. System.out.println("Patent constructor");
    11. }

    }

    static class Child extends Parent {

    1. static int q = 3;
    2. static final int w = 4;
    3. static {
    4. System.out.println("Child static block");
    5. }
  1. {
  2. System.out.println("Child block");
  3. }
  4. public Child() {
  5. System.out.println("Child constructor");
  6. }
  7. }

}

  1. <a name="DwRvh"></a>
  2. ### java是解释型还是编译型语言?
  3. 答:默认是混合模式,解释器+JIT,当某个方法调用很频繁时就走JIT<br />也可以指定为单纯的解释性/编译型
  4. 1. 解释:众所周知,Java是跨平台的语言,JVM在运行时讲class字节码解释为操作系统认识的本地代码去执行
  5. 1. 编译:这里编译是指,直接编译成操作系统认识的本地代码,不用JVM在运行时解释了
  6. 1. 解释器: bytecode interpreter
  7. 1. JIT: Just In-Time compiler
  8. 1. 混合模式:混合使用解释器 + 热点代码编译器
  9. 5.1 其实阶段是解释执行<br /> 5.2 当一个方法或者循环指令的调用频率很高时,这部分代码由JIT进行编译,下次执行就不用解释了,提高速度
  10. 6. 可以通过JVM参指定模式:
  11. -Xmixed 混合模式,默认<br /> -Xint 解释模式,启动快,执行慢<br /> -Xcomp 编译模式,启动慢,执行快<br />搞个测试类,求证一下:
  12. ```java
  13. public class T009_WayToRun {
  14. static int count = 10_0000;
  15. public static void main(String[] args) {
  16. for (int i = 0; i < count; i++) {
  17. m();
  18. }
  19. long start = System.currentTimeMillis();
  20. for (int i = 0; i < count; i++) {
  21. m();
  22. }
  23. long end = System.currentTimeMillis();
  24. System.out.println(end - start);
  25. }
  26. public static void m() {
  27. for (long i = 0; i < count; i++) {
  28. long j = i % 3;
  29. }
  30. }
  31. }