Java技术专题-JVM研究系列(28)重塑你对类加载机制的认识
📕 每日一句
极限就是为了超越而存在的,如何挑战自己的极限,只能苦练!
📕 为什么又要写类加载器?
为什么有些一篇相关与对类加载器的文章?个人觉得之前的侧重点在于ClassLoader本身,以及双亲委托机制,而本篇更多站在JVM虚拟机的层面上去讲述和描述,前者侧重于使用和实际,后者本篇注重于原理和深入分析。
📕 类加载的时机与作用
一段java程序在被执行的过程中,需要经历以下几个阶段:
📕 编译阶段
编译器将源码文件编译成class文件。
class文件是.java文件的二进制字节流表示,在class文件中,包含了对应的类或接口的定义信息等常量池数据。
-
内部存放的数据有:元数据常量池,访问标志,当前类索引、父类索引和接口索引的集合,字段表集合(类中声明的变量),方法表集合等,他们共同描述了一个类的信息。
-
每个class文件一定对应一个类,但反过来未必成立,例如,动态生成的类信息,直接生成二进制字节流送入类加载器完成类加载。
-
因此广义上来讲,class并不一定要是一个class文件,也可以仅仅就是一串二进制字节流。
📕 类加载阶段
-
class文件本质上是对某个类的静态描述,他需要被加载到内存,转化成运行时数据,才能被虚拟机执行,这个加载到内存的过程就是类加载过程。
-
类加载完成之后,在方法区内存放了类的类型信息、常量、静态变量(jdk8之后随类对象存储在堆内)等信息,在堆中存放了与class文件对应的Class对象。通过Class对象,可以获取到类的字段、方法、构造器等信息,它是反射的基础。
📕 类加载的作用
类加载在程序执行的过程中起到了承上启下的作用,将静态的二进制字节流数据转化为了运行时数据,供执行引擎去操作数据。
如图,加载-验证-准备-解析-初始化这五个阶段都属于类加载过程。
📕 类加载的时机
虚拟机规范并没有严格规定什么时候开始类加载。但是,规定了6种必须对类进行初始化的情况,它们被称为主动引用。
!!!由于初始化类对象需要在加载、验证、准备之后进行,因此这三步必然要在这之前完成。这里前4种是非常常见的,需要深刻掌握。
遇到
new, getstatic, putstatic, invokestatic
这四条字节码指令的时候,如果类型还没有被初始化,则需要初始化。
-
new :实例化对象(对象实例调用表达式所创建的对象)
-
getstatic/putstatic: 读取/设置类的静态字段(被final修饰的静态常量除外)
-
invokestatic: 调用类的静态方法
📕 其他的初始化条件
-
unsafe方法进行调用对象操作
-
clone方法进行操作,进行申请
-
通过文件IO的ObjectInputStream/ObjectOutputStream进行处理构造
-
通过反射对类进行调用的时候,需要确保类已经被初始化过。也好理解,反射的核心是Class对象。
-
当前类被初始化时,要先确保其父类已被初始化。
-
虚拟机启动时,要执行的主类(包含main方法的那个类)要先被初始化。
-
接口中定义了默认方法(被default修饰,可以有方法体的方法,比较少见),当该接口的实现类初始化时,该接口需要先被初始化。
除了以上的情况之外,所有其他对类的引用都不会触发类的初始化,他们被称为被动引用。
📕类加载的过程
加载
-
加载是类加载过程的第一步,在类加载器的控制下,将二进制字节流转化为运行时数据。加载阶段需要完成3件事。
-
根据类的全限定名获取对应的二进制字节流/定义对应的二进制数据流。
-
将二进制字节流转化成方法区的运行时数据结构。
-
在堆中创建代表这个类的java.lang.Class对象 ,作为方法区内数据的访问入口。
-
-
这里二进制字节流的获取,有多种方式,源文件也可以有多种形式。比较常见的形式有:
-
从压缩包中获取,如jar包,war包等。
-
在程序运行时,动态计算产生。应用场景:动态代理。
-
最常见的,编译.java文件生成.class文件
-
验证
验证的作用是确保Class文件内的信息符合虚拟机规范的要求,保证程序运行过程中的安全。
准备
为类变量(即静态变量)分配内存,并设初始值。(0, null, false ...)。
有两点需要留意:
-
从概念上讲,应当在方法区内为静态变量赋初值(常量会执行定制化赋值,不是单纯的默认值),但实际上jdk8以后,静态变量随着类对象一起存放在堆内存中。
-
准备阶段并不会为非静态变量(即实例变量)分配内存,实例变量会在对象实例化的时候,分配内存并赋初始。
解析
将运行时常量池中符号引用替换成直接引用。
举个例子,在解析完成之前,被引用的目标还没有被加载到内存中,只能先用一个符号来表示,如"java.lang.Object"。
-
解析的作用就是,在引用的对象被加载到内存中以后,将引用替换成指向该对象的指针或句柄。需要被解析的引用有:类或接口的解析、字段解析、方法解析、接口方法解析。
-
解析的发生时间并没有严格规定,它并非一定发生在准备和初始化之间(动态链接或者动态加载、动态分派等功能实现的场景会延迟到运行阶段)。
初始化
-
在初始化之前,加载-验证-准备这3步必然是完成了,部分的解析工作也可能完成了。
-
准备阶段:对类对象中的类变量都是系统默认的初始值(常量会直接赋值,不会等到运行阶段)。
-
初始化阶段:对类变量赋予我们在代码中指定的值。
-
在初始化阶段,需要执行类构造器(与实例对象的构造器区分开来)。类构造器并非我们直接编写的方法,而是编译器收集类变量的赋值语句和static代码块的产物。
初始化阶段就是对静态变量赋值和执行静态代码块的过程(父类会在前,子类会在后)。
实例化阶段就是执行实例构造器和实例代码块(构造代码块)的过程。
📕 执行指令(不是重点)
在这个阶段,字节码执行引擎根据指令,去操作内存中的数据,完成计算任务.

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
一种基于实时分位数计算的系统及方法
导读:性能分析等场景对实时分位数有强烈诉求。在计算累计时长时,可以将不同时间段的时长简单相加,而分位数却无法先计算不同维度下的分位值,然后对其直接聚合,该特性对实时计算带来了较大挑战。我们基于TDigest数据结构,利用Redis和Doris等高性能存储,预先计算所有可能查询的分位值指标,既可以快速计算指标,同时可以保障查询效率。该系统已经对百度内内核性能、网络性能等业务场景进行输出,并能有效满足业务高时效的分析需求。 全文3663字,预计阅读时间10分钟。 一、问题描述与技术挑战 在实际工作中,我们发现许多业务场景中都有对某一数值型指标实时统计分位数的需求,一般要求计算结果有很高准确率同时具备极低的计算延迟,实现这类需求给数据RD的开发工作带来一定的挑战,其中主要的技术挑战包括以下三个方面: 无法对全量数据进行排序:由于在实时计算场景中是逐条处理数据的,无法对全量数据排序,进而无法获得全量数据的分位数 计算逻辑复杂,计算延迟高:即时在能够排序的场景中,高复杂度的排序操作也会带来很高的计算延迟,无法满足实时计算的低延迟要求 分位数结果无法聚合:两个计算得出的分位数结果无法像求和结果那样...
-
下一篇
为什么一段看似正确的代码会导致DUBBO线程池被打满
欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习 1 一个公式 之前我们在一个公式看懂:为什么DUBBO线程池会打满这篇文章中分析了为什么DUBBO线程池为什么会打满,在本文开始时我们不妨先回顾这个公式:一个公司有7200名员工,每天上班打卡时间是早上8点到8点30分,每次打卡系统耗时5秒。请问RT、QPS、并发量分别是多少? RT表示响应时间,问题已经告诉了我们答案: RT = 5 QPS表示每秒查询量,假设签到行为平均分布: QPS = 7200 / (30 * 60) = 4 并发量表示系统同时处理的请求数量: 并发量 = QPS x RT = 4 x 5 = 20 根据上述实例引出如下公式: 并发量 = QPS x RT 如果系统为每一个请求分配一个处理线程,那么并发量可以近似等于线程数。基于上述公式不难看出并发量受QPS和RT影响,这两个指标任意一个上升就会导致并发量上升。 但是这只是理想情况,因为并发量受限于系统能力而不可能持续上升,例如DUBB...
相关文章
文章评论
共有0条评论来说两句吧...