Sean's Blog


  • Home

  • Tags

  • Categories

  • Archives

  • Search

MySQL问题总结-SHOW PROCESSLIST

Posted on 2019-09-05 | In MySQL问题总结

以下两条指令可以用来查看当前库有哪些正在运行的线程

1
2
SHOW [FULL] PROCESSLIST;
SELECT * FROM information_schema.processlist;
  1. 如果操作员有PROCESS权限,则可以查看所有的线程,否则只能查看当前用的线程。

  2. 如果没有使用FULL关键字,只能查看Info字段的前100个字符。

操作将会显示以下字段

字段 含义
id ID标识,要kill一个语句使用此字段
user 使用线程的用户
host 使用线程的IP和端口号
db 数据库名
command 连接状态,主要有休眠(sleep)、查询(query)、连接(connect)
time 连接持续时间,单位是秒
state 显示当前sql语句的状态
info 显示这个sql语句

state 主要状态和描述

状态 描述
Checking table 正在检查数据表(自动)
Closing tables 正在将表中修改的数据刷新到磁盘中,同时正在关闭已经用完的表。这是一个很快的操作,如果不是这样的话,应该确认磁盘空间是否已经满了或者磁盘是否正处于重负中
Connect Out 复制从服务器正在连接主服务器
Copying to tmp table on disk 由于临时结果集大于tmp_table_size.正在将临时表从内存存储转为磁盘存储以节省内存
Creating tmp table 正在创建临时表以存放部分查询结果
deleting from main table 服务器正在执行多表删除中的第一部分,刚删除第一个表
deleting from reference tables 服务器正在执行多表删除中的第二部分,正在删除其他表的记录
Flushing tables 正在执行FLUSH TABLES,等待其他线程关闭数据表
Killed 发送了一个kill请求给某线程,那么这个线程将会检查kill标志位,同时会放弃下一个kill请求。MySQL会在每次的主循环中检查kill标志位,不过有些情况下该线程可能会过一小段时间才能关闭。如果该线程被其他线程锁住,那么kill请求会在锁释放时马上生效
Locked 被其他查询锁住了
Sending data 正在处理SELECT查询的记录,同时正在把结果发送给客户端
Sorting for group 正在为GROUP BY做排序
Sorting for order 正在为ORDER BY做排序
Opening tables 正在尝试打开一个表,这个过程应该会很快,除非收到其他因素的干扰。例如,在执行ALTER TABLE或LOCK TABLE语句行完之前,数据表无法被其他线程打开。
Removing duplicates 正在执行一个SELECT DISTINCT方式的查询,但是MySQL无法在前一个阶段优化掉那些重复的记录。因此MySQL需要再次去掉重复的记录,然后再把结果发送给客户端。
Reopen table 获得了对一个表的锁,必须在表结构修改后才能获得这个锁,已经释放锁,关闭数据表,正尝试重新打开数据表。
Repair by sorting 修复指令正在排序以创建索引
Repair with keycache 修复指令正在利用索引缓存一个一个地创建新索引。它会比Repair by sorting慢些。
Searching rows for update 正在将符合条件的记录找出来用于更新,一定在UPDATE要修改相关记录前就完成
Sleeping 正在等待客户端发送新请求
System lock 正在等待取得一个外部的系统锁。如果当前没有运行多个mysqld服务器同时请求一个表,可以通过增加–skip-external-locking来禁止外部系统锁
Upgrading lock INSERT DELAYED正在尝试取得一个锁以插入新记录
Updating 正在修改匹配的记录
User Lock 正在等待GET_LOCK()
Waiting for tables 线程得到通知,数据表结构已经被修改了,需要重新打开数据表以取得新的结构。为了重新打开数据表,必须等到所有其他线程关闭这个表。以下几种情况会产生这个通知:FLUSH, ALTER,RENAME,REPAIR,ANALYZE,OPTIMIZE
waiting for handler insert INSERT DELAYED已经处理完了所有待处理的插入操作,正在等待新的请求

linux问题总结-linux时间调整

Posted on 2019-08-29 | In linux问题总结

修改时间说明

有时需要修改服务器的时间来测试某些定时任务,具体操作手段如下:

查看当前时间及时区

1
date -R

修改日期为2019年08月24日

1
date -s 08/24/2019

修改时间为下午18:00:00

1
date -s 18:00:00

将当前时间写入BIOS,避免重启后失效

1
hwclock -w

或

1
hwclock --systohc

查看BIOS时间

1
hwclock --show

用硬件时钟同步系统时钟

1
hwclock --hctosys

jvm字节码执行引擎

Posted on 2019-08-13 | In jvm

概述

执行引擎是Java虚拟机最核心的组成部分之一。”虚拟机”是一个相对于”物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

在Java虚拟机规范中制定了虚拟机执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。

在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。也可能两者兼备,甚至还可能包含几个不同级别的编译器执行引擎。但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的: 输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

运行时栈帧结构

栈帧(Stack Frame) 是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中, 只有栈顶的栈帧是有效的,称为 当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。

局部变量表的容量以变量槽(Variable Slot, 下称Slot) 为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有”导向性”地说明每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。这种描述与明确指出”每个Slot占用32位长度的内存空间”有一些区别,它允许Slot的长度随着处理器、操作系统或虚拟机的不同而发生变化。

不过无论如何,即使在64位虚拟机中使用了64位长度的内存空间来实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。

一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress八种类型。

reference是对象的引用。虚拟机规范既没有说明它的长度,也没有明确指出这个引用应有怎样的结构,但是一般来说,虚拟机实现至少都应当能从此引用中直接或间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。

returnAddress是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。

对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。Java语言中明确规定的64位的数据类型只有long和double两种(reference类型则可能是32位也可能是64位)。

由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。如果是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明要使用第n和第n+1两个Slot。

虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static的方法),那么局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字”this”来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用。这样的设计不仅仅是为了节省栈空间,在某些情况下Slot的复用会直接影响到系统的垃圾收集行为。

局部变量表复用的示例

在虚拟机运行参数中加上”-verbose: gc”可以查看垃圾收集的过程。

1
2
3
4
public static void main(String[] args) {
byte[] placeholder = new byte[64 * 1024 * 1024];
System.gc();
}

以上代码不会回收placeholder所占的内存,在System.gc()时,变量placeholder还处于作用域之内。

1
2
3
4
5
6
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}

此时代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作,placeholder原本所占用的Slot还没有被其他变量复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。
在下方加入新变量的赋值后,问题就可以解决:

1
2
3
4
5
6
7
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}

目前虚拟机的优化已经可以正确回收内存了。

局部变量没有默认值

局部变量不像类变量那样存在“准备阶段”。

类变量有两次赋初值的过程,一次在准备阶段,赋予系统初始值;另一次在初始化阶段,赋予程序员定义的初始值。

因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。所以不要认为Java中任何情况下都存在诸如整型变量默认为0、布尔型变量默认为false之类的默认值。

操作数栈

操作数栈也被称为操作栈,它是一个后入先出(Last In First Out, LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。

另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的。但是大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,而无需进行额外的参数复制传递了。

Java虚拟机的解释执行引擎称为”基于栈的执行引擎”,其中所指的”栈”就是操作数栈。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

方法返回地址

当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。

另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。 一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。 一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:

  1. 恢复上层方法的局部变量表和操作数栈

  2. 把返回值(如果有的话)压入调用者栈帧的操作数栈中

  3. 调整PC计数器的值以指向方法调用指令后面一条指令

…

附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。

在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前所说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法的调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法的调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

解析

所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析(Resolution)。

jvm类加载器

Posted on 2019-08-12 | In jvm

类加载器介绍

虚拟机涉及团队把类加载阶段中的”通过一个类的权限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为”类加载器”。

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。这句话等同于:

比较两个类是否”相等”,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类是来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里所指的”相等”,包括代表类的Class对象的equals()方法、isAssignableFrom() 方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果。

不同的类加载器对instanceof关键字运算结果的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 类加载器与instanceof关键字演示
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
}
Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
}
}

双亲委派模型

Java虚拟机角度的类加载器

在Java虚拟机的角度讲,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;

  2. 其他类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

开发人员角度的类加载器

从开发人员角度出发,绝大部分Java程序都会使用到以下三种系统提供的类加载器:

  • 启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在\<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。

  • 扩展类加载器(Extension ClassLoader): 这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

  • 应用程序类加载器(Application ClassLoader): 这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型简介

我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器:

1
启动类加载器(Bootstrap ClassLoader) <--- 扩展类加载器(Extension ClassLoader) <--- 应用程序类加载器(Application ClassLoader) <----多个用户自己定义的类加载器

类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parents Delegation Model)。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

类加载器的双亲委派模型在JDK1.2期间被引入并被广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者们的一种类加载器实现方式。

双亲委派模型工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派模型的优点

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序也将会变得一片混乱。

双亲委派模型的实现

双亲委派模型的实现非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中

逻辑清晰易懂:

  1. 先检查是否已经被加载过

  2. 若没有加载则调用父加载器的loadClass()方法

  3. 若父加载器为空则默认使用启动类

  4. 如果父类加载失败,则在抛出ClassNotFoundException异常后,再调用自己的findClass() 方法进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 则说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
}

双亲委派模型的破坏

为了兼容JDK1.2之前的自定义类加载器

为了向前兼容, JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。

JDK1.2之后已不提倡用户再去覆盖loadClass() 方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

基础类调用用户代码

JNDI服务的代码由启动类加载器去加载(在JDK1.3时代放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能”认识”这些代码。

Java设计团队引入了 线程上下文类加载器(Thread Context ClassLoader) 来解决这个困境。这个类加载器可以通过java.lang.Thread类的setContextClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。

JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作。

动态性的追求

动态性的追求就是希望应用程序能像电脑外设那样,插上鼠标或U盘,不用重启机器就能立即使用。

OSGi是当前业界的Java模块化标准,OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

  1. 将以java.*开头的类,委派给父类加载器加载。

  2. 否则,将委派列表名单内的类,委派给父类加载器加载。

  3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。

  4. 否则, 查找当前Bundle的ClassPath, 使用自己的类加载器加载。

  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。

  6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。

  7. 否则,类查找失败。

上面的查找顺序只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。

jvm类加载的过程

Posted on 2019-08-09 | In jvm

加载

“加载”(Loading)阶段是”类加载(Class Loading)”的第一个阶段。
在加载阶段,虚拟机需要完成以下三件事情:

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

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

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

类的获取方式

通过一个类的全限定名来获取定义此类的二进制字节流,并没有指明二进制字节流要从一个Class文件中获取,准确地说是根本没有指明要从哪里获取及怎样获取,一般有以下获取方式:

  • 从ZIP包中读取,最终成为JAR、EAR、WAR格式的基础。

  • 从网络中获取,这种场景最典型的应用是Applet。

  • 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成*$Proxy的代理类的二进制字节流。

  • 由其他文件生成,典型场景: JSP应用。

  • 从数据库中读取,这种场景相对少见些,有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

相对于类加载的其他阶段,加载阶段获取类的二进制字节流的动作是开发期可控性最强的阶段,因为加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员们可以通过定义自己的类加载器去控制字节流的获取方式。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。

然后在Java堆中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证工作的重要性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

Java语言本身是相对安全的语言(相对C/C++),使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。

但Class文件并不一定要求用Java源码编译而来,可以使用任何途径,包括用十六进制编辑器直接编写来产生Class文件。

在字节码的语言层面上,上述Java代码无法做到的事情都是可以实现的,至少语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证过程

尽管验证阶段的工作量在虚拟机的类加载子系统中占了很大一部分,但虚拟机规范对这个阶段仅仅要求如果验证到输入的字节流不符合Clas文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常。

不同的虚拟机对类验证的实现可能会有所不同,但大致上都会完成下面四个阶段的检验过程:

文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段一般包括以下验证点:

  • 是否以魔数0xCAFEBABE开头。

  • 主、次版本号是否在当前虚拟机处理范围之内。

  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。

  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。

  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。

  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

……

元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:

  • 这个类是否有父类(除了java.lang.Object之外, 所有的类都应当有父类)

  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)

  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。

  • 类中的字段、方法是否与父类产生了矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。

……

第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要工作是进行数据流和控制流分析。在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析。这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈中放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。

  • 保证跳转指令不会跳转到方法体以外的字节码指令上。

  • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

……

通过程序去校验程序逻辑是无法做到绝对准确的————不能通过程序准确地检查出程序是否能在有限的时间之内结束运行。

在JDK 1.6之后的Javac编译器中进行了一项优化,给方法体的Code属性的属性表中增加了一项名为”StackMapTable”属性,描述方法体中所有的基本块(Basic Block,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,这可以将字节码验证的类型推导转变为类型检查从而节省一些时间。

当然,这个属性也存在被篡改的可能,在JDK1.6的HotSpot虚拟机中提供了-XX: -UseSplitVerifier选项来关闭掉这项优化。

符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段————解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验,通常需要校验以下内容:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。

  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。

  • 符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

……

符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但不一定是必要的阶段。如果所运行的全部代码(包括自己写的、第三方包中的代码)都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify: none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

只分配类变量

这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

初始值”通常”是零值

假设一个类变量的定义为:

1
public static int value = 123;

那么变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器\<clinit>()方法中,所以把value赋值为123的动作将在初始化阶段才会被执行。

数据类型 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量。

符号引用与直接引用的区别:

符号引用

符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用

直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

解析进行的时机

虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast、getfield、getstatic、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。

所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的都是在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样地,如果第一次解析失败了,其他指令对这个符号的解析请求也应该收到相同的异常。

解析对应的对象

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量类型。

类或接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要包括以下3个步骤:

  1. 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于无数据验证、字节码验证的需要,又将可能触发其他类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败。

  2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava.lang.Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是”java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象。

  3. 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认C是否具备对D的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。

字段解析

要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索:

  1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

  2. 否则,如果在C中实现了接口,将会按照继承关系从上往下递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

  3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从上往下递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

  4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。

如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。

在实际应用中,虚拟机的编译器实现可能会比上述规范要求得更加严格一些,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。

类方法解析

类方法解析的第一个步骤与字段解析一样,也是需要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索:

  1. 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

  2. 如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  4. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束抛出java.lang.AbstractMethodError异常。

  5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。

最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证;如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。

接口方法解析

接口方法也是需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:

  1. 与类方法解析相反,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

  2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  3. 否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围会包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  4. 否则, 宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

由于接口中的所有方法都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常。

初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达: 初始化阶段是执行类执行器\<clinit>()方法的过程。

\<clinit>()方法

  1. \<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。

  2. \<clinit>()方法与类的构造函数(或者说实例构造器\<clinit>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的\<clinit>()方法执行之前,父类的\<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的\<clinit>()方法的类肯定是java.lang.Object。

  3. 由于父类的\<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

  4. \<clinit>() 方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成\<clinit>() 方法。

  5. 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成\<clinit>()方法。但接口与类不同的是,执行接口的\<clinit>()方法不需要先执行父接口的\<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的\<clinit>方法。

  6. 虚拟机会保证一个类的\<clinit>() 方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的\<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行\<clinit>()方法执行完毕。 如果在一个类的\<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

jvm类加载机制概述

Posted on 2019-08-09 | In jvm

类加载机制概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载和连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

类加载的时机

类的生命周期:

加载(Loading) –> 验证(Verification) –> 准备(Preparation) —> 解析(Resolution) —> 初始化(Initialization) —> 使用(Using) —> 卸载(Unloading)

其中 验证,准备,解析统称为连接。

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始。

而解析阶段则不一定: 它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

这些阶段的各个步骤是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

虚拟机并没有强制约束什么时候执行加载,但严格规定了四种情况下必须立即对类进行”初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先出发其初始化。生成这4条指令的最常见的Java代码场景是: 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

  2. 使用java.lang.reflect包的方法对类进行发射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

这四种场景中的行为称为对一个类进行主动引用。除此之外所有引用类的方式,都不会触发初始化,称为被动引用。

被动引用例子

例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 通过子类引用父类的静态字段,不会导致子类初始化
*/
public class SuperClass{
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization{
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

上述代码运行之后,只会输出”SuperClass init!”,而不会输出”SubClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现。

例二:

1
2
3
4
5
6
7
8
/**
* 通过数组定义来引用类,不会触发此类的初始化
*/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}

例三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主动使用类字段演示
*/
public class NotInitialization {
public static void main (String[] args) {
System.out.println(Constant.HELLOWORLD);
}
}

上述代码运行之后,也没有输出”ConstClass init!”, 这是因为虽然在Java源码中引用了ConstClass类中的常量HELLOWORLD,但是在编译阶段将此常量的值”hello world”存储到了NotInitialization类的常量池中,对常量ConstClass.HELLOWORLD的引用实际都被转化为NotInitialization类对自身常量池的引用了。也就是说实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。

接口的加载过程

接口的加载过程与类加载过程稍有一些不同,针对接口需要做一些特殊说明:

接口也有初始化过程,这点与类是一致的,上面的代码都是用静态语句块”static{}”来输出初始化信息的,而接口中不能使用”static{}”语句块,但编译器仍然会为接口生成”\<clinit>()”类构造器,用于初始化接口中所定义的成员变量。

接口与类真正有所区别的是前面讲述的四种”有且仅有”需要开始初始化场景中的第三种: 当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

jvm类文件的结构5-属性表集合

Posted on 2019-08-09 | In jvm

属性表简介

在字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

属性表集合的限制比较宽松,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

以下是虚拟机规范预定义的属性:

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要说明属性值所占用的位数长度即可。

属性表结构

类型 名称 数量
u2 attribute_name_index 1
u2 attribute_lenght 1
u1 info attribute_lenght

Code属性

Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。

Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或抽象类中的方法就不存在Code属性。

Code属性表的结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count

attribute_name_index

attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为”Code”,它代表了该属性的属性名称, attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节。

max_stack

max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Frame)中的操作栈深度。

max_locals

max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean、reference和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要2个Slot来存放。
方法参数(包括实例方法中的隐藏参数”this”)、显式异常处理器的参数(Exception Handler Parameter, 即try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。
另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot就可以被其他局部变量所使用,编译器会根据变量的作用域来分类Slot并分配给各个变量使用,然后计算出max_locals的大小。

code_length 和 code

code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然名为字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以相应地找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。我们知道一个u1数据类型的取值范围为0x00到0xFF,对应十进制的0~255,也就是一共可以表达256条指令。

关于code_length,还有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到2^32-1,但是虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过这个限制,Javac编译器就会拒绝编译。一般来讲,只要我们写Java代码时不是刻意去编写超长的方法,就不会超过这个最大值的限制。但是,在编译复杂的JSP文件时,可能会因为这个原因导致编译失败。

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有其他数据项目都用于描述元数据。

异常表集合

异常表对于Code来说并不是必须存在的。

异常表格式如下:

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1

如果字节码从第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任何的异常情况都需要转向到handler_pc处进行处理。

编译器为try catch生成了三条异常表记录:

  1. 如果try语句块中出现属于Exception或其子类的异常,则转到catch语句块处理。

  2. 如果try语句块中出现不属于Exception或其子类的异常,则转到finally语句块处理。

  3. 如果catch语句块中出现任何异常,则转到finally语句块中处理。

Exceptions属性

Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法在描述时在throws关键字后面列举的异常。

结构如下表:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

number_of_exceptions表示方法会抛出多少个受查异常。
每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。

LineNumberTable

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g: none 或 -g: lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是在抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候无法按照源码来设置断点。

LocalVariableTable

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它不是运行时必需的属性,默认也不会生成到Class文件之中,可以在Javac中使用-g:none 或 -g: vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将丢失,IDE可能会使用诸如arg0、arg1之类的占位符来代替原有的参数名,这对程序运行没有影响,但是会给代码编写带来较大的不便,而且在调试期间调试器无法根据参数名称从运行上下文中获得参数值。

在JDK 1.5引入泛型之后,LocalVariableTable属性增加了一个”姐妹属性”: LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名能描述的信息基本是一致的,但是引入泛型后,由于描述符中泛型的参数化类型被擦除掉了,描述符就不能准确地描述泛型类型了,因此出现了LocalVariableTypeTable.

SourceFile

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以使用Javac的-g: none 或 -g: source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错误代码所属的文件名。

结构如下所示:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

ConstantValue

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static 关键字修饰的变量(类变量) 才可以使用这项属性。
对于非static类型的变量(也就是实例变量)的赋值是在实例构造器\<init>方法中进行的;
而对于类变量,则有两种方式可以选择:

  1. 赋值在类构造器\<clinit>方法中进行

  2. 使用ConstantValue属性来赋值。

Sun Javac编译器的选择是: 如果同时使用final和static来修饰一个常量,并且这个变量的数据类型是基本类型或java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则选择在\<clinit>中进行初始化。

虽然有final关键字才更符合”ConstantValue”的语义,但虚拟机规范中并没有强制要求字段必须设置了ACC_FINAL标志, 只要求了有ConstantValue属性的字段必须设置ACC_STATIC标志,对final关键字的要求是Javac编译器自己加入的限制。

而ConstantValue的属性值则只限于基本类型和String,不过笔者不认为这是什么限制,因为此属性的属性值只是一个常量池的索引号,由于Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算ConstantValue属性想支持别的类型也无能为力。

InnerClasses

InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类定义了内部类,那编译器将会为它及它所包含的内部类生成InnerClasses属性。

属性的结构如表所示:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1
inner_classes_info inner_classes number_of_classes

数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述。

inner_classes_info表的结构如下:

类型 名称 数量
u2 inner_class_info_index 1
u2 outer_class_info_index 1
u2 inner_name_index 1
u2 inner_class_access_flags 1

inner_class_info_index, outer_class_info_index

inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。

inner_name_index

inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,则这项值为0。

inner_class_access_flags

inner_class_access_flags是内部类的访问标志,类似于类的access_flags.

Deprecated及Synthetic属性

Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

Deprecated属性用于表示某个类、字段或方法已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释进行设置。

Synthetic属性代表此字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的,在JDK1.5之后,标识一个类、字段或方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“\<init>”方法和类构造器”\<clinit>“方法。

Deprecated和Synthetic属性的结构非常简单:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置。

jvm类文件的结构4-字段表集合和方法表集合

Posted on 2019-08-09 | In jvm

字段表集合

字段表集合的信息

字段表(field_info)用于描述接口或类中声明的变量。

字段(field包括了类级变量或实例级变量,但不包括方法内部声明的变量)

Java中描述一个字段包含以下信息:

  1. 字段的作用域(public、pivate、 protected修饰符)

  2. 类级变量还是实例级变量(static修饰符)

  3. 可变性(final)

  4. 并发可见性(volatile修饰符,是否强制从主内存读写)

  5. 可否序列化(transient修饰符)

  6. 字段数据类型(基本类型、对象、数组)、字段名称。

这些信息大概可分为两类:

  1. 修饰符. 修饰符只有有和无两种状态,适合使用标志位表示

  2. 无法确定的信息。引用常量池中的常量来描述。

字段表集合的结构

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

access_flags

字段修饰符在access_flags项目中,也是以或的形式拼接各类修饰符

字段访问标志
标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否public
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_FINAL 0x0010 字段是否final
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生的
ACC_ENUM 0x4000 字段是否enum

name_index和descriptor_index

它们都是对常量池的引用,分别代表着字段的简单名称及字段和方法的描述符。

全限定名、简单名称、描述符
权限定名

格式如: org/fenixsoft/clazz/TestClass。将类全名的”.”替换成了”/“,为了使连续的多个全限定名之间不产生混淆,在使用时最后会加上;

简单名称

简单名称就是指没有类型和参数修饰的方法或字段名称,这个类中的inc()方法和m字段的简单名称分别是”inc”和”m”。

描述符

用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,如Ljava/lang/Object

对于数组类型,每一维度将使用一个前置的”[“字符来描述

如一个定义为”java.lang.String[][]“类型的二维数组,将被记录为: “[[Ljava/lang/String;”

一个整型数组”int[]”将被记录为”[I”。

用描述符描述方法时,按照先参数列表,后返回值的顺序。参数列表按照参数的严格顺序放在一组小括号”()”之内。

如方法void inc()的描述符为”()V”

方法java.lang.String toString()的描述符为”()Ljava/lang/String;”

方法int indexOf(char[] soure, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为”([CII[CIII)I”。

attribute_info

将在属性表中详细讲解

字段表总结

字段表集合中不会列出超类或父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

方法表集合

方法表描述

方法表描述和字段的描述基本一致,包括了:

  • 访问标志(access_flags)

  • 名称索引(name_index)

  • 描述符索引(descriptor_index)

  • 属性表集合(attributes)

不同之处主要在于访问标志和属性表集合的可选项。

方法中没有 volatile 和 transient,但是多了 synchronized、native、strictfp、abstract。如下:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否为编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICT 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否为编译器自动产生的

而方法里的Java代码,经过编译器编译成字节码指令之后,存放在方法属性表集合中的一个名为”Code”的属性里面。

方法表总结

如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。 但有可能会出现由编译器自动添加的方法,最典型的是类构造器”\<clinit>“方法和实例构造器”\<init>“方法。

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。
特征签名就是各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。
但是在Class文件格式之中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。 也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

jvm类文件的结构3-类索引、父类索引与接口索引集合

Posted on 2019-08-09 | In jvm

Class文件索引简介

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。

Class文件中由这三项数据来确定这个类的继承关系:

  1. 类索引用于确定这个类的全限定名。

  2. 父类索引用于确定这个类的父类的权限定名。

  3. 接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口的索引集合中。

索引的结构描述

类索引、父类索引和接口索引集合按顺序排列在访问标志之后。

类索引和父类索引

类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

1
2
索引/父类索引        常量池                           常量池
this_class ---> CONSTANT_Class_info index----> CONSTANT_Utf8_info (记录类的全限定名)

接口索引集合

接口索引集合,入口第一项————u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。

如果该类没有实现任何接口,那么该计数器值为0,后面接口的索引表不再占用任何字节。

jvm类文件的结构2-常量池与类访问的标志

Posted on 2019-08-08 | In jvm

常量池

紧接着版本号的是常量池
常量池有以下几点特征:

  1. 常量池是Class文件结构中与其他项目关联最多的数据类型

  2. 常量池是占用Class文件空间最大的数据项目之一

  3. 常量池是Class文件中第一个出现的表类型数据项目

计数值

由于常量池中常量的数量是不固定的, 所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java语言习惯不一样的是,这个容量计数是从1而不是0开始的。
制定Class文件格式规范时,将第0项常量空出来是有特殊考虑的:
某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”,就可以把索引值置为0来表示。
Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。

字面量和符号引用

常量池之中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

字面量

字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。

符号引用

符号引用主要属于编译原理方面的概念, 包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)

  • 字段的名称和描述符(Descriptor)

  • 方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。
也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。

常量表

常量池中的每一项常量都是一个表,共有11种结构各不相同的表结构数据,这11种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag,取值为1至12,缺少标志为2的数据类型),代表当前这个常量属于哪种常量类型

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用

常量表的具体内容

CONSTANT_Utf8_info

|项目|类型|描述|
|tag|u1|值为1|
|length|u2|UTF-8编码的字符串占用的字节数|
|bytes|u1|长度为length的UTF-8编码的字符串|

CONSTANT_Integer_info

|项目|类型|描述|
|tag|u1|值为3|
|bytes|u4|按照高位在前存储的int值|

CONSTANT_Float_info

|项目|类型|描述|
|tag|u1|值为4|
|bytes|u4|按照高位在前存储的float值|

CONSTANT_Long_info

|项目|类型|描述|
|tag|u1|值为5|
|bytes|u8|按照高位在前存储的long值|

CONSTANT_Double_info

|项目|类型|描述|
|tag|u1|值为6|
|bytes|u8|按照高位在前存储的double值|

CONSTANT_Class_info

|项目|类型|描述|
|tag|u1|值为7|
|index|u2|指向全限定名常量项的索引|

CONSTANT_String_info

|项目|类型|描述|
|tag|u1|值为8|
|index|u2|指向字符串字面量的索引|

CONSTANT_Fieldref_info

|项目|类型|描述|
|tag|u1|值为9|
|index|u2|指向声明字段的类或接口描述符CONSTANT_Class_info的索引项|
|index|u2|指向字段描述符CONSTANT_NameAndType的索引项|

CONSTANT_Methodref_info

|项目|类型|描述|
|tag|u1|值为10|
|index|u2|指向声明字段的类或接口描述符CONSTANT_Class_info的索引项|
|index|u2|指向字段描述符CONSTANT_NameAndType的索引项|

CONSTANT_InterfaceMethodref_info

|项目|类型|描述|
|tag|u1|值为11|
|index|u2|指向声明字段的类或接口描述符CONSTANT_Class_info的索引项|
|index|u2|指向字段描述符CONSTANT_NameAndType的索引项|

CONSTANT_NameAndType_info

|项目|类型|描述|
|tag|u1|值为12|
|index|u2|指向该字段或方法名称常量项的索引|
|index|u2|指向该字段或方法描述符常量项的索引|

javap -verbose

我们可以使用javap -verbose来查看一个Class文件的字节码信息。

访问标志

常量池之后的2个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:

  • 这个Class是类还是接口

  • 是否定义为public类型

….

以下是Java中的访问标志:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令,JDK1.2之后编译出来的类这个标志为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或抽象类来说,这个值为真,其他类值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

access_flags一共有32个标志位可以使用,当前只定义了其中的8个。
没有使用到的标志位要求一律为0。
这些标志位以或的方式集成到一起。

123…5

Sean

46 posts
10 categories
18 tags
© 2019 Sean
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4