热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

类加载机制Java虚拟机第二步

类加载机制概述本文所有代码和介绍,基于JDK1.8.0.25还是放上这个最眼熟的图,

类加载机制概述


本文所有代码和介绍,基于 JDK 1.8.0.25


还是放上这个最眼熟的图,这个针对 hotspot 虚拟机所绘制的简图:

JVM运行时数据区

本文要介绍的就是这个图中的 类加载器 ,主要内容包括类加载器的工作步骤,内部组成等。

对于类加载器的内部结构,先看下面这个图:

类加载子系统内部结构

类加载器会把 .class 字节码加载到运行时数据区的方法区。

除了类的信息外,方法区还存放着运行时常量池信息(版本、字段、方法、接口啥的)。


class 文件生命周期

从上面的图中可以看出:


.class 文件在类加载器的执行过程包括了 加载验证准备解析初始化 五个阶段。


再加上运行时数据区、执行引擎和最后消亡,根据顺序如下图所示:

class文件生命周期


Loading:加载阶段

加载阶段有不同的类加载器,当然也可以自定义类加载器。

不过这是在自建虚拟机时、或者大佬优化虚拟机才搞的,我们就知道还能自定义就行了。

类的加载分为以下步骤:



  1. 通过一个类的全限定名获取定义此类文件的二进制字节流;

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

Class加载步骤简图

获取 .class 文件的几种方式:



  • 从本地系统中直接加载

  • 通过网络下载 .class 文件,这种场景最典型的应用就是 Web Applet

  • 从 zip,jar, war 等归档文件中加载 .class 文件

  • 运行时计算生成,使用最多的是动态代理技术。在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass() 来为特定接口生成形式为 “*$Proxy” 的代理类的二进制字字符流

  • 从专有数据库中提取 .class 文件

  • 从其他文件生成,典型应用场景就是 JSP ,由 JSP 文件生成 Class 文件


Linking:链接阶段

主要包括以下三个阶段:



  • 验证: 确保被加载的类的正确性



    1. 目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。



    2. 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。



























      验证方式具体验证内容和措施
      文件格式验证验证字节流是否符合 Class 文件格式的规范;
      1. 是否以0xCAFEBABE开头
      2. 主、次版本号是否在当前虚拟机的处理范围之内
      3. 常量池中的常量是否有不被支持的类型
      4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
      5. Class 文件各个部分及文件本身是否有被删除的或附加的其他信息
      元数据验证对字节码描述的信息进行语义分析,保证描述信息符合规范要求,
      1. 这个类是否有父类,除了java.lang.Object之外
      2. 这个类的父类是否继承了不允许被继承的类(final 修饰)
      3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
      字节码验证通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
      符号引用验证确保解析动作能正确执行。



    验证阶段非常重要,这个阶段直接决定了 JVM 是否能承受恶意代码的攻击;这个阶段占用了整个类加载阶段的大量时间。

    但是验证阶段不是必须的,它对 JVM 运行没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。






  • 准备: 为类的静态变量分配内存,并将其初始化为默认值



    1. 为类变量分配内存并且设置该类变量的默认初始值,即零值。

    2. 这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配了,准备阶段会显式初始化。

    3. 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。(因为这时候还没创建对象)

    例如以下代码:

    private static int x = 1;

    在准备阶段,它只会被赋值为0,在初始化阶段才会赋值为1.


    不同的类型零值不同:











































    数据类型零值数据类型零值
    int0booleanfalse
    long0Lfloat0.0f
    short(short)0double0.0d
    char‘\u0000’referencenull
    byte(byte)0




  • 解析: 把类中的符号引用转换为直接引用

    1. 将常量池内的符号引用转换为直接引用的过程。

    2. 事实上,解析操作往往会伴随着JM在执行完初始化之后再执行。

    3. 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

    4. 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、 CONSTANT_Fleldref_info、 CONSTANT_Methodref_info等。




Initialization:初始化阶段


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

这个方法是 javac 编译器自动收集类中的所有变量的赋值动作和静态代码块中的语句合并而成的。


编译器收集顺序和语句在源文件中出现顺序相同

所以意思就是,static 代码块在初始化阶段就已经执行了。


使用和卸载

使用时,类访问方法区内的数据结构的接口, 对象是 Heap 区的数据。

Java虚拟机将结束生命周期的几种情况



  • 执行了 System.exit() 方法

  • 程序正常执行结束

  • 程序在执行过程中遇到了异常或错误而异常终止

  • 由于操作系统出现错误而导致Java虚拟机进程终止


类加载器的种类和特点

在上面的加载阶段图中,已经画出了: 启动(引导)类加载器扩展类加载器应用类加载器 ,还说明了可以 自定义加载器

但是,但是,Java 虚拟机规范只把加载器分为了两种:启动类加载器(C++语言实现的,很特别哦)和自定义类加载器。

它把派生于 public abstract class ClassLoader 的加载器都归类为了自定义类加载器。

类加载器层次结构

这个结构层次图也被成为 双亲委派模型 ,具体工作原理后面再讲,先看几个加载器的作用:



  1. 启动类加载器 : Bootstrap ClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

  2. 扩展类加载器 : Extension ClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器 : Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:



  • 在执行非置信代码之前,自动验证数字签名。

  • 动态地创建符合用户特定需要的定制化构建类。

  • 从特定的场所取得java class,例如数据库中和网络中。


ClassLoader 体验

先做个小实验:

public class JvmTest1 {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
System.out.println(systemClassLoader.getParent());
System.out.println(systemClassLoader.getParent().getParent());
ClassLoader classLoader = JvmTest1.class.getClassLoader();
System.out.println(classLoader);
}
}

输出效果:

sun.misc.Launcher$AppClassLoader@58644d46
sun.misc.Launcher$ExtClassLoader@66d3c617
null
sun.misc.Launcher$AppClassLoader@58644d46

可见我们写的代码,都是默认使用系统类加载器进行加载。

在 Class 源码中,获取 ClassLoader 是通过 native 方法:

public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
native ClassLoader getClassLoader0();


类加载器双亲委派机制


双亲委派模型工作流程为:类加载器收到加载请求时,会首先委派父加载器去加载,逐层向上委派直到父加载器反馈无法完成加载,才由子加载器完成加载。


protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

双亲委派优势



  • 系统类防止内存中出现多份同样的字节码

  • 保证Java程序安全稳定运行


参考文章

https://www.pdai.tech/md/java/jvm/java-jvm-classload.html

https://www.bilibili.com/video/BV1PJ411n7xZ



推荐阅读
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
author-avatar
hongxiaochen8846_792
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有