jvm类加载机制

一、类加载过程

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

1、加载:在磁盘上查找编译后的.class文件,并通过IO写进内存。只有当某个类被使用时才会加载(懒加载),比如调用该类的main()方法或者new对象等。在加载阶段会生成一个该类的java.lang.Class对象,作为方法区各种数据(比如方法等类元信息)的访问入口

注:每一个类被加载时,其实JVM内部会生成两个Klass对象,InstanceKlass和MateInstanceKlass,前者是在堆区,主要包含一些静态属性;后者是在方法区,包含类元信息

2、验证:校验字节码文件的正确性

3、准备:给类的静态变量分配内存,并赋予默认值

4、解析:将符号引用替换为直接引用,该阶段会把一些静态方法(比如main())替换为指向数据所在内存的指针或句柄等(直接引用),这个过程称为静态链接(类加载期间完成)。而动态链接是在程序运行过程中将符号引用转换为直接引用。

符号引用可以简单理解为一个类中定义的属性和方法等的字面量,也就是方法名、属性名等这些,而直接引用就是方法或者属性对应的在内存中的地址。每个.class文件都有一个常量池,这些常量池里面存放的就是各种符号引用。常量池又分为静态常量池和运行时常量池,常量池在磁盘上存放着时就是静态常量池,将静态常量池加载进内存中(方法区)就变成了运行时常量池,程序运行的过程中可以将新的常量放入到运行时常量池中。

5、初始化:给类的静态变量初始化为指定的值,执行静态代码块。

类被加载到方法区之后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器引用、对应Class实例的引用等信息。

类加载器引用:该类指向类加载器实例的引用

对应Class实例引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中,作为访问方法区中类定义的入口和切入点,该Class实例与加载过程中提到的InstanceKlass对象是对应的。

注意:类在使用过程中是逐步加载的,Jar包或War包中的类并不是一次性全部加载,而是使用到时才加载。

二、类加载器

启动类加载器(Bootstrap Class Loader)

负责加载存放在JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等,该加载器实例是通过JVM底层的C++代码创建的,所有Java程序中如果想打印该加载器实例时,返回的是Null。

扩展类加载器(Extension Class Loader)

该类加载器是在sun.misc.Lauuncher$ExtClassLoader内部通过Java代码实现的。主要负责加载存在在JRE的lib/ext目录下的JAR包以及通过java.ext.dirs系统变量指定的路径中的JAR包。

应用程序类加载器(Application Class Loader)

该类加载器与扩展类加载器一样,是在sun.misc.Lauuncher$AppClassLoader内部通过Java代码实现,主要负责加载ClassPath路径下的所有JAR包。

ClassLoader中的loadClass方法源码如下:

    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
                }
				//父类找不到就抛异常 findClass(name);直接抛异常ClassNotFoundException异常:
                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;
        }
    }

还有一个方法我们需要注意的是defineClass(),它的作用是将字节数组转换成Class对象

三、双亲委派机制

双亲委派机制的好处

沙箱安全机制:如果开发人员编写了一个同核心类库包和类名都相同的类,通过双亲委派机制,这样的类并不会加载(加载的是对应路径下的类),可以防止核心API库被随意篡改。
避免重复加载:父加载器已经加载过的类,无序子加载器再次加载,保证被加载类的唯一性。同一个类,被不同的类加载器加载,生成的Class实例是不同的。
除了双亲委派机制外,还有全盘负责委托机制,即:

当一个ClassLoader装载一个类时,除非显示的使用另一个ClassLoader,否则该类所依赖和引用的类也都将由该ClassLoader进行装载。

机制:类加载的过程中子加载器会交给父加载器,逐级传递,父加载器找不到后才逐级向下传递创建

四、自定义类加载器

继承ClassLoader类

//自定义类加载器
public class MyClassLoader extends ClassLoader{


    //路径
    private String codePath;

    public MyClassLoader(ClassLoader parent, String codePath) {
        super(parent);
        this.codePath = codePath;
    }

    public MyClassLoader(String codePath) {
        this.codePath = codePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        BufferedInputStream bis=null;
        ByteArrayOutputStream baos=null;
        //完整的类名
        String file = codePath+name+".class";
        try {
            //初始化输入流
            bis = new BufferedInputStream(new FileInputStream(file));
            //获取输出流
            baos=new ByteArrayOutputStream();
            int len;
            byte[] data=new byte[1024];
            while ((len=bis.read(data))!=-1){
                baos.write(data,0,len);
            }
            //获取内存中的字节数组
            byte[] bytes = baos.toByteArray();
            //调用defineClass将字节数组转换成class实例
            Class<?> clazz = defineClass(null, bytes, 0, bytes.length);
            return clazz;

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}

调用 E盘放一个实体类User.class

  public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader("E:/");
        try {
            Class<?> clazz = myClassLoader.loadClass("User");
            System.out.println("是"+clazz.getClassLoader().getClass().getName()+"加载");
            Object o = clazz.newInstance();
            System.out.println(o.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

双亲委派模型的实现核心方法是loadClass()、findClass()和defineClass(),其中loadClass()是核心逻辑,findClass()将文件加载到内存成为,defineClass()是将二进制文件转换为活的class文件,自定义类加载器也是从这三个核心方法入手的,我们通过重写findClass()方法来自定义了一个类加载器,成功加载了本地的一个class文件。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张航柯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值