虚拟机类加载机制

概念区分:
加载、类加载、类加载器

类加载是一个过程。
加载(Loading)是类加载这一个过程的阶段。
类加载器是ClassLoader类或其子类。

本文中的”类“的描述都包括了类和接口的可能性,因为每个Class文件都有可能代表Java语言中的一个类或接口。
本文中的”Class文件“并非特指存在于具体磁盘中的文件,更准确理解应该是一串二进制的字节流。

类加载过程分为:

  1. 加载 Loading(注意,别与类加载混淆,类加载是个过程,加载是其一个阶段)
  2. 验证 Verification
  3. 准备 Preparation
  4. 解析 Resolution
  5. 初始化 Initialization

加载

在这个阶段,主要完成3件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。 不一定要从本地的Class文件获取,可以从jar包,网络,甚至十六进制编辑器弄出来的。开发人员可以重写类加载器的loadClass()方法。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证

这一阶段目的为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证,如魔数(0xCAFEBABE)开头、主次版本号是否在当前虚拟机处理范围之内等。
  2. 元数据验证,此阶段开始就不是直接操作字节流,而是读取方法区里的信息,元数据验证大概就是验证是否符合Java语言规范
  3. 字节码验证,是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是否合法,符合逻辑。JDK6之后做了优化,不在验证,可以通过-XX:-UseSplitVerifier关闭优化。
  4. 符号引用验证,此阶段可以看做是类自己身意外的信息进行匹配性校验。

准备

此阶段正是为 类变量 分配内存和设置 类变量 初始值的阶段。这些变量所使用的内存都将在方法区中进行分配。注意这里仅包括 类变量(被static修饰的变量),而不是包括实例变量。

public static int value = 123;

在这个阶段中,value的值是0

以下是基本数据类型的零值

数据类型零值数据类型零值
int0booleanfalse
long0Lfloat0.0f
short(short)0double0.0d
char'u0000'referencenull
byte(byte)0

特殊情况

public static final int value = 123;

编译时javac将会为value生产ConstantValue属性,在准备阶段虚拟机会根据ConstatnValue的设置,将value赋值为123;。

解析

这个阶段有点复杂,我还讲不清,先跳过。 //TODO 2017年10月29日

初始化

类初始化是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段,用户应用程序可以通过自己定义类加载参与之外,其余动作完全由虚拟机主导和控制。

到了这个初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

在编译时,编译器或自动收集 类 中的所有类变量(被static修饰的变量)的赋值操作和静态语句块中的语句合并,从而生成出一个叫<cinit>()方法。编译器的收集顺序是源文件中出现的顺序决定的。也就是说静态赋值语句和静态代码块都是从上往下执行。

<cinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,他不需要显示地调用父类构造器,虚拟机会保证子类的<cinit>()方法执行之前,父类的<cinit>()方法语句执行完毕。 这就意味着父类定义的静态赋值语句和静态代码块要优先于子类执行。

staic class Parent{
    public static int A = 1; 
    static{
        A = 2;
    }
}

static class Sub extends Parent{
    public static int B = A;
}

public static void main(String[] args){
    System.out.println(Sub.B);  //result: 2
}

虚拟机为了保证一个类的<cinit>()方法在多线程环境中被正确地加锁、同步。于是在多个线程同时去初始化一个类时,那么只会有一个线程去执行这个类的<cinit>()方法,其他线程都需要阻塞等待。 于是这里就有个问题,如果一个类的<cinit>()方法有耗时很长的操作,就可能造成多个线程阻塞。

类加载器

重头戏来了,了解上面的类加载过程之后,我们对类加载有个感性的认识,于是我们可以使用类加载器去决定如何去获取所需的类。
虽然类加载器仅仅实现类的加载动作(阶段),但它在Java程序中起到的作用远远不限于类加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
也就是说,判断两个类是否”相等“(这个“相等”包括类的Class对象的equals()方法、isAssignableForm()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系的判定),只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要它们的类加载器不一样,那么这两个类就必定不同。

package com.jc.jvm.classloader;

import java.io.IOException;
import java.io.InputStream;

/**
 * 类加载器与instanceof关键字例子
 * 
 */
public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        //定义类加载器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".")+1)+".class";  // 只需要ClassLoaderTest.class
                InputStream in = getClass().getResourceAsStream(fileName);
                if(in==null){
                    return super.loadClass(name);
                }

                byte[] b = new byte[0];
                try {
                    b = new byte[in.available()];
                    in.read(b);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }

                return defineClass(name,b,0,b.length);


            }
        };


        //使用类加载器
        Object obj = myLoader.loadClass("com.jc.jvm.classloader.ClassLoaderTest").newInstance();



        //判断class是否相同
        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.jc.jvm.classloader.ClassLoaderTest);
    }
}
/**output:
 * com.jc.jvm.classloader.ClassLoaderTest
 * false
 *
 */

双亲委派模型

大概了解类加载器是什么东西之后。我们来了解下,从JVM角度来看,有哪些类加载器。

从JVM的角度来讲,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++语言实现,是虚拟机自身的一部分。
  2. 另一种就是其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader

而从Java开发人员的角度来看,类加载器还可以划分得跟细致些:

  1. 启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在$JAVA_HOME/lib目录下的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不会被加载)类库加载到虚拟机内存中。可以被-Xbootclasspath参数修改。启动类加载器无法被Java程序直接引用。
  2. 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Lancher$ExtClassLoader实现,负责加载$JAVA_HOME/lib/ext目录下的,或者被java.ext.dirs系统变量指定的路径中的所有类库。开发者可以直接使用扩展类加载器。
  3. 应用程序加载器(Application ClassLoader):这个类加载器由sum.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader的getSystemClassLoader()方法的返回值,所以一般也称它为 系统类加载器。如果应用程序中没有自定义过自己的类加载器,则使用该类加载器作为默认。它负责加载用户类路径(ClassPath)上所指定的类库。

再加上自定义类加载器,那么它们之间的层次关系为:双亲委托模型(Parents Delegation Model)。双亲委托模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以集成继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来服用父加载器的代码。

类加载的双亲委派模型实在JDK1.2期间引入的,但它不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。
双亲委派模型的工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派模型的实现:

 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
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class. 
                    c = findClass(name); 
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
双亲委派模型的破坏

由于双亲委派模型不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。因为历史原因和需求不同于是出现过3次破坏:

第一次破坏

由于java.lang.ClassLoader在JDK1.0就已经存在,而用户去继承ClassLoader,就是为覆写loadClass()方法,而这个方法实现有双亲委派模型的逻辑。于是这样被覆盖,双亲委派模型就被打破了。于是Java设计者在JDK1.2给ClassLoader添加一个新的方法findClass(),提倡大家应当把自己的类加载逻辑写到findClass()方法中,这样就不会破坏双亲委派模型的规则。因为loadClass()方法的逻辑里就是如果父类加载失败,则会调用自己的findClass()来完成加载,请看上面双亲委派模型的实现。

第二次破坏

双亲委派很好地解决了各个类加载器的基础类的统一问题,但如果是基础类,但启动类加载器不认得怎么办。 如JNDI服务,JNDI在JDK1.3开始就作为平台服务,它的代码是由启动类加载器加载(JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并不熟在应用的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)代码。但启动类加载器不可能”认识“这些代码。
于是Java设计团队引入一个不太优雅的设计:就是线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以设置,但默认是就是应用程序类加载器。有了这个 线程上下文类加载器(这名字有点长) 后,就可以做一些”舞弊“的事情(我喜欢称为hack),JNDI服务使用这个线程上下类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载其去完成类加载的动作。 于是又一次违背了双亲委派模型。详情请参考:javax.naming.InitialContext的源码。这里大概放出代码:

//javax.naming.spi.NamingManager
public static Context getInitialContext(Hashtable<?,?> env)
        throws NamingException {
        InitialContextFactory factory;

        InitialContextFactoryBuilder builder = getInitialContextFactoryBuilder();
        if (builder == null) {
            // No factory installed, use property
            // Get initial context factory class name

            String className = env != null ?
                (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;
            if (className == null) {
                NoInitialContextException ne = new NoInitialContextException(
                    "Need to specify class name in environment or system " +
                    "property, or as an applet parameter, or in an " +
                    "application resource file:  " +
                    Context.INITIAL_CONTEXT_FACTORY);
                throw ne;
            }

            try {
                factory = (InitialContextFactory)
                    helper.loadClass(className).newInstance(); //这个helper就是类加载器
            } catch(Exception e) {
                NoInitialContextException ne =
                    new NoInitialContextException(
                        "Cannot instantiate class: " + className);
                ne.setRootCause(e);
                throw ne;
            }
        } else {
            factory = builder.createInitialContextFactory(env);
        }

        return factory.getInitialContext(env);
    }
//获取线程上下文类加载器

  ClassLoader getContextClassLoader() {

        return AccessController.doPrivileged(
            new PrivilegedAction<ClassLoader>() {
                public ClassLoader run() {
                    ClassLoader loader =
                            Thread.currentThread().getContextClassLoader();  //线程类加载器
                    if (loader == null) {
                        // Don't use bootstrap class loader directly!
                        loader = ClassLoader.getSystemClassLoader();
                    }

                    return loader;
                }
            }
        );
    }
第三次破坏

这次破坏就严重咯,是由于用户对程序动态性的追求而导致的。也就是:代码替换(HotSwap)、模块热部署(Hot Deployment)等。
对于模块化之争有,Sun公司的Jigsaw项目和OSGi组织的规范。

目前来看OSGi语句成为了业界的Java模块化标准。

OSGi实现模块化热部署的关键则是它的自定义类加载器机制的实现。每一个程序模块(OSGi中成为Bundle)都有一个自己的类加载器。 当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。
当收到类加载的请求时,OSGi将按照下面顺序进行类搜索:

  1. 将以java.*开头的类为派给父类加载器架子啊
  2. 否则,将 委派列表名单内的类 委派给 父类加载器 加载
  3. 否则,将Import列表中的类 委派给Export这个类的Bundle的类加载器加载
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器架子啊
  7. 否则,类查找失败

总结

先大概了解类加载的过程
在了解类加载器是什么东西
然后在了解双亲委派模型
最后实际就是为热部署做铺垫,了解到都是为需求而变化,并未强制使用某种规范。从3次双亲委派模型的破坏,我们可以看出这个模型并不是很成熟。
OSGi中对类加载器的使用很值得学习,弄懂了OSGi的实现,就可以算是掌握了类加载器的精髓。

参考
《深入理解Java虚拟机——JVM高级特性与最佳实践》 周志明 机械工业出版社

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值