class加载过程

共 15942字,需浏览 32分钟

 ·

2021-03-20 10:11

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

76套java从入门到精通实战课程分享

1.class加载过程

java虚拟机把描述类的数据从class文件加载到内存,并对数据进行 校验/准备/解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

loading -> linking (verification-> preparation -> resolution )-> initializing


loading:把class文件load到内存中,采用双亲委派,主要是为了安全性

verification:校验class文件是否符合标准

preparation:静态变量分配内存并设初始值的阶段(不包括实例变量)

resolution:把符号引用转换为直接引用

initializing:静态变量赋初始值


1.1 Loading 过程

遇到new,getstatic,putstatic或者invokestatic 这四条指令时,如果类型没有进行初始,则需要先触发其初始化阶段。

能够生成这四条指令的典型Java代码场景有:

1.使用new关键字实例化对象。

2.读取或设置一个类型的静态字段。(被final修饰、已在编译期把结果放入常量池的静态字段除外,但是final static int i=10;如果没有=10还是会在准备阶段给它赋默认值,在初始化的时候赋值。)

3.调用一个类型的静态方法。

4.使用java.lang.reflect包的方法对类型进行反射调用。

5.虚拟机启动的时候被执行的主类必须初始化。

6.初始化子类时,父类首先初始化。


1.2 Linking 过程

Verification:验证文件是否符合JVM规定

Preparation: 静态成员变量赋默认值

Resolution:将类、方法、属性等符号引用解析为直接引用常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用


1.3 Initializing

调用类初始化代码 ,给静态成员变量赋初始值


1.4 双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这 个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载 请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求 (它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。


2.类加载器

类加载器实现了loading这个动作,把class文件加载到内存。

1.启动类加载器(BootstrapClassLoader):负责加载存放在lib/rt.jar charset.jar 等目录下的核心类,由c++实现。启动类加载器无法被Java程序直接引用(classLoader的loadClass方法中,若parent为 null则使用启动类加载器。

2.扩展类加载器(ExtensionClassLoader):负责加载lib/ext目录下的类,在类sun.misc.Launcher E x t C l a s s L o a d e r 中 以 J a v a 代 码 的 形 式 实 现 。3. 应 用 程 序 类 加 载 器 ( A p p l i c a t i o n C l a s s L o a d e r ) :负 责 加 载 用 户 类 路 径 ( C l a s s P a t h ) 下 所 有 的 类 库 。由 s u n . m i s c . L a u n c h e r ExtClassLoader 中以Java代码的形式实现 。3.应用程序类加载器(ApplicationClassLoader):负责加载用户类路径(ClassPath)下所有的类库。由sun.misc.Launcher ExtClassLoader中以Java代码的形式实现。3.应用程序类加载器(ApplicationClassLoader):负责加载用户类路径(ClassPath)下所有的类库。由sun.misc.LauncherAppClassLoader实现 ,是ClassLoader类中的getSystemClassLoader()方法的返回值。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

4、自定义类加载器:CustomClassLoader

public class ClassLoaderScope {
     public static void main(String[] args) {
         System.out.println("-------------------Bootstrap加载类-----------------")
          String property = System.getProperty("sun.boot.class.path");
          String s = property.replaceAll(";", System.lineSeparator());
          System.out.println(s);
  
         System.out.println("-------------------Ext加载类-------------------");
         String property1 = System.getProperty("java.ext.dirs");
         String s1 = property1.replaceAll(";", System.lineSeparator());
         System.out.println(s1);
 
         System.out.println("-------------------App加载类-------------------");
         String property2 = System.getProperty("java.class.path");
         String s2 = property2.replaceAll(";", System.lineSeparator());
         System.out.println(s2);
     }
 }


2.1 自定义类加载器

只需继承ClassLoader抽象类,并重写findClass方法(如果要打破双亲委派模型,需要重写loadClass方法)原因可以查看ClassLoader的源码:

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中的loadClass方法,大致流程如下:

1)检查类是否已加载,如果是则不用再重新加载了;

2)如果未加载,则通过父类加载(依次递归)或者启动类加载器(bootstrap)加载;

3)如果还未找到,则调用本加载器的findClass方法;

以上可知,类加载器先通过父类加载,父类未找到时,才有本加载器加载。


因为自定义类加载器是继承ClassLoader,而我们再看findClass方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {    
    throw new ClassNotFoundException(name);
}


可以看出,它直接返回ClassNotFoundException。

因此,自定义类加载器必须重写findClass方法。


自定义类加载器示例代码:类加载器HClassLoader:

class HClassLoader extends ClassLoader {

private String classPath;

public HClassLoader(String classPath) {
    this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    try {
        byte[] data = loadByte(name);
        return defineClass(name, data, 0, data.length);
    } catch (Exception e) {
        e.printStackTrace();
        throw new ClassNotFoundException();
    }

}

/**
 * 获取.class的字节流
 * @param name
 * @return
 * @throws Exception
 */
private byte[] loadByte(String name) throws Exception {
    name = name.replaceAll("\\.""/");
    FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
    int len = fis.available();
    byte[] data = new byte[len];
    fis.read(data);
    fis.close();
    // 字节流解密
    data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);
    return data;
  }
}



加载一个类Car:

public class Car {

    public Car() {
        System.out.println("Car:" + getClass().getClassLoader());
        System.out.println("Car Parent:" + getClass().getClassLoader().getParent());
    }
    public String print() {
        System.out.println("Car:print()");
        return "carPrint";
    }
 }


测试代码

@Test
public void testClassLoader() throws Exception {
    HClassLoader myClassLoader = new HClassLoader("e:/temp/a");
    Class clazz = myClassLoader.loadClass("com.ha.Car");
    Object o = clazz.newInstance();
    Method print = clazz.getDeclaredMethod("print", null);
    print.invoke(o, null);
}


需要注意的是:

执行测试代码前,必须将Car.class文件移动到e:/temp/a下,并且按包名建立层级目录(这里为com/ha/)。因为如果不移动Car.class文件,那么Car类会被AppClassLoader加载(自定义类加载器的parent是AppClassLoader)。


2.2 自定义类加载器的应用

上面介绍了Java类加载器的相关知识。对于自定义类加载器,哪里可以用到呢?

主流的Java Web服务器,比如Tomcat,都实现了自定义的类加载器。因为它要解决几个问题:

1)Tomcat上可以部署多个不同的应用,但是它们可以使用同一份类库的不同版本。这就需要自定义类加载器,以便对加载的类库进行隔离,否则会出现问题;

2)对于非.class的文件,需要转为Java类,就需要自定义类加载器。比如JSP文件。


这里举一个其它的例子:Java核心代码的加密。

假设我们项目当中,有一些核心代码不想让别人反编译看到。当前知道有两种方法,一种是通过代码混淆(推荐Allatori,商用收费);一种是自己编写加密算法,对字节码加密,加大反编译难度。

代码混淆如果用Allatori,比较简便。注意控制自己编写类的访问权限即可。接口用public,内部方法用private,其他的用默认的(即不加访问修饰符)或者protected。代码混淆这里不过多说明,这里主要介绍一下字节码加密。

大概流程是:

.class加密代码:

@Test
public void testEncode() {
    String classFile = "e:/temp/a/com/ha/Car.class";
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(classFile);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();

        data = DESInstance.enCode("1234567890qwertyuiopasdf".getBytes(), data);

        String outFile = "e:/temp/a/com/ha/EnCar.class";
        FileOutputStream fos = new FileOutputStream(outFile);
        fos.write(data);
        fos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }

}



类加载器中解密,查看上文中的:

// 字节流解密
data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);


加解密工具类

public class DESInstance {

    private static String ALGORITHM = "DESede";

    /**
     * 加密
     *
     * @param key
     * @param src
     * @return
     */
    public static byte[] enCode(byte[] key, byte[] src) {

        byte[] value = null;
        SecretKey deskey = new SecretKeySpec(key, ALGORITHM);
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, deskey);
            value = cipher.doFinal(src);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return value;
    }

    /**
     * 解密
     *
     * @param key
     * @param src
     * @return
     */
    public static byte[] deCode(byte[] key, byte[] src) {
        byte[] value = null;
        SecretKey deskey = new SecretKeySpec(key, ALGORITHM);

        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, deskey);
            value = cipher.doFinal(src);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return value;
    }
}


注意秘钥是24位,否则会报错:

java.security.InvalidKeyException: Invalid key length

如果解密密码错误,则是如下错误:

javax.crypto.BadPaddingException: Given final block not properly padded

当然,这样做还是会被反编译破解,要加大难度,还需要其他处理的。

————————————————

版权声明:本文为CSDN博主「兢兢业业的子牙」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:

https://blog.csdn.net/qq_33449307/article/details/114950029





锋哥最新SpringCloud分布式电商秒杀课程发布

👇👇👇

👆长按上方微信二维码 2 秒





感谢点赞支持下哈 

浏览 57
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报