JVM学习笔记(二)jvm类加载机制

类加载过程

  虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。
这里写图片描述
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

加载

  • 获得类的二进制字节流

  • 将这个字节流所代表的静态存储结构转为方法区的运行时数据结构

  • 在Java堆中生成对应的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证

验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

1.文件格式验证

  • 是否以魔术0xCAFEBABE开头

  • 主、次版本号是否在当前虚拟机处理范围之内

2.元数据验证

保证起字节码描述的信息符合java语言规范要求。

  • 是否有父类

  • 是否继承了不允许被继承的类(被final修饰的)

  • 非抽象类实现了所有的抽象方法

3.字节码验证

  • 栈数据类型和操作码数据参数吻合(栈里放的是int类型,使用时却按long类型来加载入本地变量表)

  • 跳转指令跳转到合理的位置

  • 方法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系的、完全不想干的一个数据类型。

4.符号引用验证
 确保解析动作能正常执行

  • 常量池中描述类是否存在

  • 访问的方法和字段是否存在且有足够的权限(private,protected,public,default)

准备

为类变量分配内存(方法区中分 配),并为类变量设置初始值。

  • public static int value = 1;

  • 在准备阶段中, v会被设置为0

  • 在初始化的< clinit >()中才会被设置为1

  • 对于static final类型,在准备阶段就会被赋上正确的值

  • public static final int value = 1

解析

将常量池中的符号引用替换为直接引用

  • 符号引用:字符串,引用对象不一定被加载
  • 直接引用:指针或者地址偏移量,引用对象一定在内存

初始化

  • 执行类构造器< clinit >方法由下面两部分合成:
      – static变量的赋值语句
      – static{}块
//静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中只能赋值不能访问
public class Test{
    static{
        i = 0;  //给变量赋值可以正常编译通过
        System.out.println(i);  //提示“非法向前引用”
    }
    static int i = 1;
}
  • 子类的< clinit >调用前保证父类的< clinit >方法已经执行完毕
     – 虚拟机中第一个被执行< clinit >方法的类是java.lang.Object
  • 一个类的< clinit >方法是线程安全的

类加载器

  • ClassLoader是一个抽象类

  • ClassLoader的实例将读入Java字节码将类装载到JVM中

  • ClassLoader可以定制,满足不同的字节码流获取方式

  • ClassLoader负责类装载过程中的加载阶段

package com.kelly.classloader;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {

            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(filename);
                    if (is == null)
                        return super.loadClass(name);
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = myLoader.loadClass("com.kelly.classloader.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.kelly.classloader.ClassLoaderTest);
    }
}

上面代码输出结果;

class com.kelly.classloader.ClassLoaderTest
false

因为虚拟机中存在两个ClassLoaderTest类,一个由系统应用程序类加载器加载的,另外一个是我们自定义的类加载器加载的。

双亲委派模型

类加载器分类

启动类加载器(Bootstrap ClassLoader)

  这个类加载器负责将< JAVA_HOME >\lib目录中的,或者被 -Xbootclasspath参数所指定的路径中,并且被虚拟机所识别的类库加载到虚拟机内存中。开发者不能直接使用启动类加载器。

扩展类加载器(Extension ClassLoader)

  负责加载< JAVA_HOME >\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用类加载器。

应用程序类加载器

负责加载用户路径上(ClassPath)上所指定的类库。默认的类加载器。

这里写图片描述
             图2. 类加载器双亲委派模型

双亲委派模型的代码都集中在java.lang.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);

                    // 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;
        }
    }

破坏双亲委派模型

  • 优点:对于通用类,各个类加载器去加载都会是同一个类。(比如Object类)。

  • 缺点:顶层的ClassLoader,无法加载底层ClassLoader的类。

  • 思考一个问题:java框架(rt.jar类)如何加载应用的类? javax.xml.parsers包中定义了xml解析的类接口Service Provider Interface (SPI) 位于rt.jar 即接口在启动ClassLoader中。而SPI的实现类,在AppLoader。但启动类记载其不可能认识这些实现类的代码,怎么办呢?

  • 解决:线程上下文加载器(Thread Context ClassLoader)。通过Thread.setContextClassLoader()方法进行设置。它的基本思想是在顶层ClassLoader中,传入底层ClassLoader的实例。

//设置类加载器contextClassLoader
 public void setContextClassLoader(ClassLoader cl) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
    }

代码来自javax.xml.parsers.FactoryFinder展示如何在启动类加载器中加载AppLoader的类

static private Class<?> getProviderClass(String className, ClassLoader cl,
            boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
    {
        try {
            if (cl == null) {
                if (useBSClsLoader) {//使用bootstrap classLoader加载这个类
                    return Class.forName(className, false, FactoryFinder.class.getClassLoader());
                } else {
                    cl = ss.getContextClassLoader();
                    if (cl == null) {
                        throw new ClassNotFoundException();
                    }
                    else {
                        return Class.forName(className, false, cl);//使用上下文加载器去加载这个类
                    }
                }
            }
            else {//使用当前提供的类加载器去加载这个类
                return Class.forName(className, false, cl);
            }
        }
        catch (ClassNotFoundException e1) {
            if (doFallback) {
                // Use current class loader - should always be bootstrap CL
                return Class.forName(className, false, FactoryFinder.class.getClassLoader());
            }
            else {
                throw e1;
            }
        }
    }

双亲委派模型是默认的模式,并不是必须要这么做。Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent。OSGi的ClassLoader形成网状结构,根据需要自由加载Class。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值