IT教程 ·

JVM类加载器是不是能够加载自定义的String

工作五年的.neter的一些经历感想和对未来的一些疑惑

媒介

曾经有一次,面试官问到类加载机制,置信大多数小伙伴都能够答上来双亲委派机制,也都晓得JVM出于平安性的斟酌,全限制类名雷同的String是不能被加载的。然则假如加载了,会涌现什么样的效果呢?非常?那是什么样的非常。假如包名不雷同呢?自定义类加载器是不是能够加载呢?置信面试官从种种差别的角度反击,很快就会答出破绽,毕竟咱没有深入研究过虚拟机...

接下来笔者就针对上述问题举行逐一考证。该篇文章抱着求证答案的方向动身,并没有太多理论方面的详解。若有明白上的误差,还望人人不吝赐教。

JVM都有哪些类加载器

起首我们放上一张节选自收集的JVM类加载机制示意图

JVM类加载器是不是能够加载自定义的String IT教程 第1张

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 完成且悉数继续自java.lang.ClassLoader:

  • BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++完成,担任加载 %JAVA_HOME%/lib目次下的jar包和类或许或被 -Xbootclasspath参数指定的途径中的一切类。
  • ExtensionClassLoader(扩大类加载器) :重要担任加载目次 %JRE_HOME%/lib/ext 目次下的jar包和类,或被 java.ext.dirs 体系变量所指定的途径下的jar包。
  • AppClassLoader(运用程序类加载器) :面向我们用户的加载器,担任加载当前运用classpath下的一切jar包和类。

JVM类加载体式格局

类加载有三种体式格局:

  • 1、命令行启动运用时刻由JVM初始化加载
  • 2、经由历程Class.forName()要领动态加载
  • 3、经由历程ClassLoader.loadClass()要领动态加载

Class.forName()和ClassLoader.loadClass()辨别

  • Class.forName():将类的.class文件加载到jvm中以外,还会对类举行诠释,实行类中的static块;
  • ClassLoader.loadClass():只干一件事变,就是将.class文件加载到jvm中,不会实行static中的内容,只要在newInstance才会去实行static块。
  • Class.forName(name,initialize,loader)带参函数也可掌握是不是加载static块。而且只要挪用了newInstance()要领采纳挪用组织函数,建立类的对象 。

JVM类加载机制

  • 通盘担任,当一个类加载器担任加载某个Class时,该Class所依靠的和援用的其他Class也将由该类加载器担任载入,除非显现运用别的一个类加载器来载入
  • 父类托付,先让父类加载器试图加载该类,只要在父类加载器没法加载该类时才尝试从本身的类途径中加载该类
  • 缓存机制,缓存机制将会保证一切加载过的Class都会被缓存,当程序中须要运用某个Class时,类加载器先从缓存区寻觅该Class,只要缓存区不存在,体系才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为何修正了Class后,必需重启JVM,程序的修正才会见效

JVM类加载机制源码

双亲委派模子完成源码剖析

private final ClassLoader parent; 
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) {//父加载器不为空,挪用父加载器loadClass()要领处置惩罚
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,运用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出非常申明父类加载器没法完成加载要求
                }

                if (c == null) {
                    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;
        }
    }

双亲委派模子的优点

双亲委派模子保证了Java程序的稳固运转,能够防止类的反复加载(JVM 辨别差别类的体式格局不单单议依据类名,雷同的类文件被差别的类加载器加载发作的是两个差别的类),也保证了 Java 的中心 API 不被改动。假如没有运用双亲委派模子,而是每一个类加载器加载本身的话就会涌现一些问题,比方我们编写一个称为 java.lang.Object 类的话,那末程序运转的时刻,体系就会涌现多个差别的 Object 类。

假如我们不想用双亲委派模子怎么办?

为了防止双亲托付机制,我们能够本身定义一个类加载器,然后重写 loadClass() 即可。

体系类加载器加载自定义String

1. 起首我们看下一般的类加载历程

package com.example.demojava.loadclass;
public class ClassLoaderDemo{

    public static void main(String[] args) {
        System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}

效果输出

ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@75bd9247
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader的父类加载器为ExtClassLoader
ExtClassLoader的父类加载器为null,null并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader

2. 我们本身定义一个String类,看下会发作什么

package com.example.demojava.loadclass;

public class String {
    public static void main(String[] args) {
        System.out.println("我是自定义的String");
    }
}

效果输出

  demo-java javac src/main/java/com/example/demojava/loadclass/String.java 
  demo-java java src.main.java.com.example.demojava.loadclass.String 
毛病: 找不到或没法加载主类 src.main.java.com.example.demojava.loadclass.String

这里清楚有main要领,全限制类名又和jdk的String不在同一个package(不会形成争执),为何会输出找不到或没法加载主类呢?

仔细的小伙伴肯定会发现该类'没有导入'体系的String类,会不会因为JVM的类加载机制,AppClassLoader加载类的时刻,因为自定义的String被加载,阻拦了上层的String类呢?String对象是自定义的,不符合main()要领的定义体式格局,故体系抛找不到main()要领。

我们反过来考证下适才的推想,再次运转适才的ClassLoaderDemo会发作什么呢?what?IDE中的main()要领去那里了?照样手动编译运转下吧

  demo-java javac src/main/java/com/example/demojava/loadclass/ClassLoaderDemo.java 
  demo-java java src.main.java.com.example.demojava.loadclass.ClassLoaderDemo 
毛病: 找不到或没法加载主类 src.main.java.com.example.demojava.loadclass.ClassLoaderDemo

效果显现: 之前一般运转的java类也找不到主类了。

我们导入准确的String类再来考证下

package com.example.demojava.loadclass;

public class String {
    public static void main(java.lang.String[] args) {
        System.out.println("我是自定义的String");
    }
}

效果输出

我是自定义的String

3. 可否覆写lang包下的String类?

上边的案例修正包途径即可

package java.lang;

public class String {
    public static void main(java.lang.String[] args) {
        System.out.println("我是自定义的String");
    }
}

输出报错

Connected to the target VM, address: '127.0.0.1:63569', transport: 'socket'
毛病: 在类 java.lang.String 中找不到 main 要领, 请将 main 要领定义为:
   public static void main(String[] args)
不然 JavaFX 运用程序类必需扩大javafx.application.Application

剖析:起首因为全限制类名java.lang.String即是jdk中的String类,依据上边类加载源码可知,当AppClassLoader加载该String时,推断java.lang.String已加载,便不会再次加载。所以实行的依旧是jdk中的String,然则体系的java.lang.String中没有main()要领,所以会报错。这是一种平安机制。

然后考证下默许的类加载器可否加载自定义的java.lang.String。==,默许的AppClassLoader能加载Everything?

public class LoadStringDemo {

    public static void main(String[] args) {
        URLClassLoader systemClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
        URL[] urLs = systemClassLoader.getURLs();
        for (URL url: urLs) {
            System.out.println(url);
        }
    }
}

输出日记以下

...
file:/Users/cuishiying/work/demo-java/target/classes/
...

日记太多,然则相对没有其他的包途径(当前包下的java.lang.String默许只能时jdk中的)

自定义类加载器

为何会存在自定义类加载器呢

自定义类加载器的中心在于对字节码文件的猎取,假如是加密的字节码则须要在该类中对文件举行解密。

因为现实项目中,会有多种加载.class文件的体式格局,

  • 从当地体系中直接加载
  • 经由历程收集下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

怎样自定义类加载器

package com.example.demojava.loadclass;

import com.demo.ClassLoaderDemo;

import java.io.*;
import java.lang.reflect.Method;


public class MyClassLoader extends ClassLoader {

    private String root;


    /**
     * @param name 全限制类名
     * @return
     * @throws ClassNotFoundException
     */

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);

        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }


    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar +
                className.replace('.', File.separatorChar) + ".class";

        try {
            InputStream ins = new FileInputStream(fileName);

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            int bufferSize = 1024;

            byte[] buffer = new byte[bufferSize];

            int length = 0;

            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }

            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args) throws Exception {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("/Users/cuishiying/Desktop/demo");

        Class<?> clz = Class.forName("LoadDemo", true, classLoader);
        Object  instance = clz.newInstance();
        Method test = clz.getDeclaredMethod("test");
        test.setAccessible(true);
        test.invoke(instance); 

        System.out.println(instance.getClass().getClassLoader());

    }
}

效果输出

test
com.example.demojava.loadclass.MyClassLoader@75bd9247

由此可知,自定义类加载器已能够一般事情。这里我们不能把LoadDemo放在类途径下,因为双亲托付机制的存在,会直接致使该类由 AppClassLoader加载,而不会经由历程我们自定义类加载器来加载。

自定义类加载器加载手写java.lang.String

改写自定义类加载器的main()要领

    public static void main(String[] args) throws Exception {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("/Users/cuishiying/Desktop/demo");

        Class<?> clz = classLoader.findClass("java.lang.String");
        Object  instance = clz.newInstance();

        System.out.println(instance.getClass().getClassLoader());
    }

JVM因为平安机制抛出了SecurityException

/Users/cuishiying/Desktop/demo/java/lang/String.class
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at com.example.demojava.loadclass.MyClassLoader.findClass(MyClassLoader.java:25)
    at com.example.demojava.loadclass.MyClassLoader.main(MyClassLoader.java:71)

民众号 【当我赶上你】

痞子衡嵌入式:恩智浦i.MX RT1xxx系列MCU启动那些事(11.1)- FlexSPI NOR连接方式大全(RT1015/1020/1050)

参与评论