1.内存结构概述


2.类加载子系统作用

  • 负责从文件系统或者网络中加载Class文件(该文件在文件开头有特定的标识)
  • 只负责Class文件的加载,至于能否运行由Execution Engine决定
  • 加载的类信息被存放在称为方法区的内存空间中

tips:

  • 除了类的信息,方法区还会存放运行时常量池信息(可能包括字符串常量和数字常量,这部分信息是Class文件中常量池部分的内存映射)
  • 将Class文件进行反编译会发现结构中有常量池,将该信息加载到内存中就是运行时常量池

3.类的加载过程

3.1 加载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将该字节流代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表该类的java.lang.Class对象作为方法区中该类的数据访问入口

3.2 链接

  • 验证:确保Class文件的字节流中包含的信息符合当前JVM要求,确保加载类的准确性

    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  • 准备:为**类变量(静态变量)**分配内存并且设置该类变量的默认初始值

    • 不包含分配内存给final修饰的静态变量(视为常量),因为final在编译时就会分配,在准备阶段会显示初始化
    • 不会为实例变量分配内存并初始化,因为类变量分配在方法区,而实例变量随着对象分配到堆中
  • 解析:将常量池内的符号引用转换为直接引用的过程(实际上解析是在JVM执行完初始化后再执行)

    • 符号引用:一组符号用于描述所引用的目标
    // 像#1就是使用到的类的符号引用
    Constant pool:#1 = Methodref          #8.#30         // java/lang/Object."<init>":()V#2 = Class              #31            // com/atguigu/SellTicket02#3 = Methodref          #2.#30         // com/atguigu/SellTicket02."<init>":()V#4 = Class              #32            // com/atguigu/SellTicket03#5 = Methodref          #4.#30         // com/atguigu/SellTicket03."<init>":()V#6 = Methodref          #4.#33         // com/atguigu/SellTicket03.start:()V#7 = Class              #34            // com/atguigu/test
    
    • 直接引用:直接指向目标的指针、相对偏移量或间接定位到目标的句柄

3.3 初始化

  • 初始化过程就是执行类构造器方法的过程

    • 该方法不需要定义,是javac编译器自动收集类中的所有类变量赋值和静态代码块中的语句合并而来
    • 方法中的指令是按语句在源文件中出现顺序执行
    • 如果类中没有静态代码块或类变量就不会生成该方法
    • 若该类有父类,JVM会保证父类的先执行后再执行子类的
    • JVM必须保证一个类的方法在多线程下被同步加锁
    Runnable r = () -> {System.out.println(Thread.currentThread().getName() + "开始");DeadThread deadThread = new DeadThread();System.out.println(Thread.currentThread().getName() + "结束");};
    Thread thread1 = new Thread(r, "线程1");
    Thread thread2 = new Thread(r, "线程2");
    thread1.start();
    thread2.start();
    // 定义类
    class DeadThread{// 用于产生<cinit>方法,死循环用于让该方法一直执行。如果该方法不是同步加锁的,则另一个线程也会执行该方法,最终结果会输出两个线程都在进行初始化static {if (true){System.out.println(Thread.currentThread().getName() + "进行初始化");while (true){}}}
    }
    // 输出结果:
    //线程1开始
    //线程2开始
    //线程1进行初始化(此时线程2因为同步加锁的原因无法执行<cinit>方法)
    
  • 类构造器方法不同于平日提到的类的构造器(在JVM视角下构造器是方法)

tips:

  • 如下面代码所示,为什么可以先赋值再声明?在链接中的准备阶段,已经对类变量number分配内存并设置默认值0,然后在初始化阶段执行clinit方法按语句顺序先完成静态代码块的赋值(即number=5),再初始化为10
static {// 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问number = 5;  // 赋值不报错System.out.println(number);  // 编译器会报错
}
public void m(){// 不会报错,因为执行该方法时,在堆中的对象中已经有了一个名为number的引用并初始化指向了System.out.println(number);
}
private static int number = 10;
// 测试
System.out.println(test2.number);  // 打印10

4.加载器的分类

  • 自定义类加载器:所有派生于抽象类ClassLoader的类加载器(即使用Java语言编写)都划分为自定义加载器,图中的Extension Class LoaderSystem Class Loader因为间接继承了ClassLoader,所以也属于该类加载器

    • 扩展类加载器Extension Class Loader

      • 父类加载器为引导类加载器
      • 如果用户创建的jar包放在JDK的jre/lib/ext目录下,会自动由扩展类加载器加载
    • 系统类加载器(应用程序加载器)System Class Loader
      • 父类加载器为扩展类加载器
      • 程序中默认的类加载器,一般来说Java应用的类都是由它完成加载
    • 用户自定义类加载器:
      • 为什么要自定义类加载器:

        • 隔离加载类:使用多个框架时可能出现类的冲突
        • 修改类加载的方式
        • 扩展加载源
        • 防止源码泄露:相当于自定义编译和反编译方式
  • 引导类(启动类)加载器(Bootstrap ClassLoader)
    • 使用C/C++实现,嵌套在JVM内部
    • 用于加载Java核心库(如JAVA_HOME/jre/lib/rt.jar),提供JVM自身需要的类
    • 不继承java.lang.ClassLoader没有父类加载器
    • 加载扩展类和应用程序类加载器,并指定为它们的父类加载器
    • 处于安全考虑,只记载包名为java、javax、sun等开头的类

// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// JDK11:jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
// JDK8:sun.misc.Launcher$AppClassLoader@...
System.out.println(systemClassLoader);
// 获取系统类加载器的父类加载器 --> 扩展类加载器
// JDK11:jdk.internal.loader.ClassLoaders$PlatformClassLoader@7c30a502
// JDK8:sun.misc.Launcher$ExtClassLoader@...
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
// 获取扩展类加载器的父类加载器 --> 引导类加载器(获取不到)
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);  // null
// 获取当前用户自定义类的加载器(默认使用系统类加载器)
ClassLoader curLoader = ClassLoaderTest.class.getClassLoader();
// JDK11:jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
// JDK8:sun.misc.Launcher$AppClassLoader@...
System.out.println(curLoader);
// 获取系统核心类库的加载器(获取不到,即使用引导类加载器)
ClassLoader sysloader = String.class.getClassLoader();
System.out.println(sysloader);  // null

tips:

  • 上述提到的父类并不是指继承关系,而是层级上的关系
  • JVM为什么需要那么多类加载器?一方面允许在一个JVM里运行不同的应用程序,另一方面方便用户独立的对不同类库进行运行时增强

5.双亲委派机制

  • 概念:JVM对Class文件采取的按需加载(即需要使用某个类才将它的Class文件加载到内存生成对象),而加载该类的Class文件时采用双亲委派模式(即把请求交给父类处理,它是一种任务委派模式)

  • 工作原理:

    • 如果一个类加载器收到了类加载的请求,它并不会自己先去加载,而是把该请求委托给父类加载器去执行
    • 如果父类加载器还存在父类加载器,则继续向上委托,直到请求到达引导类加载器
    • 如果父类加载器可以完成类的加载,就成功返回;如果无法完成,子类加载器才去尝试自己加载

  • 场景:创建一个java.lang包,且在该包下写一个String
// src/com/psj/test.java
public static void main(String[] args) {String s = new String();System.out.println("程序正常执行");
}
// src/java/lang/String
public class String {// 如果加载的是该类就会执行该静态代码块static {System.out.println("执行自定义的String类");}public static void main(String[] args) {System.out.println("程序正常执行");}
}
// 执行test.java最终结果还是执行系统类库的Strig类
// 执行Strng.java会报错,因为在核心库中的String类是没有定义main方法的
// 原因:自定义String类的类加载器默认为系统类加载器,此时由于双亲委派机制,系统类就向上一直找,最终找到引导类加载器。引导类加载器判断该类处在Java开头的包下,可以进行加载,但是加载的是核心库中的String,完成加载后就成功返回不会再向下执行
  • 优点:

    • 避免了类的重复加载
    • 保护程序的安全,防止核心API被随意修改
    • 保护引导类加载器:
    // src/java/lang/Psj.java
    public class Psj {public static void main(String[] args) {System.out.println("程序正常运行");}
    }
    // 最终程序会报错,即使类名不同,使用的包名为核心库下的包名依旧不行,防止破坏引导类加载器
    

tips:

  • 加载器能否加载类要看该类是否在指定的包路径下,比如引导类加载器就会去加载Java开头的包下的类
  • 上述场景对Java核心源代码以及引导类加载器的保护称为沙箱安全机制

6.其他

  • 在JVM中表示两个Class对象是否为同一个类有两个必要条件:

    • 类的完整类名必须一致(包括包名)
    • 加载该类的ClassLoader必须相同
  • 方法区中会保存该类是由什么加载器进行加载的

  • 类的使用分为主动使用和被动使用:

    • 主动使用:

      • 创建类的实例
      • 访问访问某个类或接口的静态变量,或者对该静态变量赋值
      • 调用类的静态方法
      • 反射
      • 初始化某类的子类
      • JVM启动时被标记为启动类的类
    • 被动使用:除了以上情况,其他使用Java类的方式都是对类的被动使用,不会导致类的初始化


JVM内存和垃圾回收-02.类加载子系统相关推荐

  1. JVM内存与垃圾回收篇——类加载子系统

    概述 完整图如下: 如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢? 类加载器 执行引擎 类加载器子系统作用 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开 ...

  2. Java进阶 JVM 内存与垃圾回收篇(一)

    JVM 1. 引言 1.1 什么是JVM? 定义 Java Vritual Machine - java 程序的运行环境(Java二进制字节码的运行环境) 好处 一次编译 ,到处运行 自动内存管理,垃 ...

  3. JVM——内存与垃圾回收

    JVM--java virtual machine java虚拟机就是二进制字节码运行的环境 特点: 一次编译,导出运行 自动内存管理 自动垃圾回收 文章目录 JVM--java virtual ma ...

  4. jvm内存与垃圾回收重点总结

    文章目录 一.jvm简介 1.jvm的位置 2.JVM的整体结构 3.java代码执行流程 二.类加载子系统 1.类的加载过程 2.类加载器分类 ⭐3.双亲委派机制 三.运行时数据区及线程 四.程序计 ...

  5. JVM:内存与垃圾回收篇

    文章目录 **1.Java虚拟机** 1.Java虚拟机 Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行. 特点 一次编译,到处运行 自动内存管 ...

  6. JVM内存与垃圾回收篇——JVM与Java体系结构

    前言 作为Java工程师的你曾被伤害过吗?你是否也遇到过这些问题? 运行着的线上系统突然卡死,系统无法访问,甚至直接OOM! 想解决线上JVM GC问题,但却无从下手. 新项目上线,对各种JVM参数设 ...

  7. JVM内存与垃圾回收篇——堆

    堆 堆的核心概念 堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域. J ...

  8. 深入浅出JVM内存模型+垃圾回收算法

    文章目录 前言 JVM内存模型 1. 程序计数器(记录当前线程) 2. Java栈(虚拟机栈) 3. 本地方法栈 4. 堆 5.方法区 6.直接内存 JVM垃圾回收 垃圾判断标准 1. 引用计数法 2 ...

  9. JVM内存与垃圾回收 Day02

    类加载子系统 1 内存结构概述 2 类加载器与类的加载过程 2.1 加载阶段 2.2 链接阶段 2.3 初始化阶段 3 类加载器分类 3.1 虚拟机自带的加载器 3.2 用户自定义加载器 4 Clas ...

最新文章

  1. 关于Simple Joule Theif Curcuit 电路的两个提问
  2. 7 centos 设置jvmgc_centos7配置java环境变量
  3. 自然语言理解属于计算机应用的那个范畴,基于自然语言理解的3D场景构造研究-计算机应用技术专业论文.docx...
  4. xxx征集系统项目目标文档
  5. 【Blog.Core开源】框架集成部门权限
  6. java switch 不加 break 继续执行 下一个case(不用匹配条件) 这个设计是为什么
  7. 王思聪也救不了熊猫直播了?
  8. 第10章-Vue.js 项目实战
  9. 牛客(3)从尾到头打印链表
  10. 【优化预测】基于matlab差分进化算法优化BP神经网络预测【含Matlab源码 1315期】
  11. 图像融合论文及代码网址整理总结(2)——红外与可见光图像融合
  12. ToStringBuilder学习(一):常用方法介绍
  13. flash builder 序列号
  14. 计算机类一级学科目录是什么,教育部学科门类及一级学科目录表.doc
  15. 模2除法(CRC校验码计算)
  16. html网页设计模板
  17. 佳能Canon PIXMA MP568 一体机驱动
  18. MTCNN优化和另类用法
  19. 常用域名控制面板地址
  20. 安卓应用开发 MyWeChat(二)

热门文章

  1. android 读写文件 简书,Android 读取asset文件
  2. 股票入门(一)基础知识
  3. 微信小程序 教你如何复制页面路径 (以及京东、虎牙、苏宁、拼多多、等大厂的加密路径详解)(多图!!!)
  4. mw325r 服务器无响应),新版水星(Mercury)MW325R拨不上号怎么办?【图解】
  5. 国内外快递公司名称一览表
  6. Spring Data JDBC、引用和聚合
  7. 华为海外15年,浮生一瞬间,我的退休告别贴
  8. Boost:shared_memory_object --- 共享内存
  9. HDMI/DVI____串行发送器
  10. 面试分享:专科半年经验面试阿里前端P6+总结