(一)JVM之类加载

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

本人工作经常听到full gc、jvm调优等名词,面试基本上也是必问,so记录一下


提示:以下是本篇文章正文内容,下面案例可供参考

1.1、 学习JVM的动力

我们为什么要学习JVM呢?我们来看一下官方解释:
Java官网 :https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-1.html#jvms-1.2

精简来说

  • 如果你在线上遇到了OOM,你是否会束手无策。
  • 线上卡顿是否可能是因为频繁Full GC造成的。
  • 新项目上线,服务器数量以及配置不足,对于性能的扩展只能靠服务器的增加,而不能通过 JVM的调优达到实现服务器性能的突破。
  • 面试经常会问到JVM的一些问题,但是当面试官问到你实际的落地点时,你就会茫然不知所 措,没有条理性,或者答非所问。

1.2、 计算机体系结构

遵循冯诺依曼计算机结构
在这里插入图片描述
jvm也是遵循这个模型,它是个抽象的计算机。

1.3、编译型和解释型

1.3.1、 编译型

使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行
的机器码,并包装成该平台所能识别的可执行性程序的格式。

1.3.2、 解释型

**使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被
解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。 **

1.3.3、 java呢?

Java属于编译型+解释型的高级语言,通过jvm参数来指定编译类型,默认是编译+解释,可以纯编译或纯解释运行。

1.4、So JVM是什么

Java Virtual Machine(Java虚拟机)
Write Once Run Anywhere

在这里插入图片描述
这张图是指一次编译,随处运行,解释一下流程:

1. java源文件->javac命令变成class文件
2. class文件通过类加载器加载进jvm
3. jvm最后把class文件转换成01字节码

1.5、 jvm要学什么

在这里插入图片描述

(1)源码到类文件
(2)类文件到JVM
(3)JVM各种折腾[内部结构、执行方式、垃圾回收、本地调用等]

1.5.1、 前期编译

一个java文件,javac命令后 经过如下过程变为class文件
在这里插入图片描述
**xx.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 **
-> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> xx.class文件

2.1、class文件

在这里插入图片描述
javac之后的class文件长这个样子,看到是不是很崩溃,不过这个不是重点,作为了解就可以。有几点要注意:

  1. class文件以cafe babe开头。
  2. 要解读class文件,要配合查官方文档,这是个不难但是麻烦的活。

2.1.1、class文件分析

用javap -v -p **.class对class文件进行反编译,查看字节码和指令信息。
在这里插入图片描述在这里插入图片描述

在这里插入图片描述

2.2.1、类文件到虚拟机(类加载机制)

类加载机制是指我们将类的字节码文件所包含的数据读入内存,同时我们会生成数据的访问入口的一种特殊机制。那么可以得知,类加载的最终产品是数据访问入口。
在这里插入图片描述

类加载方式:

  • 从本地系统中直接加载
  • 通过网络下载.class文件 典型场景:Web Applet,也就是我们的小程序应用
  • 从zip,jar等归档文件中加载.class文件 典型场景:后续演变为jar、war格式
  • 从专有数据库中提取.class文件 典型场景:JSP应用从专有数据库中提取.class文件,较为少见
  • 将Java源文件动态编译为.class文件,也就是运行时计算而成 典型场景:动态代理技术
  • 从加密文件中获取 典型场景:典型的防Class文件被反编译的保护措施。

3.1类加载流程

在这里插入图片描述
所谓类加载机制就是

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

3.1.1、装载

java有这么一段代码模块。可以实现通过类全名来获取此类的二进制字节流这个动作,并且将这个动作放到放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类,实现这个动作的代码模块成为“类加载器”。

就是说java通过“类加载器”来加载class进jvm内存。

查找和加载class文件流程:

1. 通过一个类的全限定名获取定义此类的二进制字节流(由上可知,我们不一定从字节码文件中获得,还有上述很多种方式)
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
**

3.1.2、链接

3.1.2.1、验证

验证只要是为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求我们的信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。

  • 文件格式验证

1.是否以16进制cafe babe开头 2.版本号是否正确(cafe babe后面的16进制数字)

  • 元数据验证

对类的元数据信息进行语义校验(其实就是对Java语法校验),保证不存在不符合Java语法规范的元数据信息。

举例:
1.是否有父类
2.是否继承了final类

  • 字节码验证

进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。获取类的二进制字节流的阶段是我们JAVA程序员最关注的阶段,也是操控性最强的一个阶段。因为这个阶段我们可以对于我们的类加载器进行操作,比如我们想自定义类加载器进行操作用以完成加载,又或者我们想通过JAVA Agent来完成我们的字节码增强操作。

3.1.2.2、准备

为类的静态变量分配内存,并将其初始化为默认值
private final static int a = 1 ; a = 0;
在这里插入图片描述

  • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
  • 这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量 是会随着对象一起分配到Java堆中

3.1.2.3、解析

把类中的符号引用转换为直接引用
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进 行。
直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中

对解析结果进行缓存

同一符号引用进行多次解析请求是很常见的,除invokedynamic指令以外,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用解析请求就应当 一直成功;同样的,如果第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的 异常

简单理解:class文件的一段符号引用,如果解析成功,进行缓存,比如有个耗时的操作,for循环100W次,解析完后缓存,后面就不用再解析,提高效率。

3.1.2、初始化

初始化阶段是执行类构造器()方法的过程。

在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,比如赋值。

在Java中对类变量进行初始值设定有两种方式:
声明类变量是指定初始值
使用静态代码块为类变量指定初始值

JVM初始化步骤:

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

3.1.2、使用

那么这个时候我们去思考一个问题,我们的初始化过程什么时候会被触发执行呢?或者换句话说类初始化时机是什么呢?
主动引用
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种 :

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName(“com.carl.Test”))
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(JvmCaseApplication ),直接使用java.exe 命令来运行某个主类

被动引用

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
  • 定义类数组,不会引起类的初始化。
  • 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)。

3.1.2、卸载

在类使用完之后,如果满足下面的情况,类就会被卸载:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。但是一般情况下启动类加载器加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有在极少数情况下才会被卸载。

4.1、类加载器ClassLoader

在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。

4.2、什么是类加载器?

  • 负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例的代码模块。
  • 类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。

一个类在同一个类加载器中具有唯一性(Uniqueness),而不同类加载器中是允许同名类存在的,这里的同名是指全限定名相同。但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过
instanceOf 、equals 等方式的校验。

4.3、分类

  1. Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar
    里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。如果在程序里去查看引用会是null
  2. Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包
  3. App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包
  4. 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader
    在这里插入图片描述

4.4、JVM类加载机制的三种特性

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所
依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。
显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。
以上步骤只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载class
字节码文件生成Class对象由“双亲委派”机制完成。

  • 父类委托,“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

父类委托别名就叫双亲委派机制。
“双亲委派”机制加载Class的具体过程是:

  • ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托
    给父类加载器。
  • 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给
    祖父类加载器。
  • 依此类推,直到始祖类加载器(引用类加载器)。
  • 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试
    从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果
    载入失败,则委托给始祖类加载器的子类加载器。
  • 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载
    入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。
  • 依此类推,直到源ClassLoader。
  • 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。

“双亲委派”机制只是Java推荐的机制,并不是强制的机制。
我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就 应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法

  • 缓存机制,缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用

总结

理解类加载流程,对照着看每个点,就差不多了
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值