Java虚拟机三:JVM的类加载机制

1.什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

å«ç¿»äºï¼è¿ç¯æç« ç»å¯¹è®©ä½ æ·±å»ç解javaç±»çå è½½æºå¶

需要注意的点:

  • 类加载器并不需要等到某个类被“首次主动使用”时再加载它JVM规范允许类加载器在预料某个类将要被使用时就预先加载它如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
  • Class对象是存放在堆区的,不是方法区。类的元数据才是存在方法区的。类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的。
  • JDK8移除了永久代,转而使用元空间来实现方法区,创建的Class实例依旧在java heap(堆)中

编写一个新的java类时,JVM就会帮我们编译成class对象,存放在同名的.class文件中。在运行时,当需要生成这个类的对象,JVM就会检查此类是否已经装载内存中。若是没有装载,则把.class文件装入到内存中。若是装载,则根据class文件生成实例对象。创建对象的过程其实包含两个部分:第一部分将class文件进行类加载过程。第二部分创建对象。

2.class文件类加载过程

类从磁盘加载到内存中经历的三个阶段,加载、连接、与初始化 【重点】,其中连接包含:验证、准备、解析3 个阶段。

Javaè¿é¶æç¨ä¹JVMçç±»å è½½æºå¶

①加载:查找并加载类的二进制数据(把class文件里面的信息加载到内存里面)

检测类是否被加载过,jvm会先去方法区中找有没有相应的.class类结构信息存在,如果有直接使用,如果没有,则把相应的类的class加载到方法区。

连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。三个阶段说明

验证:对加载的类进行验证,确保被加载的类的正确性

③准备【重点】正式为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。

这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

  • 内存分配的对象:要明白首先要知道Java 中的变量有类变量以及类成员变量两种类型,类变量指的是被 static 修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM 只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始(初始化阶段下面会讲到)。

举个例子:例如下面的代码在准备阶段,只会为 LeiBianLiang属性分配内存,而不会为 ChenYuanBL属性分配内存。

public static int LeiBianLiang = 666;

public String ChenYuanBL = "jvm";

  • 初始化的类型:在准备阶段,JVM 会为类变量分配内存,并为其初始化(JVM 只会为类变量分配内存,而不会为类成员变量分配内存,类成员变量自然这个时候也不能被初始化)。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的默认值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,LeiBianLiang 的值将是 0,而不是 666。

public static int LeiBianLiang = 666;

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,ChangLiang的值将是 666,而不再会是 0。

public static final int ChangLiang = 666;

原因:而 final 关键字在 Java 中代表不可改变的意思,意思就是说 ChangLiang的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

④解析:解析阶段就是jvm将常量池的符号引用替换为直接引用。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可

直接引用:指向该类的该方法在方法区中的内存位置的指针

简单的来说就是我们编写的代码中,当一个变量引用某个对象的时候,这个引用在.class文件中是以符号引用来存储的。在解析阶段就需要将其解析为直接引用。如果有了直接引用,那引用的目标必定已经在内存中存在。

⑤初始化【重点】

初始化阶段,才真正开始执行类中定义的java程序代码。主要有以下步骤:

  1. 为类的静态变量赋予正确的初始值。
  2. 执行类的静态代码块。

按照顺序自上而下运行类中的变量赋值语句和静态语句,并且只有类或接口被Java程序首次主动使用时才初始化他们。如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。原则:先加载父类在加载子类,先加载静态在加载非静态。

类的主动使用包括以下六种【重点】:

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

2、 访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰不不不其实更准确的说是在编译器把结果放入常量池的静态字段除外)

3、 调用类的静态方法

4、 反射(如 Class.forName(“com.gx.yichun”))

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

6、 Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会首先被初始化

⑥使用:当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

⑦卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

3、创建对象过程

上面分析类加载的过程,前五个阶段完成了类的加载,加载和初始化针对类变量即静态变量和静态方法,本部分将以new对象的过程进行详细说明:

类的加载过程,以Person person = new Person()为列进行说明:

①检测类是否被加载,JVM会先去方法区找有没有相应的Person.class类存在,如果有直接使用。如果没有,则把相应的类加载到JVM中(加载到方法区)。

②进行验证(检查)、准备(类变量分配内存以及初始化)、解析(符号引用替换为直接引用->指向方法区内存位置)、初始化(类变量真正赋值),完成类的加载过程。

③类加载完之后,在堆内存中开辟空间分配内存地址。

④将分配到的内存空间中的数据类型都 初始化为零值(不包括对象头)

目的:确保对象的实列在Java代码中可以不赋初始值就可以直接使用,程序能访问这些字段的零值

虚拟机要对 对象头进行必要的设置 ,例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。

调用对象的init()方法 ,根据传入的属性值给对象属性赋值。

⑦在线程 栈中新建对象引用 ,并指向堆中刚刚新建的对象实例。

注意:虚拟机 为新生的对象分配内存 目前常用的有两种方式,根据使用的垃圾收集器的不同使用不同的分配机制:

  • 指针碰撞(Bump the Pointer):假设Java堆的内存是绝对规整的,所有用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
  •  空闲列表(Free List):如果Java堆中的内存并不是规整的,已使用的内存和空间的内存是相互交错的,虚拟机必须维护一个空闲列表,记录上哪些内存块是可用的,在分配时候从列表中找到一块足够大的空间划分给对象使用。

4、相关概念

双亲委派模型

当一个类加载器收到类加载请求时,它首先不会自己去加载这个类的信息,而是把该请求转发给父类加载器,将类加载请求向上传递。所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中没有找到所需的类,子类加载器才会自己尝试去加载该类。

当前类加载器和所有父类加载器都无法加载该类时,抛出ClassNotFindException异常。

Javaè¿é¶æç¨ä¹JVMçç±»å è½½æºå¶

为什么要自定义类加载器?

  1. 可以从指定位置加载class文件,比如说从数据库、云端加载class文件
  2. 加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

 

文章参考:

https://www.cnblogs.com/gjmhome/p/11401397.html

https://www.toutiao.com/a6757598325442085384/?timestamp=1585356521&app=news_article&group_id=6757598325442085384&req_id=202003280848400101290320760C68F5F6

https://www.toutiao.com/a6767005022854054403/?timestamp=1585357983&app=news_article&group_id=6767005022854054403&req_id=20200328091302010129026037166BFB92

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章