1. java的编译

1.1 JAVA到底是编译执行还是解释执行?

java有即时编译器提供即时编译技术支持,被称为JIT(Java Just Time complier)。

默认的情况下,JAVA使用混合编译模式:即混合使用解释器+热点代码编译。
JAVA首先会把class文件以解释模式来执行,在执行过程中发现有一些代码的执行频率比较高,会使用JIT把这块代码直接编译让CPU执行。这也是为什么我们有时候会发现程序第一次执行时很慢,但是第二次执行或者过一段时间之后就会变快的原因。

热点代码检测的机制:

  1. 多次被调用的方法(方法计数器)
  2. 多次被调用的循环(循环计数器)

    1.2 JVM启动参数指定编译模式

    | 参数 | 说明 | | —- | —- | | -Xint | 纯解释模式。启动很快,执行稍慢 | | -Xmixed | 混合模式。 启动适中,执行适中。 | | -Xcomp | 纯编译模式。启动很慢,执行很快。 | | -XX:CompileThreshold=10000 | 检测热点代码的执行次数阈值,超过此阈值则会被编译成为本地代码 |

1.3 为什么不直接使用执行效率最高的编译模式?

  1. 现在的JVM执行效率已经很高了。
  2. 纯解释模式在类多的时候执行太慢, 纯编译模式在类多的时候编译太慢, 所以JVM的默认编译模式取折中的混合编译模式。

    1.4 小程序

    分别制定不同的编译模式查看执行结果

    1. public class T009_WayToRun {
    2. public static void main(String[] args) {
    3. long start = System.currentTimeMillis();
    4. m();
    5. long end = System.currentTimeMillis();
    6. System.out.println(end - start);
    7. long start1 = System.currentTimeMillis();
    8. m();
    9. long end1 = System.currentTimeMillis();
    10. System.out.println(end1 - start1);
    11. }
    12. public static void m() {
    13. for(long i=0; i<10000_0000L; i++) {
    14. long j = i%3;
    15. }
    16. }
    17. //纯解释模式结果-Xint
    18. //210444 212055
    19. //混合模式结果(-Xmixed -XX:CompileThreshold=100000)
    20. //4816 2583
    21. //纯编译模式结果 -Xcomp
    22. //2712 2598
    23. }

    2.class加载

    大概来讲class文件加载分为loadinglinkinginitializing三个过程。
    图片.png

1.1 Loading(双亲委派)

1.1.1 java加载器类别

Java的加载器类别自上而下共有4类加载器,分别为:

  1. BootstrapClassLoader(启动类加载器)
  2. ExtClassLoader (标准扩展类加载器)
  3. AppClassLoader(系统类加载器)
  4. CustomClassLoader(用户自定义类加载器) | BootstrapClassLoader | c++编写,加载java核心库 java.*。比如String。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作 | | :—- | —- | | ExtClassLoader | java编写,加载扩展库 jre/lib/ext/*.jar | | AppClassLoader | 加载classpath下的内容, 我们一般自己写的代码都由AppClassLoader加载 | | CustomClassLoader | java编写,用户自定义的类加载器,可加载指定路径的class文件 |

1.1.2 双亲委派

java加载类的机制为双亲委派机制。大概流程如下
点击查看【processon】

由于加载器从低级到高级分别为BootstrapClassLoader、ExtClassLoader、AppClassLoader、CustomClassLoader。而且每个加载器所负责加载的类也不一样。因此不同类型的类需要每个加载器去加载。过程如下:

  1. 一个新的class请求被加载
  2. 首先由CustomClassLoader加载, CustomClassLoader从自己缓存中找,找到则返回,找不到则委托自己的父级AppClassLoader去加载。
  3. AppClassLoader先从缓存中找,找到则返回,找到则返回,找不到则委托自己的父级ExtClassLoader去加载。
  4. ExtClassLoader先从缓存中找,找到则返回,找到则返回,找不到则委托自己的父级BootstrapClassLoader去加载。
  5. BootstrapClassLoader先从缓存中找,找到则返回,找到则返回,找不到则验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则委托自己的下一级ExtClassLoader去加载。
  6. ExtClassLoader验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则委托自己的下一级AppClassLoader去加载。
  7. AppClassLoader验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则委托自己的下一级CustomClassLoader去加载。
  8. CustomClassLoader验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则抛出NotClassFoundException。

需要注意的是: 这4个加载器并不存在继承关系。 也就是说:这里面说的子类加载器并不是从父加载器派生的。据个人猜测可能是源码里面有一个类似属性叫做 parent之类的吧。

  1. public class TestClassLoader {
  2. public static void main(String[] args) {
  3. System.out.println(TestClassLoader.class.getClassLoader());
  4. System.out.println(TestClassLoader.class.getClassLoader().getParent());
  5. System.out.println(TestClassLoader.class.getClassLoader().getParent().getParent());
  6. }
  7. }

输出结果:

sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@4554617c null

一般我们写程序都是放在classpath下面的, 所以我们的TestClassLoader类会被AppClassLoader加载。 多以第一个打印的是AppClassLoader, 第二个打印的是AppClassLoader的parent, 所以是ExtClassLoader。 第三个打印的是AppClassLoader的parent的parent,所以应该是BootstrapClassLoader,但是由于BootstrapClassLoader是C++实现的并非JAVA实现,所以结果是null。

从输出结果看, AppClassLoader和ExtClassLoader和BootstrapClassLoader都属于是sun.misc.Launcher类里面的。 我们打开sun.misc.Launcher看源码会发现这个:
image.png
image.pngimage.png

1.1.3 双亲委派机制的作用

  1. 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
  2. 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。想象一下,如果没有双亲委派机制, 自己写一个String.class 然后打成Jar包给友商用, 友商引入自己写的String.class之后会吧他们自己机器的String.class替换掉,这样太危险。

    1.1.4 自定义类加载器

    自定义类加载器需要继承ClassLoader类并重写findClass方法:
    先来一个待加载的Person类: ```java package com.wangfan.exercise;

public class Person { public void sayHello(){ System.out.println(“hello!”); } }

  1. 此时Person类里面只有一个sayHello方法。把这个类编译成class文件, Person.class, 存放路径为:
  2. > /Users/wangfan/Desktop/com/wangfan/exercise/Person.class
  3. 自己写一个加载器并测试Person类:
  4. ```java
  5. package com.wangfan.exercise;
  6. import java.io.ByteArrayOutputStream;
  7. import java.io.File;
  8. import java.io.FileInputStream;
  9. import java.io.InputStream;
  10. import java.lang.reflect.InvocationTargetException;
  11. import java.lang.reflect.Method;
  12. /**
  13. * @Author:壹心科技BCF项目组 wangfan
  14. * @Date:Created in 2020/10/28 12:19
  15. * @Project:epec
  16. * @Description:TODO
  17. * @Modified By:wangfan
  18. * @Version: V1.0
  19. */
  20. public class CustomClassLoader extends ClassLoader {
  21. public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
  22. ClassLoader customClassLoader = new CustomClassLoader();
  23. Class<?> clazz = customClassLoader.loadClass("com.wangfan.exercise.Person");
  24. Object instance = clazz.newInstance();
  25. Method sayHello = clazz.getMethod("sayHello");
  26. sayHello.invoke(instance);
  27. }
  28. @Override
  29. protected Class<?> findClass(String name) throws ClassNotFoundException {
  30. byte[] data = null;
  31. InputStream is = null;
  32. ByteArrayOutputStream baos = null;
  33. try {
  34. String filePath = "/Users/wangfan/Desktop/"+name.replace(".","/")+".class";
  35. is = new FileInputStream(new File(filePath));
  36. System.out.println(filePath);
  37. baos = new ByteArrayOutputStream();
  38. int ch = 0;
  39. while (-1 != (ch = is.read())) {
  40. baos.write(ch);
  41. }
  42. data = baos.toByteArray();
  43. } catch (Exception e) {
  44. e.printStackTrace();
  45. } finally {
  46. try {
  47. is.close();
  48. baos.close();
  49. } catch (Exception e) {
  50. e.printStackTrace();
  51. }
  52. }
  53. return this.defineClass(name, data, 0, data.length);
  54. }
  55. }

需要注意的是:

  1. 如果想要自己的Person.class文件生效,就不能把Person.class放在项目路径下面, 因为双亲委派机制的AppClassLoader会加载Person.class。
  2. 因为JVM规范的要求。Person.class文件要放在它声明import下面。也就是“com/wangfan/exercise” 路径下面。

使用场景:

  1. class文件加密,防止反编译。

有了自定义的class加载器,我们可以使用密文类型的class文件, 即将java代码便编译成为class文件后再把文件内容进行一次加密,然后我们再该类的自定义加载器里面把class文件解密后再definedClass, 这样可以更安全。保证源码不被反编译。

  1. 框架反射+代理。

    1.2 linking

    2.1.1 verification

    校验阶段:校验.class是否符合JVM标准。

    2.1.2 preparation

    准备阶段: 主要是给静态变量赋值默认值

    2.1.3resolution

    将类、方法、符号引用解析为直接引用。
    常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用。

    1.3. initializing

    调用类初始化代码, 给静态成员变量赋值初始值。

1.4 JAVA的懒加载与JVM加载规范。

JVM并没有规定java何时被加载,但是严格规定了类什么时候必须初始化。 因此很多JVM实现(比如Hostop)使用懒加载机制加载JAVA类。

JVM和初始化类的规范:

  1. 执行new、 getstatic、pustatic、invokestatic指令时必须初始化类,访问final变量除外。
  2. java.lang.reflec对类进行反射时必须初始化。
  3. 初始化子类时父类必须先逐级向上初始化。
  4. 虚拟机启动时,被执行的主类必须先初始化。
  5. 动态语言支持java.lang.invoke.MethodHandle的解析结果为:REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄时,该类必须初始化。
  1. public class LazyLoading { //严格讲应该叫lazy initialzing,因为java虚拟机规范并没有严格规定什么时候必须loading,但严格规定了什么时候initialzing
  2. public static class P {
  3. final static int i = 8;
  4. static int j = 9;
  5. static {
  6. //在类被加载后会自动执行linking和initicializing,而在initicializing阶段的会执行类的静态块, 所以可以认为执行只要类P被加载就一定会打印“P”
  7. System.out.print("P");
  8. }
  9. }
  10. public static class X extends P {
  11. static {
  12. System.out.print("X");
  13. }
  14. }
  15. public static void main(String[] args) throws Exception {
  16. //P p; //没有任何结果, 因为只是声明了一下, 没有访问任何p的变量或调用p的方法,因此虚拟机不会加载P
  17. //X x = new X(); //结果:PX 因此使用new关键字, 所以需要先加载X的父类P
  18. //System.out.println(P.i); //结果:8 因为使用了p的final属性。所以类P没有被初始化
  19. //System.out.println(P.j); //结果:P9 访问了P的飞final的静态属性, 所以类P会被初始化。
  20. //Class.forName("com.mashibing.jvm.c2_classloader.T008_LazyLoading$P");//结果:P 使用烦着一定会被初始化。
  21. }
  22. }

3. 一道面试题

下面的代码会输出什么?

  1. public class A {
  2. public static void main(String[] args) {
  3. System.out.println(T.I);
  4. }
  5. }
  6. class T {
  7. public static int I = 2;
  8. public static T t = new T();
  9. T(){
  10. I++;
  11. }
  12. }

结果: 3

下面的代码又会输出什么?

  1. public class A {
  2. public static void main(String[] args) {
  3. System.out.println(T.I);
  4. }
  5. }
  6. class T {
  7. public static T t = new T();
  8. public static int I = 2;
  9. T(){
  10. I++;
  11. }
  12. }

结果: 2

为什么T内部两行代码换一换位置输出的结果就不一样的? 构造器里面的函数到底什么时候执行?

原因:
从类的加载过程分析。
loading -> verification -> preparation -> solution -> initializing.

第一种情况:

  1. loading: 将class文件载入内存。
  2. verification: 检查。
  3. preparation:静态变量赋值默认值 因此,到此时:I = 0,t = null
  4. solution:解析。
  5. initializing:静态变量赋值初始值并调用构造方法,因此先I = 2I++
  6. T.I = 3

第二种情况:

  1. loading: 将class文件载入内存。
  2. verification: 检查。
  3. preparation:静态变量赋值默认值 因此,到此时:t = null, I = 0
  4. solution:解析。
  5. initializing:静态变量赋值初始值并调用构造方法,因此先执行t = new T(),会调用T的构造方法。因此此时 I = 1。再执行I的赋值初始值: I = 2
  6. T.I = 2

由此可见, 构造器里面的代码在类初始化时执行。

4. 又一道面试题

下面的DCL单例模式:

  1. //DCL单例(Double Check Lock)
  2. public class DCLSingleton {
  3. private static volatile DCLSingleton INSTANCE = null;//#1
  4. private int i;
  5. private DCLSingleton(){
  6. i = 10;
  7. }
  8. public DCLSingleton getInstance(){
  9. if (INSTANCE == null) { //#2
  10. synchronized (DCLSingleton.class){ //#3
  11. if (INSTANCE == null){ //#4
  12. INSTANCE = new DCLSingleton();//#5
  13. }
  14. }
  15. }
  16. return INSTANCE;
  17. }
  18. }

请问? 上面的INSTANCE 私有静态变量需不需要加volatile?
答案是需要加的。 为什么呢?
主要是因为CPU发生了指令重排序。 CPU的指令重排序
我们都知道:之所以双检锁单例只要是为了规避懒加载式单例时,有多个不同的线程以极快的速度同时进入 #1 内,造成生产多个实例的问题。
举个例子来详细说一下, 假设线程A先进来经过#1->#2->#3->#4 最终达到 #5 然后开始一些列的类加载与初始化的过程: loading -> verificition -> preparation -> resolution -> initializing。 当进行到preparation就已经分配好了对象的内存空间并赋值了默认值。

接下来initializing要做的就是2步:

  1. 调用调用类的初始化代码进行初始化。
  2. 把当前内存空间的指针指向INSTENCE

很不幸的是, 这个时候CPU发生了指令重排序。所以结果变成了这样:

  1. 把当前内存空间的指针指向INSTENCE
  2. 调用调用类的初始化代码进行初始化。

这个时候假如赶得特别巧,当线程A刚刚执行到第1步, 有一个线程B来了, 进入到了#2这个时候 INSTENCE 就已经不是null了。所以会直接把INSTENCE 返回给其他对象使用。 但是这个时候DCLSingleton由于还没有执行构造器代码,所有里面的变量i是默认值0。这样线程B就拿着0这个值去做运算了。 当然会出错。当然这种情况的出现的概率极低。

我们来总结一下如果出现这种情况需要满足什么条件?

  1. 线程A在initicalizing时恰好发生了指令重排序
  2. 线程A在进行initicalizing时恰好有一另一个线程B进入#2
  3. 线程B进入把半初始化的DCLSingleton.class 返回并使用里面为初始化的变量i进行计算。