JVM加载机制详解

一. 类加载子系统

  1. 类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识。
  2. 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
  3. ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
  4. 如果调用构造器实例化对象,则该对象存放在堆区;

1.1 类加载器ClassLoader角色

  1. class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
  2. class file 加载到JVM中,被称为DNA元数据模板。
  3. 在 .class文件 --> JVM --> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。

1.2 类加载的执行过程

类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)

加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。接下来讲解加载、验证、准备、解析、初始化五个步骤,这五个步骤组成了一个完整的类加载过程。使用没什么好说的,卸载属于GC的工作 。

1.2.1 加载

加载阶段做了三件事情

  1. 获取.class文件的二进制流
  2. 将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
  3. 在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的

加载是类加载的第一个阶段。有两种时机会触发类加

预加载

虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时常用到的,像java.lang.*、java.util.、java.io. 等等,因此随着虚拟机一起加载

运行时加载

虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的 全限定名来加载这个类。

验证

Java语言本身是相对安全的语言(相对C/C++来说),.class文件未必要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件。在字节码语言层面上,Java代码至少从语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证阶段将做一下几个工作

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备

准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。关于这 点,有两个地方注意一下:

  1. 这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
  2. 这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value =123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。

解析

解析阶段负责把整个类激活,串成一个可以找到彼此的网,大体可以分为:

  • 类或接口的解析
  • 类方法解析
  • 接口方法解析
  • 字段解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用和直接引用有什么区别:

符号引用 符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。 这个其实是属于编译原理方面的概念,符号引用包括了下面三类常量:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符

那其实总而言之,符号引用和我们上面讲的是一样的,是对于类、变量、方法的描述。符号引用和虚拟机的内存布 局是没有关系的,引用的目标未必已经加载到内存中了。

直接引用 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的 内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用, 那引用的目标必定已经存在在内存中了。

初始化

类的初始化阶段是类加载过程的最后一个步骤, 之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码, 将主导权移交给应用程序。

初始化阶段就是执行类构造器()方法的过程。()并不是程序员在Java代码中直接编写 的方法, 它是Javac编译器的 自动生成物,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的 语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问 到定义在静态语句块之 前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问。

()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法不同, 它不需要显 式地调用父类构造器,Java虚拟机会保证在子类的()方法执行前,父类的()方法已经执行 完毕。 因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。由于父类的()方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

二. 类加载器

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

2.1 类加载器分类

  1. jvm支持两种类型的加载器,分别是引导类加载器和自定义加载器
  2. 引导类加载器是由c/c++实现的,自定义加载器是由java实现的。
  3. jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。
  4. 按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)

启动类加载器

  • 1、这个类加载器使用c/c++实现,嵌套再jvm内部
  • 2、它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
  • 3、并不继承自java.lang.ClassLoader,没有父加载器

扩展类加载器

  • 1、java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 2、从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader。
  • 3、父类加载器为启动类加载器

系统类加载器

  • 1、java语言编写,由 sun.misc.Lanucher$AppClassLoader实现
  • 2、该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库;派生于 ClassLoader
  • 3、父类加载器为扩展类加载器 4、通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。

用户自定义类加载器

在日常的Java开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的 加载方式。

三. 双亲委派模型

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException ),子加载器才会尝试自己去加载。

双亲委派意义

为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:

黑客自定义一个java.lang.String 类,该String 类具有系统的String 类一样的功能,只是在某个函数稍作修改。比如equals 函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM 中。此时,如果没有双亲委派模型,那么JVM 就可能误以为黑客自定义的java.lang.String 类是系统的String 类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加 载器加载系统的java.lang.String 类,最终自定义的类加载器无法加载java.lang.String 类。

四. 自定义加类加载器

实现方式: 所有用户自定义类加载器都应该继承ClassLoader类 在自定义ClassLoader的子类是,我们通常有两种做法:

  • 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
  • 重写findClass方法 (推荐)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值