类加载过程

目录

 整体的流程图

1.什么是类加载过程:

2.类加载分为三部

2.1加载

a.通过类的全类名获取定义此类的二进制流

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

c.在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

类加载器的任务

自定义ClassLoader

​编辑

2.2 链接阶段

2.2.1 验证

2.2.2 准备

 2.2.3 解析

2.3 初始化

 3. 类执行顺序


 整体的流程图

 

1.什么是类加载过程:

       一个java文件从编码完成到最终运行,都会经历两个过程,编译期和运行期。编译,通过javac命令,将java文件转换为二进制的字节码文件,即.class文件。运行,就是将编译期产生的.class文件交给jvm去执行。而类的加载过程就是将.class文件中的元信息加载进内存,创建Class对象并进行解析,初始化变量等的过程。

2.类加载分为三部

  加载,链接,初始化

2.1加载

     加载即Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用。

类加载主要有三个步骤:  

a.通过类的全类名获取定义此类的二进制流

对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合 JVM 规范即可)

  • 虚拟机可能通过文件系统读入一个 Class 后缀的文件(最常见)
  • 读入 jar、zip 等归档数据包,提取类文件
  • 事先存放在数据库中的类的二进制数据
  • 使用类似于 HTTP 之类的协议通过网络进行加载
  • 在运行时生成一段 Class 的二进制信息等

在获取到类的二进制信息后,Java 虚拟机就会处理这些数据,并最终转为一个 java.lang.Class 的实例,如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。

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

   类模型的位置

加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(JDK 1.8之前:永久代;JDK 1.8之后:元空间)

c.在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

   Class 实例的位置

类将 .class 文件加载至元空间后,会在堆中创建一个 java.lang.Class 对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象。

如图:外部可以通过访问代表 Order 类的 Class 对象来获取 Order 的类数据结构

 特别说明:数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

类加载器的任务

   是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。

类加载器分为四类:

启动类加载器(Bootstrap ClassLoader):主要负责加载存放在Java_Home/jre/lib下,或被-Xbootclasspath参数指定的路径下的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载),启动类加载器是无法被Java程序直接引用的。

扩展类加载器(Extension ClassLoader):主要负责加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载Java_Home/jre/lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

系统类加载器(System ClassLoader):主要负责加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类加载器(Custom ClassLoader:自己开发的类加载器

类加载过程遵循双亲委派机制

如果一个类加载器需要加载类,那么首先它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

通过查看Classloder类的loadclass方法的实现可以看出类在加载的过程中实现了双亲委派

正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载。

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          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
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  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

1、为什么要自定义ClassLoader

       因为系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader。而且我们可以根据自己的需求,对class文件进行加密和解密。

2.如何自定义ClassLoader

2.1 新建一个类继承自java.lang.ClassLoader,重写它的findClass方法。

2.2 将class字节码数组转换为Class类的实例

2.3 调用loadClass方法即可

代码如下:

public class MyClassloader extends ClassLoader {

    String classPath;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class aClass = null;
        // 获取该class文件字节码数组
        byte[] classData = getData();

        if (classData != null) {
            // 将class的字节码数组转换成Class类的实例
            aClass = defineClass(name, classData, 0, classData.length);
        }
        return aClass;

    }

    /**
     * 将class文件转化为字节码数组
     *
     * @return
     */
    private byte[] getData() {
        File file = new File(classPath);
        if (file.exists()) {
            FileInputStream in = null;
            ByteArrayOutputStream out = null;
            try {
                in = new FileInputStream(file);
                out = new ByteArrayOutputStream();

                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = in.read(buffer)) != -1) {
                    out.write(buffer, 0, size);
                }

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        } else {
            return null;
        }
    }

可以再getData里面做很多事情 ,比如加密解密之类的 都是可以的。

自己编写一个类测试

public class HelloMyClassLoader {
    public void hello() {
        System.out.println("Hello MyClassLoader");
    }
}

测试代码

  public static void main(String[] args) throws ClassNotFoundException {
        MyClassloader myClassloader = new MyClassloader("com/zpw/test_classloader/demo/HelloMyClassLoader.java");
        Class<?> aClass = myClassloader.loadClass("com.zpw.test_classloader.demo.MyClassloader");
        System.out.println(aClass);
        System.out.println("HelloMyClassLoader自定义类的类加载器" + HelloMyClassLoader.class.getClassLoader());
        System.out.println("HelloMyClassLoader自定义类的父类加载器" + HelloMyClassLoader.class.getClassLoader().getParent());
        HelloMyClassLoader helloMyClassLoader = new HelloMyClassLoader();
        helloMyClassLoader.hello();
    }

测试结果

类的加载

类加载有三种方式:

1、命令行启动应用时候由JVM初始化加载

2、通过Class.forName()方法动态加载

3、通过ClassLoader.loadClass()方法动态加载

如下验证:

public class HelloMyClassLoader {
    public void hello() {
        System.out.println("Hello MyClassLoader");
    }

    static {
        System.out.println("静态初始化块执行了!");
    }
}

 

public class LoadTest {
    public static void main(String[] args) throws ClassNotFoundException {
        /*ClassLoader loader = HelloMyClassLoader.class.getClassLoader();
        System.out.println(loader);*/
        //使用ClassLoader.loadClass()来加载类,不会执行初始化块
        //loader.loadClass("com.zpw.test_classloader.demo.HelloMyClassLoader");
        //使用Class.forName()来加载类,默认会执行初始化块
        //Class.forName("com.zpw.test_classloader.demo.HelloMyClassLoader");
        //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
        //Class.forName("com.zpw.test_classloader.demo.HelloMyClassLoader", true, loader);
        HelloMyClassLoader helloMyClassLoader = new HelloMyClassLoader();
    }
}

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

 

2.2 链接阶段

2.2.1 验证

       验证的过程只要是保证 class 文件的安全性和正确性,确保加载了该 class 文件不会导致 JVM 出现任何异常,不会危害JVM 的自身安全。验证包括对文件格式的验证,元数据和字节码的验证。验证一般分为四个阶段:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行

2.2.2 准备

准备阶段是为类变量分配内存并设置类变量初始值的阶段,分配这些内存是在方法区里面进行的,这个阶段有两点需要重点介绍以下的:

1、只有类变量(被static修饰的变量)会分配内存,不包括实例变量,实例变量是在对象实例化的时候在堆中分配内存的。

2、设置类变量的初始值是数量类型对应的默认值,而不是代码中设置的默认值。例如public static int number=111,这类变量number在准备阶段之后的初始值是0而不是111。而给number赋值为111是在初始化阶段。

基本数据类型默认值如下:

                      

 

 3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

   假设上面的类变量value被定义为: public static final int value = 3;

   编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。

 2.2.3 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

2.3 初始化

      初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  ①声明类变量是指定初始值

  ②使用静态代码块为类变量指定初始

 JVM初始化步骤

 1、假如这个类还没有被加载和连接,则程序先加载并连接该类

 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

 3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

– 创建类的实例,也就是new的方式

– 访问某个类或接口的静态变量,或者对该静态变量赋值

– 调用类的静态方法

– 反射(如Class.forName(“com.shengsiyuan.Test”))

– 初始化某个类的子类,则其父类也会被初始化

– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

 3. 类执行顺序

1. 静态代码块
(1)格式
在java类中,使用static关键字和{}声明的代码块

public class CodeBlock {
    static{
        System.out.println("静态代码块");
    }
}


(2)执行时机
        静态代码块在类被加载的时候就运行了,而且只运行一次,并且优先于各种代码块以及构造函数。如果一个类中有多个静态代码块,会按照书写顺序依次执行。后面在比较的时候会通过具体实例来证明。
(3)静态代码块的作用
        一般情况下,如果有些代码需要在项目启动的时候就执行,这时候就需要静态代码块。比如一个项目启动需要加载的很多配置文件等资源,我们就可以都放入静态代码块中。
(4)静态代码块不能存在任何方法体中
首先我们要明确静态代码块是在类加载的时候就要运行了。我们分情况讨论
       对于普通方法:由于普通方法是通过加载类,然后new出实例化对象,通过对象才能运行这个方法,而静态代码块只需要加载类之后就能运行了。
      对于静态方法:在类加载的时候,静态方法也已经加载了,但是我们必须要通过类名或者对象名才能访问,也就是说相比于静态代码块,静态代码块是主动运行的,而静态方法是被动运行的。
(5)静态代码块不能访问普通变量
普通变量只能通过对象来调用,是不能放在静态代码块中的。

2. 构造代码块

(1)格式

     在java类中使用{}声明的代码块(和静态代码块的区别是少了static关键字):

public class CodeBlock {
    static{
        System.out.println("静态代码块");
    }
    {
        System.out.println("构造代码块");
    }
}

(2)执行时机

构造代码块在创建对象时被调用,每次创建对象都会调用一次,但是优先于构造函数执行。构造代码块依托于构造函数,也就是说,如果你不实例化对象,构造代码块是不会执行的

public class Constructor {
    {
        System.out.println("构造代码块");
    }
     
    public Constructor(){
        System.out.println("无参构造函数");
    }
    public Constructor(String str){
        System.out.println("有参构造函数");
    }
    
    public static void main(String[] args) {
        new Constructor();
        System.out.println();
        new Constructor("构造代码块");
    }
}

(3)构造代码块的作用

和构造函数的作用类似,都能对对象进行初始化,并且只要创建一个对象,构造代码块都会执行一次。

反过来,构造函数则不一定每个对象建立时都执行(多个构造函数情况下,建立对象时,传入的参数不同则初始化使用对应的构造函数)。

3、构造函数
(1)构造函数的命名必须和类名完全相同。在java中普通函数可以和构造函数同名,但是必须带有返回值
(2)构造函数的功能主要用于在类的对象创建时定义初始化的状态。它没有返回值,也不能用void来修饰。这就保证了它不仅什么也不用自动返回,而且根本不能有任何选择。而其他方法都有返回值,即使是void返回值
(3)构造函数不能被直接调用,必须通过new运算符在创建对象时才会自动调用;
而一般的方法是在程序执行到它的时候被调用的
(4)默认先调用父类的无参构造函数

4、普通代码块

普通代码块和构造代码块的区别是:

  构造代码块是在类中定义的,

  普通代码块是在方法体中定义的。且普通代码块的执行顺序和书写顺序一致。

5、各种类型变量的默认初始值

JVM 类加载机制中提到,**类连接 (验证, 准备, 解析)**中准备工作:

  • 负责为类的类变量(非对象变量)分配内存,并设置默认初始值,准备类中每个字段、方法和实现接口所需的数据结构
  • 这里说的初始值都是默认的值, 并不是程序中指定的值 :
  • // 例子
    public class Text {
        public static int k = 10;
        public int a = print("a");
        public static int b = print("b");
        public final static int c = 10;
       
        public static Text t1 = new Text("t1");
        public static Text t2 = new Text("t2");
        public static int i = print("i");
        public static int n = 99;
        public int j = print("j");
    }
    // 经过准备工作后,类中变量的初始值为如下:
    //k =0;     b=0;     t1=null;     t2=null;    i=0;    n=0;
    

    测试代码:

  • public class ClassMethodOrder {
        public static void main(String[] args) {
            Fruit fruit = new Fruit();
            fruit.getName("香蕉");
            System.out.println("***************************************8");
            Apple apple = new Apple();
            apple.getName("hello");
            System.out.println("*******************再new一个子类看是否执行静态代码块********************8");
            Apple apple1 = new Apple();
            apple1.getName("苹果");
        }
    }
    
    class Fruit {
        static {
            System.out.println("++++++++++++父类静态代码块+++++++++++++++");
        }
    
        {
            System.out.println("++++++++++父类普通代码块++++++++++");
        }
    
        public Fruit() {
            System.out.println("++++++父类构造方法+++++");
        }
    
        public void getName(String name) {
            System.out.println("+++父类普通方法+++");
        }
    }
    
    class Apple extends Fruit {
        static {
            System.out.println("++++++++++++子类静态代码块+++++++++++++++");
        }
    
        {
            System.out.println("++++++++++子类普通代码块++++++++++");
        }
    
        public Apple() {
            System.out.println("++++++子类构造方法+++++");
        }
    
        public void getName(String name) {
            System.out.println("+++子类普通方法+++");
        }
    
    
    }

    结果:

  •  

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值