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

Java从编码到执行

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

JVM调优之你应知道类加载(第一弹)_第1张图片

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

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

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

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

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

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

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

  3. 混合模式 (默认)

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

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

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

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

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

类加载的过程

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

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

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

加载(loading)

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

验证(verification)

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

准备 (preparation)

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

解析(resolution)

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

类加载器

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

JVM调优之你应知道类加载(第一弹)_第3张图片

双亲委派

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

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

如果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!");
    }
}

你可能感兴趣的:(java,jvm)