jvm 之 类加载和初始化

三、类加载和初始化

面试题:

  1. 描述一下类加载器的层次?

  2. 双亲委派

  3. 为什么要双亲委派

Class文件 如何加载到内存中的 并且是如何执行的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8VDRdKSX-1641746615741)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image002-164174456853445.jpg)]

3.1 Class Cycle

Class文件在我们硬盘中,那么它是如何加载到内存中 总共需要三个大的步骤

在这里插入图片描述

  1. Loading步骤

Loading 是将本地的classfile的二进制的内容加载到内存中

  1. Linking 步骤

    1. Verification 校验

    Verification 的主要过程就是用来校验,如何加载的文件的字节码头不是CAFEBABE,该过程就会被拒绝。

    1. Preparation

      Preparation过程 主要作用就是将静态变量赋默认值 假设定义public static int i=8;在这个过程中并不是把i的值赋值成8,而是要对静态变量i进行默认值的赋值 也就是0;

    2. Resolution

      该过程 将class文件中常量池用到的一些符号引用转换为内存地址

  2. Initializing 步骤

静态变量在该步骤下进行赋值为初始值,才会调用静态代码块。

3.2 ClassLoader

JVM本身有个类加载器的层次 这个类加载器就是普通的Class,这个加载器的层次就是用来加载不同的class
在这里插入图片描述
在这里插入图片描述

注意: 任何一个classfile被加载到内存中的都会存在两个部分,

第一个部分 二进制的classfile确实被load内存中

第二个部分 生成的class类的对象,class中还会存在其他对象,引用到class对象,而class类对象指向classfile的内存加载

扩展为 Class对象究竟存储在哪里?

Class对象存储在metaspace里面

Metaspace 是JDK1.8版本出现的,Metaspace 就是方法区methodarea 1.8版本移出了永久代,原本在1.8版本之前 PermGenerationspace部分变更成了metaspace 而这两个地方指代的都是方法区。

怎么才能够知道哪些类是由哪些加载器进行加载的呢?

最顶层

BootstrapClassLoader

加载lib/rt.jar charset.jar等核心类 C++实现。

主要负责加载jdk中最核心的jar ,例如runtime.jar 或者是我们平时锁说的String.class,Object.class 都是位于lib/rt.jar

会出现null值 调用的是最顶层加载器,在java的类中没有这样的对象去应对他。

第二层

ExtClassLoader

加载扩展的jar包,jre/lib/ext/*.jar

或由-Djava.ext.dirs指定

第三层

AppClassLoader

加载classpath指定的内容

第四层

自定义加载器

加载自定义的类的内容。

package edu.yau;

import sun.net.spi.nameservice.dns.DNSNameService;

public class ClassLoaderTest01 {

    public static void main(String[] args) {
        //由最顶层的加载器加载的,核心代码库,由c++编写,java中没有一个可以对应的对象
        System.out.println(String.class.getClassLoader());
        System.out.println(sun.awt.HKSCS.class.getClassLoader());

        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
        //当前本类对象获取的类的加载器
        System.out.println(ClassLoaderTest01.class.getClassLoader());

        //由最顶层的加载器加载的
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
        System.out.println(ClassLoaderTest01.class.getClassLoader().getClass().getClassLoader());
    }
}


类加载器的加载过程叫双亲委派。

在双亲委派中存在一个概念 叫父加载器 这里的父加载器不是继承关系

在这里插入图片描述

该图描述的是语法上一种继承关系,而继承关系和父加载器没关系。

父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent
在这里插入图片描述

3.3 双亲委派

Class文件通过自定义的classloader进行加载,如果他没有加载,那么则委托它的父加载器appclassloader 加载, appclassloader 判断是否为本地加载 如果有则直接加载,如果没有则继续向上委托,直到顶层的加载器bootstrapClassLoader,但是当顶层的加载器,也没有加载,就会向下委托,当所有的下级加载器都没有加载那么则抛出异常 classNotFound 异常,如果下级加载器能够加载,那么就由下级加载器进行加载。

双亲:指的有一个从子到父的过程 又有一从父到子的过程

委派:自己不想做的事情 委托别人去完成

向上委派的时候 父加载器都是到 Cache中取寻找

可以把这个缓存理解成是一个list或者是一个数组。

面试题 为什么要去使用双亲委派?

  1. 防止加载同一个class文件,保证数据的安全

  2. 保证核心的class文件不被篡改,即使被篡改了也不会加载,即使被加载也不会是同一个class对象 为了保证class的执行安全。

这部分代码是被写死的。

3.4 父加载器

父加载器不是了的加载器的加载器,也不是加载器的父类的加载器

父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent

package edu.yau;

public class ClassLoaderTest02 {
    public static void main(String[] args) {
        //获取本类的加载器
        System.out.println(ClassLoaderTest02.class.getClassLoader());
       //获取本类的加载器的class对象的加载器--顶级加载器加载的
        System.out.println(ClassLoaderTest02.class.getClassLoader().getClass().getClassLoader());
        //获取本类加载器的父类加载器
        System.out.println(ClassLoaderTest02.class.getClassLoader().getParent());
        System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent());
        //System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent().getParent());
    }
}


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tzS4tOlI-1641746615745)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image010-164174456853548.jpg)]

3.5 类加载器范围

从上个案例的执行结果中,我们可以看出appclassloader和extclassloader 都是Launcher的内部类。 Launcher是classloader的包装类启动类

在Launcher源码中

private static String bootClassPath = System.getProperty("sun.boot.class.path");
final String var1 = System.getProperty("java.class.path");
String var0 = System.getProperty("java.ext.dirs");

sun.boot.class.path 是BootstrapClassloader的加载路径

java.class.path 是AppClassloader的加载路径

java.ext.dirs 是ExtClassLoader的加载路径

 import sun.misc.Launcher; 
 public class ClassLoaderTest03 {
 public static void main(String[] args){
 String pathBoot = System.getProperty("sun.boot.class.path");
 System.out.println(pathBoot.replaceAll(";",System.*lineSeparator()));
 System.out.println("---------------------------------");    
     String pathExt = System.getProperty("java.ext.dirs");    System.out.println(pathExt.replaceAll(";",System.*lineSeparator()));     
 System.out.println("---------------------------------"); 
 String pathApp = System.getProperty("java.class.path");    System.out.println(pathApp.replaceAll(";",System.*lineSeparator()));     } }

3.6 自定义加载器

小demo


public class ClassLoaderTest04 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = ClassLoaderTest04.class.getClassLoader().loadClass("com.openlab.Person");
        System.out.println(clazz.getName());
//       类加载器也可以用来加载资源
//       ClassLoaderTest04.class.getClassLoader().getResourceAsStream();

    }
}

Tomcat 加载的Servlet

Spring框架中加载ApplicationContext

比如在写一些类库的时候或者修改底层框架时。想加载哪个类就可以加载谁。

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 {
//继续使用parent的classloader 递归调用loadClass方法
                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
            }

// 调用findClass方法去找class
            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;
    }
}


  1. 继承ClassLoader

  2. 重写模板方法 findClass

​ ----调用defineClass方法

从目录中读取class文件,将class文件通过自定义加载器进行加载

利用IO流

Java语言是比较容易被反编译

-防止反编译

-防止篡改

可以给class文件进行加密 解密

作业:1.自定义加载器的实现 视频到群里

2.classfile解析的内容 需要整理 博客的形式 Xmind的形式

3.预习JVM的基础知识点

import java.io.*;

public class MacluClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(
                "c:/test",
                name.replaceAll(".","/").concat(".class"));

        try {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;
            while ((b = fis.read())!=0){
                baos.write(b);
            }

          byte[] bytes = baos.toByteArray();

            baos.close();
            fis.close();

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

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

         return super.findClass(name);// throw ClassNotFoundException
    }

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

        ClassLoader cl = new MacluClassLoader();
        Class clazz = cl.loadClass("com.openlab.Person");

        Person person = (Person) clazz.newInstance();
        person.m();

        System.out.println(cl.getClass().getClassLoader());
        System.out.println(cl.getParent());

    }
    }



我们可以定义自己格式的classloader,一般情况下class文件就是一个二进制文件流,可以采用一种比较简单的方式对class文件进行加密和解密

加密: 通过^ 异或 可以定义一个数字 在读取每一个字节的后的写入操作时,可以用流里面获取到的数据和这个数字进行异或的算法, 那么这种情况就可以进行加密的操作

解密: 字节数字这个数字这个数字 那么就完成了解密的操作。

import java.io.*;

public class MacluClassLoaderWithEncription extends ClassLoader{

    public static int seed = 0B10110110; // 进行参加加密算法的数字

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(
                "c:/test",
                name.replaceAll(".","/").concat(".class"));

        try {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;
            while ((b = fis.read())!=0){
                baos.write(b^seed);
            }

            byte[] bytes = baos.toByteArray();

            baos.close();
            fis.close();

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

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return super.findClass(name);// throw ClassNotFoundException
    }

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

        encFile("com.openlab.Person");
        ClassLoader cl = new MacluClassLoaderWithEncription();

        Class clazz = cl.loadClass("com.openlab.Person");

        Person person = (Person) clazz.newInstance();
        person.m();

        System.out.println(cl.getClass().getClassLoader());
        System.out.println(cl.getParent());

    }

    private static void encFile(String name) throws IOException {
        File file = new File(
                "c:/test/",
                name.replace(".","/").concat(".class"));

        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(
                new File("c:/test",name.replaceAll(".","/").concat(".macluclass")));
        int b = 0;

        while ((b = fis.read())!=-1){
            fos.write(b^seed);
        }
        fis.close();
        fos.close();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3Qyd8J2-1641746615745)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image012-164174456853549.jpg)]

生成加密好的文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbhIC0r9-1641746615746)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image014-164174456853550.jpg)]

验证加密文件 打开后是乱码。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C9vkEySu-1641746615746)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image016-164174456853551.jpg)]

3.7 编辑器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ZRO5OWR-1641746615747)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image018-164174456853552.jpg)]

解释器: bytecode-interpreter

JIT 即时编辑器 Just In Time compiler

Java语言究竟是一个解释型语言还是编译式的语言

想解释器的可以用解释器,想编译也可以用编译器 看需求是怎么写的 可以通过JVM的一些参数进行设置。

默认的情况是一种混合模式

混合模式:使用解释器+热点编辑器 hotspot

起始阶段采用解释来执行

热点代码的检测 默认值为10000

多次被调用的方法(方法计数器:检测方法的执行频率)

多次被调用的循环(循环的计数器:检测循环的执行频率)

当这样的一个循环或者是一个方法,或者是一段代码,一直都会被多次调用的时候,也就是这段代码执行频率特别高的情况下,那么干脆直接将这段代码编译成本地的代码,在下次直接访问的时候,直接访问本地的代码就可以。就不需要解释器对其进行解释执行。从而达到效率的提升。这种执行代码的方式被称为混合模式。

那么为什么不直接编译成本地代码,编译的执行速度更快?能够提高效率?

  1. 现在的解释器的执行效率已经是非常高的了,在一些简单的代码执行上,它并不属于编译器。

  2. 如果要执行的程序 依赖的类库特别多的情况下,在虚拟机中编译一遍,那么启动的过程会非常的缓慢。

-Xmixed 为混合模式:

开始解释执行,启动速度比较快,对热点代码进行检测和编译。

-Xint 解释模式

启动速度很快,执行较慢

-Xcomp 纯编译模式,

启动较慢,执行较快

测试这三个jvm参数

  public class WayToRunTest01 {     public static void main(String[] args){ 
        *//**这段代码被短时间执行很多次,请JVM虚拟机对其进行优化 *     
      for (int i = 0;i<10_0000;i++)       *m*();    
       long start =System.*currentTimeMillis*(); 
           for (int i = 0;i<10_0000;i++){       *m*();     }       
           long end = System.*currentTimeMillis*();     S
           ystem.*out*.println(end-start);     
           }     
           *//* *该方法本身没有意义,就是耗时间用的。 * 
             public static void m(){ 
                   for (int i = 0;i<10_0000L;i++){      
                    long j = i%3;   
  }   
  } }

默认的混合模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOlUxQpS-1641746615748)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image020-164174456853553.jpg)]

在JVM的执行参数中 -Xint 解释模式

很慢 回去洗洗睡吧

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LTdkNqvI-1641746615749)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image022-164174456853554.jpg)]

纯编译的模式-Xcomp

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7Nc4xFC-1641746615749)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image024-164174456853555.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dxaQtnKO-1641746615749)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image026-164174456853556.jpg)]

3.8 懒加载

严格来讲应该叫lazyInitializing

JVM规范中并没有规定什么时候加载

严格的规定了初始化的规则 扩展

  1. New对象 getstatic 访问静态变量时 putstatic 访问静态实例时,invokestatic指令

以上指令是必须要初始化这个类 访问final变量除外。

  1. 当反射调用的时候

  2. 初始化子类的时候 首先父类初始化

  3. 虚拟机启动时 被执行的主类必须要初始化

  4. 动态语言支持java.lang.invoke.MethodHandler解析结果为REF-getstatic REF-putstatic REF-invokestatic的方法句柄时 该类必须要初始化。

这个案例 主要看什么时候打印P和X

public class LazyLoadingTest {

    public static void main(String[] args) throws ClassNotFoundException {
//        P p;
//        X x = new X();
//        System.out.println(P.i);
//        System.out.println(P.j);
        Class.forName("com.openlab.LazyLoadingTest$P");


    }

    public static class P{
        final  static int i=8;// 打印final的值是不需要加载整个类的
        static int j = 9;
        static{
            System.out.println("P");
        }
    }

    public static class X extends P{
        static{
            System.out.println("X");
        }
    }
}

面试题:

如何打破classloader的双亲委派模式?

去重写Classloader中的loadClass方法 而不是findClass方法 这个时候就能够打破双亲委派的机制。

什么时候需要打破需要去打破双亲委派的机制:

  1. 在JDK1.2版本之前 要自定义classloader的话 必须要重写loadClass方法

  2. 在一个线程中设定自己的线程的上下文的加载器对象 ThreadContextClassLoader 可以实现基础调用实现类的代码, 通过thread.setContextClassLoader 来设定。

  3. 模块化的 热部署 热启动

像osgi和tomcat 都有自己的模块指定classloader,可以加载同一个类库的不同版本的对象,目前这个方式用的比较多。

Tomcat中 Webapplication 对象是可以存在多个的,有两个Webapplication 被加载,但是他们的版本不同,这种情况下可以打破双亲委派的。

注意:类的名字的是相同的 只是说版本不同 如果采用双亲委派的机制,那么这两个对象是不可能加载到同一个空间里面 因为加载的过程中,发现在同一空间有同名的类,那么他一定不会被加载。

所以tomcat的每一个Webapplication 都有一个classloader

双亲委派模式加载

package com.openlab;

public class ClassReloadingTest {

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

        MacluClassLoader classloader = new MacluClassLoader();

        Class clazz = classloader.loadClass("com.openlab.Person");

        classloader = null;
        System.out.println(clazz.hashCode());
        classloader = null;

        classloader = new MacluClassLoader();
        Class clazz1 = classloader.loadClass("com.openlab.Person");
        System.out.println(clazz1.hashCode());

        System.out.println(clazz == clazz1);


    }
}


从上案例中可以看出,双亲委派,即便重新创建了classloader对象,那么曾经被加载的对象,再次加载的时候,加载的还是这个对象。

热部署应该如何实现

import java.io.*;
public class T012_ClassReloading2 {
    private static class MyLoader extends ClassLoader {
        @Override     public Class<?> loadClass(String name) throws ClassNotFoundException {
            File f = new File("C:/test/" + name.replace(".", "/").concat(".class")); 
            if(!f.exists()) return      super.loadClass(name);      
            try {      
                InputStream is = new FileInputStream(f);   
                 byte[] b = new byte[is.available()];         is.read(b);     
                return defineClass(name, b, 0, b.length);       
            } catch ( FileNotFoundException e) {         e.printStackTrace();       
                                               } catch (IOException e) {    
                e.printStackTrace();      
            }       return super.loadClass(name);    
        }   
    }     
    public static void main(String[] args) throws Exception {   
        MyLoader m = new MyLoader(); 
        Class clazz = m.loadClass("com.openlab.Person");   
        m = new MyLoader(); 
        Class clazzNew = m.loadClass("com.openlab.Person");     
        System.out.println(clazz == clazzNew);   } }

上一篇 jvm 基础到入门 class文件结构
下一篇 jvm java内存模型

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zzsaixuexi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值