前言
了解dubbo的时候,因为SPI机制用到了动态代理的机制,从而涉及到了类加载机制相关的东西,整个概念也属于非常底层的逻辑,也好久没整理了,现整理一下,便于后续翻阅。
尽可能的关联JVM相关的知识点,如果读者有补充的可以留言补充。
类加载机制概念
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。*
Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能,这里就是我们经常能见到的Class类。
类加载过程
image.png类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。主要要经过以下步骤:
- (1) 装载:查找和导入Class文件;
- (2) 链接:把类的二进制数据合并到JRE中;
- (a)校验:检查载入Class文件数据的正确性;
- (b)准备:给类的静态变量分配存储空间;
- (c)解析:将符号引用转成直接引用;
- (3) 初始化:对类的静态变量,静态代码块执行初始化操作
1.加载
类的装载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区
内,然后在堆区
创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
加载.class文件的方式有:
- 1). 从本地系统中直接加载
- 2). 通过网络下载.class文件
- 3). 从zip,jar等归档文件中加载.class文件
- 4). 从专有数据库中提取.class文件
- 5). 将Java源文件动态编译为.class文件
2.验证
验证的目的是为了确保class文件中的字节流包含的信息流符合虚拟机的规范,因此,不同的虚拟机可能有不同的实现,大致可分为以下几步:
- 1)文件格式的验证:验证字节流是否符合Class文件格式的规范,经过该阶段的验证后,字节流才会进入内存的
方法区
中进行存储,后面的三个验证都是基于方法区
的存储结构进行的。 - 2)元数据验证:对类中的各数据类型进行语法校验,保证不存在不符合Java语法规范的元数据信息。
- 3)字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
- 4)符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候,主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
3.准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
- 1)这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2)这里所设置的初始值通常情况下是数据类型默认的初始值值(如0、0L、null、false等),具体的赋值是在初始化过程中,而常量是在这个时候进行赋值的。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 1)、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
- 2)、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
- 3)、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
- 4)、接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。
初始化
类初始化阶段是类加载过程的最后一步,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- ①声明类变量时指定初始值
- ②使用静态代码块为类变量指定初始值
以上也可知,方法区
存储了静态变量
、类信息
堆
中存储了类变量
(局部变量
)
扩展:即时编译器(JIT Just In Time)
编译后的一些热点代码也会存放在方法区
常量
数据在编译访问常量的代码时才会放入方法区
中。
由上面介绍可以知道,方法区
存放着各线程都可用的数据,因此是线程共享的。
类的实例化
类实例化的一般过程是:
父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数->静态代码块->方法。
在类的实例化过程中,首先会在虚拟机栈
中,保存实例对象的引用,具体对象的属性实例会存放在堆
中。
扩展:这里也可以理解垃圾的生成
Test testA = new Test();
Test testB = new Test();
testA.setName("a");
testB.setName("b");
testB = testA;
以上伪代码可解释,当引用关系变化时,原有testB.setName("b");
所对应的堆中的内存就可以当做垃圾回收。
堆
除了对象实例,还包括数组
,所以这也解释为什么会报java.lang.OutOfMemoryError: Requested array size exceeds VM limit
方法调用
当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。
将局部变量表(Local Variables)
、操作数栈(Operand Stack)
、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)
、方法返回地址(Return Address)和一些额外的附加信息
压入虚拟机栈
中。
这也就解释了为什么递归过多会导致StackOverflowError
,因为会不断的往栈中压入方法返回地址
。
这部分是在线程执行方法时生成,故也可以知是线程私有的。
扩展:程序计数器存储当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。因此也是线程私有的。
JVM内存结构
image.png堆
堆的作用是存放对象实例
和数组
。从结构上来分,可以分为新生代
和老年代
。而新生代
又可以分为Eden 空间
、From Survivor 空间(s0)
、To Survivor 空间(s1)
。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor
的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden
复制过来的对象,和从前一个Survivor
复制过来的对象,而复制到老年代的只有从第一个Survivor
区过来的对象。而且,Survivor
区总有一个是空的。
如图:-Xms
设置堆的最小空间大小。-Xmx
设置堆的最大空间大小。-XX:NewSize
设置新生代最小空间大小。-XX:MaxNewSize
设置新生代最小空间大小。
方法区
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,也有人把方法区称为“永久代”(Permanent Generation),在Java8中永生代彻底消失了。
如图:-XX:PermSize
设置最小空间 -XX:MaxPermSize
设置最大空间。
方法栈
每个线程会有一个私有的栈。每个线程中方法的调用又会在本栈中创建一个栈帧。
如图:-Xss
控制每个线程栈的大小。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。
如图:-Xss
控制每个线程的大小。
网友评论