JVM - 类加载机制 & 类加载流程

类加载流程                                      

一个类的加载到使用,一般经历下面过程:

加载 > 验证 > 准备 > 解析 > 初始化 > 使用 > 卸载

类加载器图解:

所谓类加载机制就是
    虚拟机把 Class 文件加载到内存
    并对数据进行校验,转换解析和初始化
    形成可以虚拟机直接使用的 Java 类型,即 java.lang.Class

1. 加 载(Loading)

查找和导入class文件

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。
Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口

2. 链接(Link)

     1.验证(Verify)

保证被加载类的正确性
  • 文件格式验证 
    1. ​​​​​是否一 0xCAFEBABE 开头
    2. 版本号是否合理
  • 元数据验证
    1. 验证这个类是否又父类
    2. 是否继承了final类(final类不能被继承,如果继承了就有问题了。)
    3. 非抽象类是否实现了所有抽象方法,没有全部实现的化,这个类是无效的
  • 字节码验证
    1. 运行的检查
    2. 栈数据类型和操作码操作参数吻合(比如栈空间只有2字节,但其实却需要大于2字节,此时就认为这个字节码是有问题的)
    3. 跳转指令是否指向合理的位置
  • 符号引用验证
    1. 验证常量池中描述类的是否存在
    2. 访问的方法活字段是否存在且有足够的权限

    2.准备(Prepare)

为类的静态变量分配内存,并将其初始化为默认值,就是系统的初始值( 给加载进来的类分配好了内存空间,类变量也分配好了内存空间,并且给了默认的初始值)

在准备阶段,这段代码正常打印出0,因为静态变量i在准备阶段会有默认值0。静态变量编译通过

public class Demo1 { 
    private static int i; 
    public static void main(String[] args) {
         // 正常打印出0,因为静态变量i在准备阶段会有默认值0 
        System.out.println(i); 
    } 
}

非静态变量编译不通过

public class Demo2 {
 public static void main(String[] args) { 
    // 编译通不过,因为局部变量没有赋值不能被使用 
    int i; System.out.println(i); 
    } 
}

 final static  修饰

  •         final static 修饰的变量 在准备阶段会直接赋值为用户定义的值,比如 private final static int value = 123,直接赋值123,而不是0;
  •         static 修饰的变量 在准备阶段依然是0. 比如 private static int value = 12,依然是0
  •         static User u = new User() , 在准备阶段 u 的值是 null 。 在初始化才会 new User() 给 u 

    3. 解析(Resolve)

实际上是把符号引用替换为直接引用的过程

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

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

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

3. 初始化(Initialize)

对类的静态变量,静态代码块执行初始化操作

初始化阶段会给这个 int age 赋值 =10.

来看下这段代码:

 在 初始化这个阶段,就会执行类的初始化代码,比如上面的  Configuration.getInt("replica.flush.interval")  代码就会在这里执行,完成一个配置项的读取,然后赋值给这个类变量 “flushInterval” 。在上一个阶段(准备阶段),就是给“flushInterval” 变量赋值 0 

还有一个非常重要的规则,就是如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类


JDK 自带的命令
javap -h
可以验证一下上述 Classfifile Structure 前面几块内容的正确性
javap -v -p Person.class 进行反编译,查看字节码信息和指令等信息
Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口

一个class文件通过 CLASSLOADER 加载并初始化 。这个.class文件就是类似于一个模板,通过他创建多个对象实例。

根据上面堆栈的理解:car.class 就是在方法区,而对应的创建的多个实例就是在堆中

CLASSLOADER 加载机制 

CLASSLOADER 加载机制 (加载原则)和 双亲委派机制

  • 上级委托接待机制 也叫 双亲委派机制
    1. 某个特定的 类加载器 在接到加载类的请求时,首先会检查这个类是否已被加载,如果已加载,这拒绝这次请求
    2. 如果没有加载过,则像上级类加载器询问是否有上级加载器来加载,此时上级加载器根据他们的加载规则,检查这个类是否已经加载过此类,如果已加载过,则将结果返回。如果没有加载过,则继续像上级类加载器(如果还有更高级的)询问,以此类推。如果找不到,就会下推加载权力给下级加载器
    3. 加载的顺序
      自顶向下,也就是由上层来逐层尝试加载此类。
  • JVM的类加载器是有亲子层级结构的,就是说 启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器
  • JVM 提供的三层 ClassLoader
    • 加载顺序是 : Bootstrap Classloader  →→ Extension ClassLoader →→  Application ClassLoader
      • Bootstrap Classloader 启动类加载器
        1. 主要加载JVM自身工作需要的类,系统核心的类,加载你的Java安装目录下的“lib”目录中的核心类库。java.lang 下的。完全有JVM控制
        2. Bootstrap Classloader 不遵守 双亲委派机制,它仅仅是一个类的加载工具
        3. Bootstrap Classloader 既没有更高级的父加载器,也没有子加载器
      • Extension ClassLoader 扩展类加载器
        1. 服务的特定目标:System.getPropety("java.ext.dirs")
      • Application ClassLoader 应用程序类加载器
        1. 它的父类是 ExtClassLoader
        2. 它服务广大普通的类,所有在System.getPropety("java.class.path")目录下的类都可以被这个类加载进来,这个目录就是我们常用到的 classpath
        3. 实现自己的类加载器,不管是 直接实现抽象类 ClassLoader,还是继承 URLClassLoader 类或其子类,它的父加载器瓯都市 AppClassLoader
      • 自定义类加载器   Custom ClassLoader
        1. ​​​​​​​除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。

类加载器各个层级加载的类,以及加载顺序:

ClassLoad 源码

   ClassLoader类中 代码显示了自底向上检查该类是否已经加载

为什么用这种加载机制呢?   

父类已经加载了,子类加载器就不能再次加载! 安全。防止重复加载类。如果自己随便写一个类覆盖java自带的String 等类,就被随意修改了

  • 加载父类就是父类,除非用到子类才会加载子类;但是加载子类要初始化之前,必须先加载父类,初始化父类
  • Object Header(4字节) + Class Pointer(4字节)+ Fields(看存放类型),但是jvm内存占用是8的倍数,所以结果要向上取整到8的倍数
  • 类加载是按需加载,可以一次性加载全部的类吗?
    •   如果是默认的类加载机制,那么是你的代码运行过程中,遇到什么类加载什么类。如果你要自己加载类,那么需要写自己的类加载器

  • 运行你写的代码的时候,遇到你用了什么类,再加载什么类。执行new ReplicaManager 的时候加载类 。

  • 类是在准备阶段分配内存空间的 。实例变量得在你创建类的实例对象时才会初始化。 类的初始化阶段,仅仅是初始化类而已,跟对象无关,用new关键字才会构造一个对象出来

  • 自定义类加载器如何实现?
    • 答:自己写一个类,继承ClassLoader类,重写类加载的方法,然后在代码里面可以用自己的类加载器去针对某个路径下的类加载到内存里来


Tomcat类加载如果按委派模型的加载流程和实际实现的流程


 JVM 的 堆,栈,方法区

堆:

  • 提供所有类实例和数组对象存储区域
  • jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身(即具体的实例)
  • jvm只有堆区(heap)和方法区被所有线程共享
  • 存储的全部是对象,每个对象都包含一个与之对应的class的信息
  • 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。

栈:

  • 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
  • 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
  • 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
  • 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.

方法区:

  • 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量
  • 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
  • 运行时常量池都分配在 Java 虚拟机的方法区之中
  • 常量池主要存储两方面内容:字面量(Literal)和符号引用(Symbolic References)
    • 字面量:文本字符串,final修饰等
    • 符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值