JVM第二课:详解Class加载过程

类加载和初始化

class cycle

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

  1. Loading,把class文件load到内存,双亲委派(安全)
  2. Linking,分三小步:
    1. Verification,校验,文件是否符合JVM规定的class格式.
    2. Preparation,静态变量赋默认值,比如int-0,double-0.0,boolean-false
    3. Resolution,解析,将类、方法、属性等符号引用解析为直接引用
      如常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
  3. Initializing,初始化,这时候静态变量赋初始值(代码指定的值),调用静态代码块

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

在这里插入图片描述

类加载器 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.

类加载器的分层关系:
在这里插入图片描述

注意
1.这个上层加载器,即父加载器,是逻辑上的关系,其实就是一个成员变量
2.不是类的继承关系,那是另一种维度的关系
3.加载器也是一个对象,也要由另一个加载器加载,但并不一定是由他的parent加载,是谁不一定.最终都是由Bootstrap加载的
设一个加载器a的上层加载器是b,那么 a不一定是被b加载的

双亲委派

双亲委派并不是指父母双方,而是指"查找类时从子到父,加载类时从父到子"的这么一个机制.
在这里插入图片描述

具体含义

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

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

父加载器不是类加载器的加载器,也不是类加载器的父类加载器

package character02;

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());
    }
}
为什么需要双亲委派

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

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

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

类加载器的范围

ClassLoader是Launcher的内部类,具体可以去看一下Launcher的源码
在这里插入图片描述
查看一下具体路径

package character02;

public class T003_ClassLoaderScope {
    public static void main(String[] args) {
        String pathBoot = System.getProperty("sun.boot.class.path");
        System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        String pathExt = System.getProperty("java.ext.dirs");
        System.out.println(pathExt.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        String pathApp = System.getProperty("java.class.path");
        System.out.println(pathApp.replaceAll(";", System.lineSeparator()));
    }
}

从源码角度去理解ClassLoader

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

ClassLoader源码分析

别的方法ClassLoader类已经写好啦(模板设计模式),
ClassLoader的loadClass()源码

	protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 这个查找调用了native方法,具体可能要看HotSpot或者其他JVM源码了,可以理解为一个"缓存"
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                // 然后调用parent的loadClass,parent也是先检查下是否已加载
                // 然后调用parent.parent.loadClass或者findBootstrapClassOrNull
                // 这里体现了双亲委派的第一步,查找类时从子到父
                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();
                    // loader的缓存中找不到,需要去实现双亲委派的第二步,加载类时从父到子
                    c = findClass(name);

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

由源码我们可以知道,
双亲委派过程:首先会从自己的缓存中找是否加载过class对象,如果有,直接返回,没有则调用父加载器的loadClass()方法,看看父加载器是否能找到,如果一直调用到bootstrap加载器的缓存中都没找到,则从bootstrap加载器开始尝试加载该类,如果加载到则返回,否则一直回调到子类尝试加载

其中自定义ClassLoader的关键点就是下面的findClass方法,该方法直接抛异常,是个必须被子类重写的方法.(模板方法,钩子函数)
ClassLoader的findClass()源码

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

自定义一个简单的ClassLoader

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

拓展:

CompilerAPI 动态编译,直接在内存中完成编译源码和加载Class,不需要经过硬盘了

自定义ClassLoader实现:
在C盘下面有一个Hello.class文件,并且当前项目没有Hello.java,loadClass时,会先从父加载器开始找,即当父加载器不能加载该类时,就会调用自定义的findClass()方法查找是否能找到

package character02;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("C:/", name.replace(".", "/").concat(".class"));
        System.out.println(f.getName());
        FileInputStream fis = null;
        ByteArrayOutputStream bao = null;
        try {
            fis = new FileInputStream(f);
            bao = new ByteArrayOutputStream();

            int b = 0;
            while ((b = fis.read()) != -1) {
                bao.write(b);
            }
            byte[] bytes = bao.toByteArray();
            //通过这个方法把一个二进制数组转化为一个class对象
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bao != null) {
                try {
                    bao.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //找不到就调用父加载器的findClass方法
        return super.findClass(name);
    }

    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader classLoader = new MyClassLoader();
        String className = "Hello";
        Class<?> aClass = classLoader.loadClass(className);
        System.out.println(aClass.getClassLoader());
    }
}

LazyLoading

了解即可,不需深究
在这里插入图片描述

加载顺序

  1. final static修饰的变量,在类加载前就初始化好了,访问它不需要初始化类.
  2. 访问静态变量时需要加载类,先加载父类执行父类的静态代码块,再加载自己执行自己的静态代码块.
  3. 如果有非静态代码块或者构造器的内的代码,整体的顺序是:
    1. Parent static block
    2. Child static block(这时子类加载完毕,先加载父类)
    3. Patent block
    4. Patent constructor
    5. Child block
    6. Child constructor(这时子类对象创建完毕,先调用父类的代码块和构造器)

java是解释型还是编译型语言?

在这里插入图片描述

答:默认是混合模式,解释器+JIT,当某个方法调用很频繁时就走JIT
也可以指定为单纯的解释性/编译型

  • 解释:众所周知,Java是跨平台的语言,JVM在运行时讲class字节码解释为操作系统认识的本地代码去执行
  • 编译:这里编译是指,直接编译成操作系统认识的本地代码,不用JVM在运行时解释了

默认模式是混合模式,就是混合使用解释器加热点代码编译。什么叫热点代码编译?
多次被调用的方法,多次被调用的循环进行编译,怎么检测呢?就是用一个计数器,每个方法上都有一个方法,计数器循环有循环计数器。结果在发现某个方法一秒钟执行了超过某个10万次。我要对他进行编译,拿大腿想一想我该怎么办?直接编译成本地代码,再用的话直接用本地的。不用解释器执行了。

这时候会有人问,为什么不直接都编译成本地代码呢,执行效率更高,因为Java解释器现在效率已经非常高了,在一些简单的代码上它不使用编译器。第二点,如果你有一段代码执行文件,特别特别多各种各样的类库,有时候好几十个class,这是正常的。你上来二话不说先给编译器让他编译,编译的过程会长的吓人,所以现在默认的模式是混合模式,但是完全可以用参数来指定到底是什么模式。

这个东西是可以验证的,有一个小程序,可以通过指定不同的参数,看一下编译模式、解释模式、混合模式的执行速度上的差异。
差不多编译模式和混合模式都在3秒左右,解释模式耗时比较长。
默认是-Xmixed(混合模式) -Xint(纯解释)-Xcmp(纯编译)
检测热点代码:-XX:ComplieThreshold = 10000

package character02;

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;
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值