【虚拟机系列二】把虚拟机类加载机制探个究竟

上一期讲了高并发服务中的GC调优,对G1、CMS的作了一下简单的优缺点分析,和业务中的Full GC、Young GC、Mixd GC进行了原理分析和优化建议。
这次和各位小伙伴聊一下虚拟机中类加载这块的东西,以及实际业务中使用的场景。

一、Class文件的结构

package org.apache.dubbo.vm;

public class Halo {
    public static void main(String[] args) {
        System.out.println("\"hello\" = " + "hello");
    }
}

将Halo这个Java文件编译成.class文件,然后用十六进制的编辑打开:

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 34 00 1D 0A 00 06 00 0F 09    J~:>...4........
00000010: 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07    ................
00000020: 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29    .....<init>...()
00000030: 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E    V...Code...LineN
00000040: 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69    umberTable...mai
00000050: 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67    n...([Ljava/lang
00000060: 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75    /String;)V...Sou
00000070: 72 63 65 46 69 6C 65 01 00 09 48 61 6C 6F 2E 6A    rceFile...Halo.j
00000080: 61 76 61 0C 00 07 00 08 07 00 17 0C 00 18 00 19    ava.............
00000090: 01 00 0F 22 68 65 6C 6C 6F 22 20 3D 20 68 65 6C    ..."hello".=.hel
000000a0: 6C 6F 07 00 1A 0C 00 1B 00 1C 01 00 18 6F 72 67    lo...........org
000000b0: 2F 61 70 61 63 68 65 2F 64 75 62 62 6F 2F 76 6D    /apache/dubbo/vm
000000c0: 2F 48 61 6C 6F 01 00 10 6A 61 76 61 2F 6C 61 6E    /Halo...java/lan
000000d0: 67 2F 4F 62 6A 65 63 74 01 00 10 6A 61 76 61 2F    g/Object...java/
000000e0: 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75    lang/System...ou
000000f0: 74 01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69    t...Ljava/io/Pri
00000100: 6E 74 53 74 72 65 61 6D 3B 01 00 13 6A 61 76 61    ntStream;...java
00000110: 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 01    /io/PrintStream.
00000120: 00 07 70 72 69 6E 74 6C 6E 01 00 15 28 4C 6A 61    ..println...(Lja
00000130: 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29    va/lang/String;)
00000140: 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 00    V.!.............
00000150: 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01 00    ................
00000160: 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00    ...*7..1........
00000170: 00 06 00 01 00 00 00 03 00 09 00 0B 00 0C 00 01    ................
00000180: 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2 00    .....%........2.
00000190: 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00 00    ...6..1.........
000001a0: 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D 00    ................
000001b0: 00 00 02 00 0E  

由于是16进制的格式,所以0001中的每列中的一个元素表示一个字节。.class文件存储的基本单位就是一个8位字节,如果需要占用一个字节以上,就按照高位在前Big-endian进行存储。

Big-endian

int类型的数据0x01234567。假设在内存中的起始地址为0x200,则内存中的存储地址就是下面这样的:

0x2000x2010x2020x203
01234567

.class文件中主要以两种数据类型组成:

  1. 无符号数,class文件中的最基础的数据类型,比如u1u2u4u8。u后面的数字表示这个无符号数为几个字节。
  2. 表,表是由多个无符号数或者其他表组成的数据类型。比如cp_infomethod_info,习惯以_info结尾来表示。

.class文件中依次存储的数据结构如下:

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count - 1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_count

比如看下魔数,是一个无符号数u4,按照高位在前的存储顺序,可以看到实际的值为0xCAFFBABE,刚好是我的公众号名字,哈哈。

后面的类加载机制中验证阶段文件格式验证的阶段,主要就是校验当前的.class文件是否以0xCAFFBABE开头,minor_versionmajor_version是否在当前虚拟机兼容范围内,constant_pool中的常量是否有不支持的数据类型等等。

比如对于constant_pool_count,值为0x001D,转化为十进制之后为29,表示的的是constant_pool中的常量个数。反编译上面的class文件看到:

Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // \"hello\" = hello
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // org/apache/dubbo/vm/Halo
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Halo.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               \"hello\" = hello
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               org/apache/dubbo/vm/Halo
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V

反编译看到的是28,这是为什么?因为constant_pool_count的第一位存储的是另外的信息,所以实际constant_pool中常量个数为28,是一致的。

这块的内容在面试中基本上不会问的很深,知道是大概的存储结构,以及其中的某几个数据类型就可以了。这里先管中窥豹一下,哈哈。

二、类加载

正在准备面试中的大伙对这块肯定很熟悉了,这里我也比较概括性的写一下。后面的实战环节中细看会比较深刻一些。

  1. 类的生命周期

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载,这里着重说一下加载这个阶段,因为这块在实际中的工作场景中使用的比较多。

  • 比如运行时动态生成,无论是从文件中生成,比如SPI技术,还是从内存中使用字符串动态编译成.class二进制字节流动态代理等都需要对类加载有一定的了解,这块也是我们日常写代码可控性最强的一个阶段。
  1. 加载过程
  • 获取class的二进制字节流
  • 将class的存储结构转化为方法区的数据结构
  • 内存中生成Class对象,作为方法区对于这个类的访问入口

加载过程是通过类加载器完成的,此外使用equals()isAssignableFrom()isInstance()方法时,类加载器也起着很大的作用。

虚拟机中比较两个类是否相等,需要判断类的加载器是否相等,以及类本身是否相等。类加载器可以分为以下3种:

  • 启动类加载器,主要加载/lib目录下的,比如rt.jar
  • 扩展类加载器,主要加载/lib/ext中的类
  • 应用程序加载器,负责加载用户的ClassPath中的类,应用中默认的加载器就是它。
  • 自定义加载器

这三种加载器,上面的加载器为下面的父类,只有启动类加载器没有父类。

  1. 双亲委派

比如类加载器收到了加载类的请求,他会把这个过程委派给父类加载器去完成,每一种加载器都是如此,所以最终的所有加载请求都会传到顶层的启动类加载器。

只有当父类加载器说我没有办法加载,子加载器才会尝试自己去加载。

这样设计的好处就是避免了Java类型的混乱,如果用户没有自己定义加载器,那么最终的类加载都是由启动类加载器,这样就保证了类型唯一。

比如看ClassLoaderloadClass方法

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

ClassLoader中有一个属性parent,表示的是父类加载器,先会调用父类的loadClass()方法,因此保证了双亲委派机制。

看到如果父类加载器无法自己去加载,则会调用自己的findClass()方法,因此,在自定义类加载逻辑的时候应该写在这个方法里。

我们自己在定义类加载器的时候,也不要覆盖loadClass这个方法,不要随意的破坏这个机制,避免引起混乱。

  1. 双亲委派可以被破坏吗?

比如三方库的用的虚拟机SPI机制,去实现不同的接口,比如MyBatis等。

SPI机制需要将实现的接口代码放在应用程序的ClassPath中,而JDK的SPI接口都是放在<JAVA_HOME>/lib中的,由启动类加载器加载。

由于启动类加载器不能加载程序类路径中的类,因此诞生了线程上下文加载器。

如果创建线程时没有调用Thread.setContextClassLoader方法自己定义,会从父线程中继承一个,如果没有全局都没有设置的话,这个类加载器就是默认的应用程序加载器。

因此,实际中的SPI代码是通过父类去委托子类进行加载的,因此某种意义上破坏了双亲委派模型。

还有比如就是我们常用的自定义类加载的逻辑,比如dubbo框架中的SPI
机制,实现了javassist和覆盖JDKClassLoader去完成Adaptive类的加载。

这种都是打破了双亲委派模型,由子类去定义类加载逻辑。CAFFE上一周也在看dubbo相关的源码,就发现了dubbo中的Adaptive的实现是如此的。

CAFFE后面有时间会自己仿照dubbo写一个rpc框架出来,到时候在详细讲解。

类加载实战环节

这次主要从实际的业务场景出发,比如一些规则引擎项目需要去实现一些模板的解析,根据业务传进来的字符串,进行相应的模板解析等。

这次就简单讲解下如何将一个字符串编译成Class,加载进虚拟机的内存中。CAFFE在研究如何写这个样例的时候,是用idea单步调试法,找到需要覆盖的核心方法,在写的,因此实际中也可以照着使用。

首先定义一个加载器

public class ClassLoaderImpl extends ClassLoader {

    /**
     * 缓存javaFileObject,实际中用来对同一个动态类进行更新,避免重复加载
     */
    private Map<String, JavaFileObject> classes = new HashMap<>(16);

    public ClassLoaderImpl(final ClassLoader parentClassLoader) {
        super(parentClassLoader);
    }

    public Collection<JavaFileObject> files() {
        return Collections.unmodifiableCollection(classes.values());
    }

    // 应该把自己的类加载逻辑写在findClass中
    @Override
    protected Class<?> findClass(final String qualifiedClassName) throws ClassNotFoundException {
        JavaFileObject javaFileObject = classes.get(qualifiedClassName);
        if (javaFileObject != null) {
            byte[] bytes = ((JavaFileObjectImpl) javaFileObject).getByteCode();
            return defineClass(qualifiedClassName, bytes, 0, bytes.length);
        }
        throw new ClassNotFoundException("qualifiedClassName not found");
    }

    public void add(String qualifiedClassName, JavaFileObject javaFileObject) {
        classes.put(qualifiedClassName, javaFileObject);
    }

    // 不提倡覆盖loadClass方法,如果双亲都加载失败,则会调用自己的findClass方法来完成加载
    @Override
    protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
        return super.loadClass(name, resolve);
    }

    @Override
    public InputStream getResourceAsStream(final String name) {
        if (name.endsWith(".class")) {
            String qualifiedClassName = name.substring(0, name.length() - ".class".length()).replace("/", ".");
            JavaFileObjectImpl file = (JavaFileObjectImpl) classes.get(qualifiedClassName);
            if (file != null) {
                return new ByteArrayInputStream(file.getByteCode());
            }
        }
        return super.getResourceAsStream(name);
    }
}

然后实现一个JavaFileObject子类

public class JavaFileObjectImpl extends SimpleJavaFileObject {

    // code
    private final CharSequence source;
    // byte code
    private ByteArrayOutputStream bytecode;

    protected JavaFileObjectImpl(URI uri, Kind kind) {
        super(uri, kind);
        this.source = null;
    }

    /**
     *
     * @param className simpleName
     * @param source code
     */
    public JavaFileObjectImpl(String className, final CharSequence source) {
        super(URI.create(className + Kind.SOURCE.extension), Kind.SOURCE);
        this.source = source;
    }

    @Override
    public CharSequence getCharContent(final boolean ignoreEncodingErrors) throws UnsupportedOperationException {
        if (source == null || source.toString().trim().length() == 0) {
            throw new IllegalStateException("source code == null");
        }

        return source;
    }

    @Override
    public InputStream openInputStream() {
        return new ByteArrayInputStream(getByteCode());
    }

    @Override
    public OutputStream openOutputStream() {
        return bytecode = new ByteArrayOutputStream();
    }

    public byte[] getByteCode() {
        return bytecode.toByteArray();
    }
}

然后实现一个Java文件管理器,完成相应的编译以及加载

public class JavaFileManagerImpl extends ForwardingJavaFileManager<JavaFileManager> {

    private final ClassLoaderImpl classLoader;
    private final Map<URI, JavaFileObject> fileObjects = new HashMap<>();

    protected JavaFileManagerImpl(JavaFileManager fileManager, ClassLoaderImpl classLoader) {
        super(fileManager);
        this.classLoader = classLoader;
    }

    @Override
    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
        JavaFileObject file = fileObjects.get(uri(location, packageName, relativeName));
        if (file != null) {
            return file;
        }
        return super.getFileForInput(location, packageName, relativeName);
    }
    // genCode
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String qualifiedName, JavaFileObject.Kind kind, FileObject outputFile)
            throws IOException {
        URI uri;
        try {
            uri = new URI(qualifiedName);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e.getMessage());
        }
        JavaFileObject file = new JavaFileObjectImpl(uri, kind);
        classLoader.add(qualifiedName, file);
        return file;
    }

    @Override
    public ClassLoader getClassLoader(JavaFileManager.Location location) {
        return classLoader;
    }

    @Override
    public String inferBinaryName(Location loc, JavaFileObject file) {
        if (file instanceof JavaFileObjectImpl) {
            return file.getName();
        }
        return super.inferBinaryName(loc, file);
    }

    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
            throws IOException {
        Iterable<JavaFileObject> result = super.list(location, packageName, kinds, recurse);
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        List<JavaFileObject> files = new ArrayList<>();

        if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
            for (JavaFileObject file : fileObjects.values()) {
                if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
                    files.add(file);
                }
            }
            files.addAll(classLoader.files());
        } else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
            for (JavaFileObject file : fileObjects.values()) {
                if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
                    files.add(file);
                }
            }
        }

        for (JavaFileObject file : result) {
            files.add(file);
        }

        return files;
    }

    public void putFileForInput(StandardLocation location, String packageName, String relativeName, JavaFileObject file) {
        fileObjects.put(uri(location, packageName, relativeName), file);
    }

    private URI uri(Location location, String packageName, String relativeName) {
        try {
            return new URI(location.getName() + "/" + packageName + "/" + relativeName);
        } catch (URISyntaxException e) {
            throw new RuntimeException("uri error");
        }
    }

}

最后我们来实验一下:

private static final JavaCompiler compile = ToolProvider.getSystemJavaCompiler();

    public static void main(String[] args) {
        List<String> options = new ArrayList<String>();
        options.add("-source");
        options.add("1.6");
        options.add("-target");
        options.add("1.6");

        String packageName = "";
        String className = "Point";

        /**
         * 实际中会加代理,这里节约时间就不加了
         */
        StringBuilder buf = new StringBuilder("public class Point {\n")
                .append("public void halo() {\n")
                .append("System.out.println(\"fdfdf\");\n")
                .append("}\n")
                .append("}\n");
        JavaFileObjectImpl javaFileObject = new JavaFileObjectImpl(className, buf.toString());

        final DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
        StandardJavaFileManager manager = compile.getStandardFileManager(diagnosticCollector, null, null);

        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        ClassLoaderImpl classLoader = AccessController.doPrivileged(new PrivilegedAction<ClassLoaderImpl>() {
            @Override
            public ClassLoaderImpl run() {
                return new ClassLoaderImpl(loader);
            }
        });

        JavaFileManagerImpl fileManager = new JavaFileManagerImpl(manager, classLoader);

        fileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName, className + JavaFileObject.Kind.SOURCE.extension,
                javaFileObject);

        Boolean result = compile.getTask(null, fileManager, diagnosticCollector, options, null, Arrays.asList(javaFileObject))
                .call();

        Class<?> clazz = null;
        try {
            clazz = classLoader.loadClass(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            System.out.println("1 = " + 1);
        }

        // 代理这个类
        try {
            // 追求性能可以使用javaassist
            Object obj = clazz.getConstructor().newInstance();
            Method main = clazz.getMethods()[0];
            main.invoke(obj);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }

    }

可以看到最后的运行结果打印出出了fdfdf

对应的流程可以复制下来,使用单步调试法,就可以知道几个核心方法的调用上下文,对于理解虚拟机加载这个机制有很大的帮助。

其实现在有很多的字节码三方框架了,比如最近比较流行的ByteBuddy,以及老一点的javassistcglib等。但是无论选用哪种,都需要对类加载十分熟悉才行。

对于虚拟机类加载的其他阶段,实际写代码中可控性几乎没有,因此感兴趣的可以去看下相关的阶段。

我是CAFFBABE,一周一肝虚拟机原理实战篇,我们下周再见。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值