资讯专栏INFORMATION COLUMN

虚拟机类加载机制

airborne007 / 1303人阅读

摘要:虚拟机为了保证一个类的方法在多线程环境中被正确地加锁同步。但启动类加载器不可能认识这些代码。实现模块化热部署的关键则是它的自定义类加载器机制的实现。

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

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

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

类加载过程分为:

加载 Loading(注意,别与类加载混淆,类加载是个过程,加载是其一个阶段)

验证 Verification

准备 Preparation

解析 Resolution

初始化 Initialization

加载

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

通过一个类的全限定名来获取定义此类的二进制字节流。 不一定要从本地的Class文件获取,可以从jar包,网络,甚至十六进制编辑器弄出来的。开发人员可以重写类加载器的loadClass()方法。

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

在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证

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

文件格式验证,如魔数(0xCAFEBABE)开头、主次版本号是否在当前虚拟机处理范围之内等。

元数据验证,此阶段开始就不是直接操作字节流,而是读取方法区里的信息,元数据验证大概就是验证是否符合Java语言规范

字节码验证,是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是否合法,符合逻辑。JDK6之后做了优化,不在验证,可以通过-XX:-UseSplitVerifier关闭优化。

符号引用验证,此阶段可以看做是类自己身意外的信息进行匹配性校验。

准备

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

public static int value = 123;

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

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

数据类型 零值 数据类型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char "u0000" reference null
byte (byte)0

特殊情况

public static final int value = 123;

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

解析

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

初始化

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

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

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

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

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
}

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

类加载器

重头戏来了,了解上面的类加载过程之后,我们对类加载有个感性的认识,于是我们可以使用类加载器去决定如何去获取所需的类。
虽然类加载器仅仅实现类的加载动作(阶段),但它在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的角度来讲,只存在两种不同的类加载器:

启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++语言实现,是虚拟机自身的一部分。

另一种就是其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader

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

启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在$JAVA_HOME/lib目录下的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不会被加载)类库加载到虚拟机内存中。可以被-Xbootclasspath参数修改。启动类加载器无法被Java程序直接引用。

扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Lancher$ExtClassLoader实现,负责加载$JAVA_HOME/lib/ext目录下的,或者被java.ext.dirs系统变量指定的路径中的所有类库。开发者可以直接使用扩展类加载器。

应用程序加载器(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() {
                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将按照下面顺序进行类搜索:

将以java.*开头的类为派给父类加载器架子啊

否则,将 委派列表名单内的类 委派给 父类加载器 加载

否则,将Import列表中的类 委派给Export这个类的Bundle的类加载器加载

否则,查找当前Bundle的ClassPath,使用自己的类加载器加载

否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载

否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器架子啊

否则,类查找失败

总结

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70725.html

相关文章

  • 虚拟机类加载机制(读书笔记)

    摘要:类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括加载验证准备解析初始化使用和卸载 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(verification)、准备(preparation)、解析(resolution)、初始化(initialization)、使用(using)和卸载(unloading)

    stormjun 评论0 收藏0
  • 深入理解Java虚拟机05--虚拟机类加载机制

    摘要:我们甚至可以从网络或者其他的地方加载一个二进制流作为程序的一部分。对于任何一个类,我们通过类和这个类的加载器共同确定在中的唯一性,为了保证父类和子类的层次关系。一.前言  我们一定心里有个疑问,我们那个多态是怎么回事?我们指定的一个接口,却可以等到运行时可以对应于不同的实现类。这是因为,Java有个特性就是依赖运行期动态加载和动态连接,这样实现了Java可以动态进行扩展。我们甚至可以从网络或...

    yanbingyun1990 评论0 收藏0
  • 虚拟机类加载机制

    摘要:加载阶段在类的加载阶段,虚拟机需要完成以下件事情通过一个类的全限定名来获取定义此类的二进制字节流。验证阶段验证是连接阶段的第一步,这一阶段的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 注:本篇文章中的内容是根据《深入理解Java虚拟机--JVM高级特性与最佳实践》而总结的,如有理解错误,欢迎大家指正! 虚拟机把描述类的数据从Class文件...

    k00baa 评论0 收藏0
  • JAVA 虚拟机类加载机制和字节码执行引擎

    摘要:实现这个口号的就是可以运行在不同平台上的虚拟机和与平台无关的字节码。类加载过程加载加载是类加载的第一个阶段,虚拟机要完成以下三个过程通过类的全限定名获取定义此类的二进制字节流。验证目的是确保文件字节流信息符合虚拟机的要求。 引言 我们知道java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢? 一 类文件结构 无关性基石 ja...

    RichardXG 评论0 收藏0
  • 深入理解虚拟机之虚拟机类加载机制

    摘要:最终形成可以被虚拟机最直接使用的类型的过程就是虚拟机的类加载机制。即重写一个类加载器的方法验证验证是连接阶段的第一步,这一阶段的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版》读书笔记与常见相关面试题总结 本节常见面试题(推荐带着问题阅读,问题答案在文中都有提到): 简单说说类加载过...

    MadPecker 评论0 收藏0
  • Java虚拟机类加载过程

    摘要:二验证验证主要是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。五初始化类的初始化阶段是类加载过程的最后一步,该阶段才真正开始执行类中定义的程序代码或者说是字节码。 关注我,每天三分钟,带你轻松掌握一个Java相关知识点。 虚拟机(JVM)经常出现在我们面试中,但是工作中却很少遇到,导致很多同学没有去了解过。其实除了应付面试,作为java程序员,了解...

    lentoo 评论0 收藏0

发表评论

0条评论

airborne007

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<