欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JVM调优之你应知道类加载(第一弹)

程序员文章站 2022-07-07 18:51:19
Java从编码到执行在理解类加载前, 我觉得我们应该要先知道为什么我们写的代码可以被执行, 看下图:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOD0euGG-1610449772015)(C:\Users\曹太白\Desktop\java从编码到执行.png)]我们写的代码编译成.class文件后, 被classloader加载到内存中, 同时也会把java自带的类库load到内存中然后会编译器进行编译, 最后通过执行引擎执行这里的编译会有三种模式:纯...

Java从编码到执行

在理解类加载前, 我觉得我们应该要先知道为什么我们写的代码可以被执行, 看下图:

JVM调优之你应知道类加载(第一弹)

我们写的代码编译成.class文件后, 被classloader加载到内存中, 同时也会把java自带的类库load到内存中

然后会编译器进行编译, 最后通过执行引擎执行

这里的编译会有三种模式:

  1. 纯解释模式 (启动快, 执行慢)

    只使用字节码解释器 (Bytecode Intepreter)一条一条的读取, 解释并执行字节码命令;

  2. 纯编译模式 (启动慢, 执行快)

    只使用 JIT(Just In-Time compiler)即时编译器把字节码编译成本地代码;

  3. 混合模式 (默认)

    起始阶段采用纯解释执行, 热点代码使用编译器编译成本地代码执行

    扩展 (怎么才会是热点代码)

    1.  多次被调用的方法 (通过方法计数器监测)
    2.  多次被调用的循环(通过循环计数器监测频率)
    

当然我们可以通过命令来指定某种模式

  1. -Xmixed (default) : 混合模式
  2. -Xint : 纯解释模式
  3. -Xcomp : 纯编译模式

类加载的过程

先上个图:
JVM调优之你应知道类加载(第一弹)

在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。

另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载(loading)

​ 就是把一个.class文件load到内存中, load进去后在内存中创建了两块内容, 一块存储了这个.class文件的二进制内容, 二是创建了这个Class的对象, 并且将这个对象指向了第一块内容

验证(verification)

​ 校验你这个.class文件内容是否合法

准备 (preparation)

​ 会在这个阶段为你的静态变量赋初始值

解析(resolution)

​ 在这个阶段将类、方法、属性等符号引用解析为直接引用(指针、偏移量等内存地址)

类加载器

类加载器分类以及加载范围

JVM调优之你应知道类加载(第一弹)

双亲委派

​ 上图中当要加载某一个类时, 自下向上 检查该类是否已经加载, 又自上而下进行实际的查找和加载, 这就是双亲委派, 继续看图
JVM调优之你应知道类加载(第一弹)

当某个类加载器接收到类加载请求时, 会先去自己的缓存中监测有没有, 没有就交给自己的父加载器, 依次类推, 直到查找到返回为止;

如果BootStrap也没查找到, 就会进行加载, 当BootStrap发现这个类不归它管时, 则交给子加载器加载, 依次类推, 直到加载到返回为止;

如果都没加载上, 就抛出ClassNotFoundException以后看见这个异常就别怕了

举个例子:

​ java.lang.String类, 我们知道这个Java自带的String位于rt.jar中, 那它的加载将经过Custom、Application、ExtensionClassloader, String这个类都不归前面三个加载器负责, 最终由Bootstrap加载并返回;

为什么要搞双亲委派

​ 最主要的原因是为了安全

假设如果没有双拼委派会怎样?

​ 我们是不是可以自定义一个java.lang.String类, 然后做一些特殊的处理, 比如说记录一下网站登录时输入的银行卡卡号与密码, 然后把这个类打包成一个jar, 然后在系统中调用, 然后会有两种结果, 1. 福布斯排行榜上会有你的名字; 2. 能吃上免费早中晚饭

其次是节约资源

这个没啥好说的, 加载一次后就不会再加载第二次了

双亲委派如何实现的

可以看ClassLoader的loadClass方法, 注释写的很清楚, 就不过多赘述

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查该类是否已经加载
            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
                }

                if (c == null) {
                    // 如果仍然找不到,请调用findClass以便找到该类。
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 这是定义类加载器;记录统计数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

如何打破双亲委派机制

​ 其实我们可以继承ClassLoader, 重写他的loadClass方法

思路:

原版有一个判断是不是已经加载过了, 而自定义的没有

判断这个类存不存在, 存在直接加载, 不存在就让父加载器去加载, 如果已经加载过了, 那就新起一个ClassLoader加载,
这也是为什么在tomcat中, 你上传了两个war包都执行的原因

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

            File f = new File("E:\\workspace\\joys\\pay\\epay-service\\src\\main\\java" + 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.joy.epay.feign.Hello");

        m = new MyLoader();
        Class clazzNew = m.loadClass("ccom.joy.epay.feign.Hello");

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

如何自定义一个类加载器

  1. 继承ClassLoader
  2. 重写findClass

这个findClass是一个模板方法, 在ClassLoader中已经有了实现

protected Class<?> findClass(String name) throws ClassNotFoundException {
	throw new ClassNotFoundException(name);
}
  1. 自定义加载器逻辑

下面是个例子, 并且验证了第二次是否会被加载

public class ClassLoaderTest extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("D:/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);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        //throws ClassNotFoundException
        return super.findClass(name); 
    }
    
    
    public static void main(String[] args) throws Exception {
        ClassLoader l = new ClassLoaderTest();
        // 加载Hello类
        Class clazz = l.loadClass("com.joy.epay.feign.Hello");

        Hello h = (Hello)clazz.newInstance();
        h.print();

        // 重复加载, 验证第二次是否还会加载
        Class clazz1 = l.loadClass("com.joy.epay.feign.Hello");
        System.out.println(clazz == clazz1);

        // 打印该类的加载器
        System.out.println(l.getClass().getClassLoader());
    }
}
public class Hello {
    void print(){
        System.out.println("hello world!");
    }
}

本文地址:https://blog.csdn.net/jilunxian0985/article/details/112544869

相关标签: java jvm