【深入理解JVM】1、在JVM类加载如何加载?双亲委派加载类及混合模式,代码演示【面试必备】

JVM加载顺序

 javac编译器-->字节码-->类加载器-->内存,其中类加载这步需要完成三个步骤:

1、loading(加载)

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在java堆中生成一个代表这个类的java.lang.Class对象,做为方法区这些数据的访问入口。
  4. 加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中。

2、linking(关联)

  1. verification(检查):用来检查加载进来的class是否符合解析的标准
    1. 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
    2. 元数据验证:对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范的要求
    3. 字节码验证:这个阶段主要是进行数据流和控制流的分析,是为了确保被验证的类的方法在运行时不会做出危害虚拟机安全的行为。
    4. 符号引用验证:这个阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要对类自身以外的信息进行匹配性的校验。目的是确保解析动作能够正常执行。
  2. preparation(准备):给class的静态变量赋默认值,一般基本类型为0,引用类型为null。
    1. 正式为变量分配内存并设置初始值,这些内存都将在方法区中进行分配,这里的变量仅包括类标量不包括实例变量。
  3. resolution(处理):将class类中常量池中的地址符号转换成为直接的内存地址,及可访问的内存地址(这里是没有进行真正的初始化赋值的,很多人习惯这里搞混)
    1. 符号的引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
    2. 直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接饮用是与内存布局相关的。
    3. 类或接口的解析
    4. 字段的解析
    5. 类方法解析
    6. 接口方法解析

3、initalizing(初始化):调用类构造器<clinit>方法的过程,给静态成员变量赋初始值,这里才开始真正的初始化赋值。(与2.2之间的指令重排也是造成半实例化的原因,具体可以去用插件BinEd-Binary插件去看编译的字节码文件,这也是单例模式中懒加载用volatile的主要原因)

类加载器

1.Bootstrap类加载器(启动类加载器)为顶级的classloader加载路径为<JAVA_HOME>\lib目录中jar文件/charset.jar等核心类,C++实现。
2.Extension类加载器(扩展类加载器)负责加载<JAVA_HOME>\lib\ext目录中的jar文件或者由-Djava.ext.dirs指定。
3.App类加载器(系统类加载器负责加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径,如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。
4.Custom为(自定义类加载器),由用户自己实现。

双亲委派的过程图

双亲委派机制。

在这里插入图片描述

我们先用代码去论证双亲委派的存在。(可以自行验证)

这里补充一个问题:

parent是如何指定的?

源码是用super(parent)指定的。

还有如果没有特别指定,默认是用用的AppClassLoader@18b4aac2,可以自行用代码试一下,源码我没有找到默认值。

public class T004_ParentAndChild {
    public static void main(String[] args) {
        System.out.println(T004_ParentAndChild.class.getClassLoader());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getClass().getClassLoader());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent());
        //System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent().getParent());

    }
}

打印出来是这样:
sun.misc.Launcher$AppClassLoader@18b4aac2
null
sun.misc.Launcher$ExtClassLoader@12bb4df8
null

(ClassLoader加载过程用的是 模板方法模式 设计模式)

JVM是按需动态加载,采用双亲委派机制,自底部往上检查该类是否加载(图中1>2>3的顺序),如果没有classloader加载过该类则会自上而下(4>5>6)再去查找加载class,直到找到该class并加载到内存中。

采用双亲委派机制的好处是为了安全,避免外部加载进来的类和内部classloader链中产生冲突,做恶意破坏,如果有同名的类被加载进来JVM首先会判断是否有这样一个类已经加载过,如果已经加载则不会再加载一次该类。

可以看一下ClassLoader的源码:
name参数为class的名称,实际调用的是loadClass(String name)

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

调用loadClass(String name, boolean resolve)

执行顺序如上图:

  1. 先去调用findLoadedClass(name)看是否之前有加载过相同的类,至于去哪找呢?听说先去内存里面的hashset表里面找(源代码我是没找到,找到的可以说一下)。
  2. 如果找不到再去调用parent.loadClass();这里是个迭代。直到找到。
  3. 如果实在找不到会调用findClass(name);方法抛出ClassNotFoundException(name)异常。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                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();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

为什么JVM要使用双亲委派呢?

  1. 为了安全(最主要)
    1. 如果不用双亲委派,把自定义的加载器加载java.lang.String然后打包发给客户,就会覆盖掉自带的String类库,一般我们保存密码都是用String存储。假如这时候我在自定义String类里面加一个发送邮箱的业务或者存储到我对象的数据库的业务线,是不是相当于用我的自定义的String我都会很轻松的获取密码。
  2. 为了效率

扩展:

1、类加载器加密

可以在类加载器加载的时候进行加密,代码如下:

这里用的亦或加密的。

public class T007_MSBClassLoaderWithEncription extends ClassLoader {

    public static int seed = 0B10110110;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("c:/test/", name.replace('.', '/').concat(".msbclass"));

        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) {
                baos.write(b ^ seed);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {

        encFile("com.mashibing.jvm.hello");

        ClassLoader l = new T007_MSBClassLoaderWithEncription();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());
    }

    /**
     * 用亦或加密
     * @param name
     * @throws Exception
     */
    private static void encFile(String name) throws Exception {
        File f = new File("c:/test/", name.replace('.', '/').concat(".class"));
        FileInputStream fis = new FileInputStream(f);
        FileOutputStream fos = new FileOutputStream(new File("c:/test/", name.replaceAll(".", "/").concat(".msbclass")));
        int b = 0;

        while((b = fis.read()) != -1) {
            // 亦或一个数,再亦或的话就解密了
            fos.write(b ^ seed);
        }

        fis.close();
        fos.close();
    }
}

2、类加载器什么时候开始初始化?

JVM规范并没有规定何时加载。但是严格规定了什么时候必须初始化。

类的加载不是在初始化时loading所有的类,而是在用到的时候才会去加载class。即会发生懒加载,懒加载有五种情况:

  1. new对象,获取和访问静态变量,记住访问final变量除外。

  2. java.lang.reflect对类进行反射调用时。

  3. 初始化子类的时候,父类首先初始化。

  4. 虚拟机启动时,被执行的主类必须初始化。

  5. 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化。

3、如何打破双亲委派机制?

我们从上述源码可以推断,只要重写ClassLoader的loadClass()方法,在loadClass方法中不在调用父类的loadClass(),而是直接去load自己的class,这样就可以打破双亲委派机制

4、何时打破双亲委派机制?

  1. 在JDK1.2版本之前,自定义自定义ClassLoader都必须重写loadClass()。
  2. ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定 。
  3. 热启动 。osgi tomcat都有自己的模块指定classloader(可以加载同一类库的不同版本)
    1. tomcate的热部署就是打破了双亲委派机制,修改一个class文件可以立即同步到上下文中,其实本质就是重新加载了一次该class,如果有感兴趣的可以去了解一下tomcat的实现原理里面大致应该也是重写了loadClass()方法。

重写loadClass()代码:

public class T012_ClassReloading2 {
    private static class MyLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            File f = new File("C:/work/ijprojects/JVM/out/production/JVM/" + name.replace(".", "/").concat(".class"));

            if(!f.exists()) return super.loadClass(name);

            try {

                InputStream is = new FileInputStream(f);

                byte[] b = new byte[is.available()];
                is.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                e.printStackTrace();
            }

            return super.loadClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
        MyLoader m = new MyLoader();
        Class clazz = m.loadClass("com.mashibing.jvm.Hello");

        m = new MyLoader();
        Class clazzNew = m.loadClass("com.mashibing.jvm.Hello");

        System.out.println(clazz == clazzNew);
    }
}

5、JVM的混合模式

默认情况下是一种混合模式(混合使用解释器+热点代码编译)。

java是解释执行的,class文件到内存之后,通过java的解释器-bytecode intepreter来执行,

JIT(Just In-Time Compiler):有些代码会编译成本地格式的代码来执行。

所以说java不能单纯说是解释型语言还是编译型语言。

一、什么时候会用JIT编译成本地代码呢?

    写了一段代码,刚刚开始是用解释器执行,结果发现在执行过程中有某一段代码执行的频率特别高(1s中执行几十万次),JVM就会把这段代码编译成本地代码(类似用C语言编译本地*.exe的文件),再执行该段代码时就不会用解释器解释来执行了,提升效率。

二、为什么不直接编译成本地代码,提高执行效率呢?

  1. java解释器的执行效率其实也很高了,在某些代码的执行效率上不一定输于执行本地代码。
  2. 如果执行的代码引用类库特别多,在执行启动时时间会非常长。

三、用参数改变模式:

  1. -Xmixed:默认为混合模式,启动速度较快,对热点代码实行检测和编译。
  2. -Xint:使用解释模式,启动很快,执行稍慢。
  3. -Xcomp:使用纯编译模式,执行很快,启动很慢(很多类库的时候)

代码验证:

Edit Configurations-->VM options

  • 混合模式:
    • 不修改任何参数用默认配置
    • 执行时间:2700左右
  • 解释模式:
    • 将Edit Configurations-->VM options-->-Xint
    • 执行时间:执行时间太长减一个循环的0,19000
  • 编译模式:
    • 将Edit Configurations-->VM options-->-Xcomp
    • 执行时间:2600
public class T009_WayToRun {
    public static void main(String[] args) {
        for(int i=0; i<10_0000; i++)
            m();

        long start = System.currentTimeMillis();
        for(int i=0; i<10_0000; i++) {
            m();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static void m() {
        for(long i=0; i<10_0000L; i++) {
            long j = i%3;
        }
    }
}

下一篇:【深入理解JVM】3、CPU储存器+MESI+CPU伪共享+CPU乱序问题及代码论证【面试必备】

猜你喜欢

转载自blog.csdn.net/zw764987243/article/details/109502435