Jvm 类加载机制解析,一起来了解神秘的类加载机制吧

版权声明:https://blog.csdn.net/petterp https://blog.csdn.net/petterp/article/details/88757059

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过 加载,连接,初始化,这三个步骤对类进行初始化,如果没有意外,JVM 将会连续完成这三个步骤,所以有时也称为类初始化。

虽然我们并无序过分关心类加载机制,但是 基本 工作机制我们还是要知道的,这样对我们来说也能更好的去理解。先用一张思维导图来概括类加载机制。不过需要注意的是,jdk1.9对类加载器进行了改变,并废除了一些方法,本篇博客并没有对 jdk1.9进行概述。

在这里插入图片描述

类加载 (划重点)

类加载指的是将类的 class 文件读入内存,并为之创建一个 Java.lang.class 对象,也就是说,当程序中使用任何类时,系统都会为之创建 一个 java.lang.Class 对象。
类的加载由加载器完成,类加载器通常由 jvm 提供,这些类加载器也是前面所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,还可以通过继承 ClassLoader 基类来创建自己的类加载器。一般有如下几种类加载来源:

  • 从本地文件系统加载class 文件
  • 从jar包记载 class 文件
  • 通过网络加载class文件
  • 把一个java源文件动态编译,并执行加载。

类加载器通常无序等到 首次使用该类时才加载,Java 虚拟机允许系统预加载某些类。
当 JVM 启动时(也就是你点击运行程序之后),会形成有三个类 加载器组成的初始类加载器层次结构:

  1. Bootstrop ClassLoader ,被称为引导(也成为原始或根) 类加载器。他负责加载 Java的核心类。 我们一般使用的 Math,String,System类就是由此加载。
  2. Extension Classloader ,被称为扩展类加载器,他负责加载 jre 的扩展目录中 jar 包的类。通过这种方式,我们就可以为 java 扩展核心类 以外的新功能。
  3. System Classloader,系统(也称应用) 类加载器,该加载器继承自扩展类加载器,主要功能是加载我们在日常编译器(idea,eclipse等)里面编写的类,这些类的加载,都是由该类加载器完成的。程序可以通过 ClassLoader 的静态方法 getSysemClassLoader() 获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以类加载器作为父加载器。

类加载机制主要有以下三种

  • 全盘负责:当一个类加载器加载某个 Clas时,该Class 所依赖和引用的其他 Class 也将于该类加载器负责载入,除非显示的使用另一个类加载器来载入。
  • 双亲委派: 当一个类加载器收到了类加载请求,它会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径加载该类,如果直至引用程序类加载器还没有发现该类同名的类,则会抛出异常。举个简单例子,你可以自定义一个 java.lang.Math类,但你永远永不了该类,因为该类存于 java 核心类,会由引导加载器去加载,所以不论你怎么调用,它加载的永远是 java 核心类库的 Math类。
  • 缓存机制: 缓存机制会保证所有加载过的 Class 都会被缓存,当程序中使用某个Class时,类加载器首先从缓存区寻找 该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class对象。这也就是为什么修改了 Class之后,必须重新运行程序才会生效的原因。
    但需要注意,类加载器之间并不是继承上的父子关系,而是类加载器实例之间的关系。

连接

当类被加载之后,系统为之生成一个对应的 Class 文件,接着将会进入连续阶段,链接阶段负责把 类的二进制数据合并到 jre中,类链接又分为如下3个阶段。

  1. 验证: 验证阶段同于检验被加载的类是否有正确的内部结构,就是检查符不符合Java语言规范,并和其他类协调一致。
  2. 准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值。
    • 八种基本类型默认的初始值为0
    • 引用类型默认的初始值是 null
    • 有 static final 修饰的会直接赋值,例如 static final a=10,则默认就是10
  3. 解析:将类的二进制数据中的符号引用替换成直接引用。就是jvm 会把所有的类或接口名,字段名,方法名转换为具体的内存地址。

初始化

虚拟机负责对类进行初始化,主要就是对类 变量 进行初始化,有如下两种初始化方式:

  • 声明类变量时初始化
  • 使用静态初始化为类变量指定初始值

一般初始化一个类包含如下几个步骤:

  1. 假如这个类没有被加载和连接,则程序先加载并连接该类。
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
  3. 假如该类中有初始化语句,则系统一次执行这些初始化语句。

一个类什么时候会被初始化呢?

  • 创建类的实例。为某个类创建实例的方法会包括:使用new 操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
  • 调用某个类的类方法(静态方法)。

  • 访问某个类或接口的类变量,或为该类变量赋值。

  • 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。

  • Class.forName("Person") 
    //如果系统还未初始化 一个 类,则这行代码将会导致该类被初始化,并返回该 类对应的 java.lang.Class对象。
    
  • 初始化某个类的子类。当初始化某个类的子类时,盖子类的所有父类就会被初始化。

  • 直接使用 java.exe 命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。

  • 不过也需要注意以下几种情形:

    • 对于一个 final 型的变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于 “宏变量” 。java 编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。

    • public class MyClass {
      
          //使用静态初始化块打印的语句
          static {
              System.out.println("123");
          }
          //声明变量a时指定初始值
          static final int a = 5;
      }
       class Demo{
           public static void main(String[] args) {
               //并不会初始化Myclass
               System.out.println(MyClass.a);
           }    
      }
      

      反之,如果final修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,通过该类来访问它的类变量,则会导致该类被初始化。

      public class MyClass {
          static {
              System.out.println("静态方法块");
          }
          //运行时才可以确定的变量,就算加了final也不行
          static  final String com=System.currentTimeMillis()+"";
      }
      class Demo{
          public static void main(String[] args) {
              System.out.println(  MyClass.com);
          }
      }
      

当某个变量使用了 final 修饰,而且他的值可以在编译时就确定下来,那么程序其他地方使用该变量时,实际上并没有使用该变量的值,而是相当于使用常量。

猜你喜欢

转载自blog.csdn.net/petterp/article/details/88757059