文章目录

  • 2. 编写第一个程序
    • 2.1 编写源代码并运行
      • 2.1.1 创建 .java 文件编写源码
      • 2.1.2 配置环境变量
      • 2.1.3 解释代码含义
      • 2.1.4 编译与运行
    • 2.2 class 文件内容
      • 2.2.1 魔数与版本号
      • 2.2.2 常量池
      • 2.2.3 访问标志
      • 2.2.4 类索引、父类索引、接口索引集合
      • 2.2.5 字段表集合
      • 2.2.6 方法表集合
      • 2.2.7 属性表集合
    • 2.3 类加载过程
      • 2.3.1 类的初始化时机
      • 2.3.2 加载
      • 2.3.3 验证
      • 2.3.4 准备
      • 2.3.5 解析
      • 2.3.6 初始化
    • 2.4 代码书写规范
      • 2.4.1 代码书写
      • 2.4.2 代码规范
      • 2.4.3 标识符与关键字
    • 2.5 注释
      • 2.5.1 单行注释
      • 2.5.2 多行注释
      • 2.5.3 文档注释
      • 2.5.4 IDEA 工具自动快捷键添加注释
      • 2.5.5 使用 javac 命令编译时出现的"错误: 编码 GBK 的不可映射字符"
      • 2.5.6 查看系统编码
      • 2.5.7 使用 API 文档
    • 2.x 总结回顾
    • 2.y 课后习题
    • 2.z 习题答案

2. 编写第一个程序

内容导视:

  • 编写源代码并运行
  • class 文件内容
  • 类加载过程
  • 代码书写规范
  • 注释

我初学时往往忽略了什么类加载过程、运行时常量池、栈结构等内容,结果对象倒是会 new,只是听着类变量(静态变量)类加载时默认初始化有点懵,于是从 《深入理解java虚拟机》这本书中节选一些内容,可以跳过。

网址:https://docs.oracle.com/javase/specs/index.html

2.1 编写源代码并运行

内容导视:

  • 创建 .java 文件编写源码
  • 配置环境变量
  • 解释代码含义
  • 编译与运行

2.1.1 创建 .java 文件编写源码

开始写源代码,但只是在 DOS 窗口中输出一句话而已…

创建以 .java 结尾的文件,如 Hello.java。[1]

双击此文件,输入如下。还是直接复制、粘贴吧,记得保存。(Ctrl + S)

码 2.1.1-1 你好世界!
public class Hello {public static void main(String[] args) {System.out.println("hello world!");}
}

打开 DOS 窗口,使用 cd 命令切换到此文件的所在目录。[2]

图 2.1.1-1 最方便的一种方法

在 DOS 窗口中输入 javac Hello.java,得到如下结果。

码 2.1.1-2 不被识别的命令
'javac' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

你可能会疑惑?怎么没有找到这个命令,不是在 JDK 的安装路径/bin 目录下有 javac.exe 命令吗?你说找不到?那为什么之前 DOS 窗口查看 IP 时,也就是 C:\Windows\System32 下的 ipconfig 命令就可以执行?

此电脑右键/属性/高级系统设置/环境变量/系统变量/Path 下看到了 %SystemRoot%\system32,即 C:\Windows\System32[3]

猜想:难道系统只会在 Path 指定的路径下寻找命令吗?我随手就删了它(别试,否则手动还原),确定保存刚刚的修改操作,重新打开 DOS 窗口,再次执行 ipconfig,很明白了。

图 2.1.1-2 执行 ipconfig 命令

得出结论:执行的程序如果在当前目录下不存在,系统会在名为 Path 的环境变量指定的目录下查找。

好的,大概明白了,但我的电脑上有多个用户,那么为了不影响其他用户,我只配置用户变量的 Path,优先级当然没有系统变量的高。(系统变量针对所有用户生效)

[1] “xxx.java” 中的 “java” 称为后缀或扩展名,打开文件资源管理器,查看,勾上文件扩展名和隐藏的项目。

文件右击选择打开一种打开方式,(EditPlus 或其他文本编辑器,找不到选择其它应用,然后选中 “始终使用此应用打开此类型的文件” 复选框)这样你以后双击 .java 文件就可以直接使用此软件打开啦。

[2] 如果你在桌面上创建的文件,路径是 C:\Users\自己的用户名\Desktop,你可以自己打开文件资源管理器,点击左边的桌面图标,在地址栏上输入 cmd 后回车。

[3] 如果桌面上没有此电脑图标:

  • 打开文件资源管理器,此电脑的图标在左边。
  • 或者 Windows + i 打开设置,点击系统/关于,往下翻,相关设置/高级系统设置。
  • 右键/个性化/主题/桌面图标设置,勾选计算机图标后点击应用和确定。

2.1.2 配置环境变量

上节知道了 Path 变量的作用,现在该把命令地址添加到 Path 中了。

双击 Path,添加 javac 命令所在路径。(如果你还记得 JDK 安装的位置,打开 bin 目录,复制地址栏上的地址)

图 2.1.2-1 配置 path 变量

如果这个 JDK 安装目录,以后会重复使用。为了方便,新建变量 JAVA_HOME 保存 JDK 安装目录,供其它地方使用。

图 2.1.2-2 自定义变量 JAVA_HOME

再把之前的 javac 命令所在路径替换成 %JAVA_HOME%\bin

图 2.1.2-3 替换之前的路径

%JAVA_HOME% 代表变量 JAVA_HOME 对应的值。以后 JavaEE、开发工具自动获取 JDK 的安装目录,这样就不需要再手动设置了。

有的人可能版本较老,Path 变量值在一行文本框上显示。那么添加路径时,注意路径与路径之前使用;分隔,注意这个分号是英文状态下的半角符号,你按下 Shift,看看电脑右下角是否中英在切换。写成 ;就错了。以后如果不说明,统一使用英文状态下的符号。

例:

图 2.1.2-4 老版本显示的变量值

检查自己是否配置好了:

码 2.1.2-1 检查配置
打开 DOS 窗口,输入 java -version 和 javac 等命令
如果显示版本信息,证明 java.exe 是可以用的再输入 echo %Path%
查看刚刚配置的 JDK的家\bin 路径是否在其中

有的人可能有疑惑,明明自己没配环境变量,为什么 java.exe 可以使用?

答:使用 exe 文件安装 JDK 后,会自动在系统变量 Path 添加如下图路径:

C:\Program Files (x86)\Common Files\Oracle\Java\javapath

图 2.1.2-5 其它的 java.exe

所以没有手动配置环境变量时,java 命令可以用,但 javac 不行。

每次改动,都需要重新打开 DOS 窗口,配置才能生效。输入 javac Hello.java[1],如果你的语法正确,就会在当前路径下生成 class(字节码)文件,再输入 java Hello ,此时别带后缀。

图 2.1.2-6 编译与运行

有人就说了,每次运行 class 文件,都要使用 cd 命令进入此目录,再执行命令很麻烦。那么可以通过 classpath 变量指定字节码文件所在位置。

新建环境变量 classpath:

图 2.1.2-7 新建 classpath 变量

“.” 是英文句号,代表当前路径,如果不写就不会从当前路径寻找 class 文件。
由于我习惯把 class 文件放在桌面上,于是新增了桌面的路径,路径和路径直接使用 “;” 分隔,别使用中文的分号。

这样不管在任何地方,输入 java Hello 就可以执行了。

总结

Path 是 Windows 查找 “.exe” 文件的路径;classpath 是 JVM 查找 “.class” 文件的路径;如果你以后学会了如何打 jar 包,假如它的绝对路径为 E:\a\c\d\Tool.jar,如果想任何位置敲 java -jar Tool.jar 让 jar 包执行,就可以把 E:\a\c\d\Tool.jar 加到 classpath 变量值里。

[1] javac Te*.java 会将文件名开头为 “Te” 的所有 “java” 文件都编译。

2.1.3 解释代码含义

看不懂很正常,学完面向对象就懂了。突然遇到看不懂的不要怀疑自己智力有问题,只是还没到这步而已,大不了直接跳过。

码 2.1.3-1 第一个程序
public class Hello {// args 是变量名,可以根据标识符的命名规则取名public static void main(String[] args) {System.out.println("hello world!");}
}
  • 关键字 public 是访问修饰符,表明该类是一个公共类,可以控制其他对象对类成员的访问。
  • 关键字 class 用于声明一个类,其后所跟的 Hello 是类名。
  • Hello 后跟的 {} 称为类体,包含了方法、字段…
  • public static void main(String[] args) {}:这个是 main 方法,由于被 JVM 调用,也被称为主方法、入口方法。所有 java 代码都是最先从入口方法开始执行的。
    • main 后的 {} 称为方法体,包含了 java 语句。
    • 关键字 static 表示该方法是一个静态方法,无须创建类的实例即可调用。
    • 关键字 void 表示此方法没有返回值。
    • void 后面的是方法名 main。
    • main 后的括号包含一个形式参数,这个形参是 String 类型的数组,参数名是 args。
    • System.out.println(""); 是 java 语句,代表打印(输出)括号中的内容到控制台(DOS 窗口)。
    • “hello world!” 是一个字符串。
    • ; 代表一条 java 语句的结束,注意别写成中文分号。

2.1.4 编译与运行

编译与运行

首先新建后缀为 java 的文件,编写出符合语法规则的代码。

编译:使用 javac 源文件名.java 命令编译生成后缀为 class 的文件(也称字节码文件)。

运行:使用 java class文件名 命令,生成了一个 JVM 实例,Java 程序运行在此实例上;JVM 实例通过调用此类型的 main 方法来运行一个 Java 程序,当程序退出时,JVM 实例随之消亡。

同时运行三个 Java 程序,会得到三个 JVM 实例,每个 Java 程序都运行在它自己的 JVM 实例中。

图 2.1.4-1 编译与运行

编译

编译期将源码交给编译器,编译成可以被 JVM 识别的字节码,如果源代码不符合语法规则,就会报错(错误提示很智能,可以根据报错信息相应找到原因,自己试试比如删除一个大括号、引号等,看看 javac 命令给你报什么错)

一个源文件可以定义多个类型,如 class xxx、class xxx2、enum xxx…、编译后生成多个 class 文件,每个 class 文件都对应着唯一的一个类型的定义信息。

码 2.1.4-1 定义不同类型
package com.cqh;class A {}// A 是类名
class A2 {}// A2 是类名
interface B {}// B 是接口名
@interface C {}// C 是注解名
enum D {}// D 是枚举类名// 上述类型的名称称为简单名,如果再加上包名,如 com.cqh.A,称为全限定名或完整名

编译后生成 A.class、B.class、C.class、D.class 文件。

在程序运行过程中也可以动态生成某类型,所以 class 文件不一定只指存放在磁盘上的文件,而是代表某类型的二进制字节流,无论以何种形式存在。

运行

(当源文件修改后,必须重新编译才能生效)

编译后生成 class 字节码文件,使用 java class文件名 运行,别加 .class 后缀。

运行期类加载器(Class Loader)找字节码文件(如果没有配 classpath,默认从当前路径下找),找到了就加载字节码文件到 JVM 虚拟机,JVM 启动解释器对字节码文件进行解析,生成的二进制码读到内存中,由 CPU 进行二进制码的执行。

字节码文件是一种和任何具体机器环境及操作系统环境无关的中间代码,编程人员和计算机都无法直接读懂字节码文件。它是一种二进制文件,是 Java 源文件由 Java 编译器编译后生成的目标代码文件。它必须由专用的 Java 解释器来解释执行,因此 Java 是一种在编译基础上进行解释运行的语言。

2.2 class 文件内容

  • 内容导视:

    • 魔数与版本号
    • 常量池
    • 访问标志
    • 类索引、父类索引、接口索引集合
    • 字段表集合
    • 方法表集合
    • 属性表集合

    java 文件经 javac 编译器编译后生成 class 文件。

    class 文件是一组以 8 个字节为基础单位的二进制流[1],只有两种数据类型:无符号数、表。

    无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1、2、4、8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。

    表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以 “_info” 结尾。一系列连续的同一类型、但数量不定的数据称为某类型的集合。

    整个 class 文件本质上可以视作是一张表,由所示的数据项按严格顺序排列构成。

    分别为:魔数、次版本号、主版本号、常量数量、常量表的集合、访问标志、类索引、父类索引、接口索引集合、字段数量、字段表集合、方法数量、方法表集合、属性数量、属性表集合。

    表 2.2-1 class 文件结构
    类型 名称 数量
    u4 magic 1
    u2 minor_version 1
    u2 major_version 1
    u2 constant_pool_count 1
    cp_info constant_pool constant_pool_count-1
    u2 access_flags 1
    u2 this_class 1
    u2 super_class 1
    u2 interfaces_count 1
    u2 interfaces interfaces_count
    u2 fields_count 1
    field_info fields fields_count
    u2 methods_count 1
    method_info methods methods_count
    u2 attribute_count 1
    attribute_info attributes attribute_count

    [1] 一个字节 8 位,每位都是 0 或 1,例 11001010,转为十六进制显示:CA。常常加上前缀表明进制,如 0xCA 代表按十六进制显示的一个字节。

2.2.1 魔数与版本号

使用十六进制编辑器打开 class 文件,提取一部分:

表 2.2.1-1 魔数与版本号
Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 34

00 ~ 03:0xCAFEBABE,这 4 个字节称为魔数(magic),用来确定此文件是否为一个能被虚拟机接受的 class 文件。[1]

04 ~ 05:次版本号(Minor Version)0x0000。

06 ~ 07:主版本号(Major Version)0x0034,对应的十进制为 52,代表 JDK8;JDK1 从 45 开始,每隔一个大版本加 1;高版本的 JDK 可以向下兼容以前版本的 class 文件,反之则不行。

例:JDK1 无法执行版本号为 46 及以上的 class 文件。

强行运行会报错,抛出 UnsupportedClassVersionError:

码 2.2.1-1 错误信息
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.UnsupportedClassVersionError:
com/cqh/arr3/FieldResolution has been compiled by a more recent version of the Java Runtime (class file version 60.0),
this version of the Java Runtime only recognizes class file versions up to 52.0

意思大概是 class 文件被 JDK16(60.0)编译,使用 JDK8(52.0)运行只能识别 52.0 以下的版本。

[1] 使用魔数作为文件格式的标识,因为扩展名可以随意改动,不安全。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过而且不会引起混淆。

2.2.2 常量池

表 2.2.2-1 常量池
Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: 00 27 0A 00 09 00 16 09
00000010: 00 08 00 17 08 00 18 09 00 08 00 19 09 00 1A 00
00000020: 1B 08 00 1C 0A 00 1D 00 1E 07 00 1F 07 00 20 01
00000150: 69 6E 74 6C 6E 01 00 15 28 4C 6A 61 76 61 2F 6C
00000160: 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56

由于常量池中,常量的数量不固定,需要一项 u2 类型的数据,记录下数量;

08 ~ 09:常量的数量(constant_pool_count),0x0027,十进制为 39,代表常量池中有 38 项常量,索引值为 1 ~ 38。[1]

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量即文本字符串、被声明为 final 的常量值等。

符号引用主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)

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

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

  • 方法的名称和描述符

  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)

  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

class 文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。

当虚拟机加载类型时,将会从常量池获得对应的符号引用,再在类创建时或运行时,解析、翻译到具体的内存地址之中。[2]

常量池中每一项常量都是一个表,它们都有一个共同的特点,表结构起始的第一位是个 u1 类型的标志位(tag),代表着当前常量属于哪种常量类型。

表 2.2.2-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_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Module_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开放或导出的包

0A:第 1 项常量的标志位 0x0A,十进制为 10;查表可知这个常量属于 CONSTANT_Methodref_info 类型,代表类中方法的符号引用。

表 2.2.2-3 CONSTANT_Methodref_info 常量的结构
类型 名称 数量 描述
u1 tag 1 标志位 10
u2 class_index 1 此索引指向 CONSTANT_Class_info 类型的常量,表示方法所属的类型
u2 name_and_type_index 1 此索引 CONSTANT_NameAndType_info 类型的常量,表示方法名称及描述符

0B ~ 0C:0x0009,指向第 9 项常量,第 9 项常量属于 CONSTANT_Class_info 类型,此常量的 name_index 值为 32,指向第 32 项常量,第 32 常量属于 CONSTANT_Utf8_info 类型,此常量的 bytes 值经 UTF-8 解码后,为 “java/lang/Object”。所以此方法所属类型已然确定。

0D ~ 0E:0x0016,十进制为 22,指向第 22 项常量,第 22 项常量属于…略。

0F:第 2 项常量的标志位 0x09,十进制为 9,查表可知这个常量属于 CONSTANT_Fieldref_info 类型,代表字段的符号引用。

表 2.2.2-4 CONSTANT_Fieldref_info 型常量的结构
类型 名称 数量 描述
u1 tag 1 标志位 9
u2 class_index 1 此索引指向 CONSTANT_Class_info 类型的常量,表示字段所属类型
u2 name_and_type_index 1 此索引指向 CONSTANT_NameAndType_info 类型的常量,表示字段名及描述符

10 ~ 11:0x0008,指向第 8 项常量。

12 ~ 13:0x0017,指向第 23 项常量。

14:第 3 项常量池的标志位 0x08,属于 CONSTANT_String_info 类型,代表字符串类型的字面量。

表 2.2.2-5 CONSTANT_String_info 型常量的结构
类型 名称 数量 描述
u1 tag 1 标志位 8
u2 string_index 1 此索引指向 CONSTANT_Utf8_info 类型的常量,表示字符串字面量

15 ~ 16:0x0018,指向第 24 项常量。

155:第 38 项常量的标志位 0x01,属于 CONSTANT_Utf8_info 类型,代表 UTF-8 编码的字符串,用来表示字符串字面量、类型的全限定名、字段名、…[3]

表 2.2.2-6 CONSTANT_Utf8_info 型常量的结构
类型 名称 数量 描述
u1 tag 1 标志位 1
u2 length 1 UTF-8 编码的字符串占用字节数
u1 bytes length UTF-8 编码的字符串

2 个字节能够表示的最大值为 65535,占用字节数最大不能超过 65535,即要小于 64 KB,否则无法通过编译。

156 ~ 157:0x0015,此字符串字面量占用 21 个字节。

158 ~ 16C:0x284C6A6176612F6C616E672F537472696E673B2956,经 UTF-8 解码,得 (Ljava/lang/String;)V

使用 javap 反编译工具,加 -verbose 参数输出 class 文件的部分内容:

码 2.2.2-1 常量表集合
final class Happyminor version: 0major version: 52flags: ACC_FINAL, ACC_SUPER
Constant pool:#1 = Methodref          #9.#22         // java/lang/Object."<init>":()V#2 = Fieldref           #8.#23         // Happy.i:I#3 = String             #24            // java#4 = Fieldref           #8.#25         // Happy.name:Ljava/lang/String;#5 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;#6 = String             #28            // Hello World#7 = Methodref          #29.#30        // java/io/PrintStream.println:(Ljava/lang/String;)V#8 = Class              #31            // Happy#9 = Class              #32            // java/lang/Object#10 = Utf8               i#11 = Utf8               I#12 = Utf8               name#13 = Utf8               Ljava/lang/String;#14 = Utf8               <init>#15 = Utf8               ()V#16 = Utf8               Code#17 = Utf8               LineNumberTable#18 = Utf8               main#19 = Utf8               ([Ljava/lang/String;)V#20 = Utf8               SourceFile#21 = Utf8               Happy.java#22 = NameAndType        #14:#15        // "<init>":()V#23 = NameAndType        #10:#11        // i:I#24 = Utf8               java#25 = NameAndType        #12:#13        // name:Ljava/lang/String;#26 = Class              #33            // java/lang/System#27 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;#28 = Utf8               Hello World#29 = Class              #36            // java/io/PrintStream#30 = NameAndType        #37:#38        // println:(Ljava/lang/String;)V#31 = Utf8               Happy#32 = Utf8               java/lang/Object#33 = Utf8               java/lang/System#34 = Utf8               out#35 = Utf8               Ljava/io/PrintStream;#36 = Utf8               java/io/PrintStream#37 = Utf8               println#38 = Utf8               (Ljava/lang/String;)V

第 1 项常量是方法的符号引用,代表调用 Object 的 <init> 方法;<init> 是构造方法,内部调用父类构造器(Object 除外)、显示初始化实例变量和执行实例语句块(如果有)、执行本类的构造器中的语句。

第 2 项常量是字段的符号引用,说明此字段在 Happy 类中,变量名为 i,int 类型。

第 3 项常量是字符串字面量 “java”,可表示为 java.lang.String 的实例。

第 4 项常量是字段的符号引用,说明此字段在 Happy 类中,变量名为 name,java.lang.String 类型。

[1] 空出的一项为 0,将索引值设为 0,代表不引用任何一个常量池项目。

[2] 解析阶段的前期绑定与后期绑定。

[3] 受篇幅原因,其余类型常量的结构,可在 《Java 虚拟机规范》的 4.4 节查看。

2.2.3 访问标志

表 2.2.3-1 标志
Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000160: 00 30

16D ~ 16E:0x0030,访问标志(access_flags),表示类型的修饰符。

表 2.2.3-2 访问标志
标志名称 标志值 含义
ACC_PUBLIC 0x0001 public
ACC_FINAL 0x0010 final
ACC_SUPER 0x0020 使用 invokespecial 字节码指令的新语义,JDK 1.0.2 后都为真
ACC_INIERFACE 0x0200 接口类型
ACC_ABSTRACT 0x0400 abstract
ACC_SYNIHETIC 0x1000 这个类型并非由用户代码生成
ACC_ANNOTATION 0x2000 注解类型
ACC_ENUM 0x4000 枚举类型
ACC_MODULE 0x8000 模块

例:

0x0001 的二进制为 0000 0000 0000 0001,若类是 public 类型,则第 1 位应为 1,否则为 0。

0x0010 的二进制为 0000 0000 0001 0000,若类是 final 类型,则第 5 位应为 1,否则为 0。

0x0020 的二进制为 0000 0000 0010 0000,若类使用 invokespecial 指令的新语义,则第 6 位应为 1,否则为 0。

0x0030 的二进制为 0000 0000 0011 0000,第 5、6 位为 1,代表它是使用 invokespecial 新语义的 final 类,其它位为 0,说明此类型没有 public、abstract 修饰…

2.2.4 类索引、父类索引、接口索引集合

表 2.2.4-1 类索引、父类索引、接口索引集合
Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000160: 00
00000170: 08 00 09 00 00

16F ~ 170:0x0008 这 2 个字节称为类索引(this_class),用于确定此类的全限定名,指向第 8 项常量。

再回过头来;

29:0x07,十进制为 7,第 8 项常量的标志位,属于 CONSTANT_Class_info 类型。

表 2.2.4-2 CONSTANT_Class_info 型常量的结构
类型 名称 数量 描述
u1 tag 1 标志位 7
u2 name_index 1 此索引指向 CONSTANT_Utf8_info 类型的常量,表示类或接口的全限定名

2A ~ 2B:0x001F,十进制为 31,指向第 31 项常量。

E9:0x01,第 31 项常量的标志位,属于 CONSTANT_Utf8_info 类型。

EA ~ EB:0x0005,此字符串字面量占用 5 个字节。(每个英文字符占用 1 个字节)

EC ~ F0:0x4861707079,使用 UTF-8 解码,得 Happy;所以此类的全限定名为 Happy,在默认包下。

图 2.2.4-1 类索引的查找过程

171 ~ 172:0x0009 这两个字节称为父类索引(super_class),用于确定父类的全限定名,除了 java.lang.Object 外,所有类都有父类,索引都不为 0。[1]

0x0009,指向第 9 项常量;第 9 项常量属于 CONSTANT_Class_info 类型,它的 name_index 指向第 32 项常量,第 32 项常量属于 CONSTANT_Utf8_info 类型,它的 bytes 经 UTF-8 解码后,为 java/lang/Object。[2]

173 ~ 174:接口数量(interfaces_count),0x0000,转十进制为 0,没有实现任何接口。

若实现了接口,后面每 2 个字节称为一个接口索引,每个接口索引指向某项 CONSTANT_Class_info 类型的常量,对应一个接口的全限定名。

[1] 若是接口类型,父类索引对应的全限定名为 java/lang/Object,接口索引对应的才是此接口继承的父接口全限定名。

[2] class 文件里,所有的 “.” 都被斜杆 “/” 代替,例 “java.lang.Object” 变为 “java/lang/Object”。

2.2.5 字段表集合

表 2.2.5-1 字段表集合
Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000170: 00 02 00 00 00 0A 00 0B 00 00 00
00000180: 00 00 0C 00 0D 00 00

175 ~ 176:字段数量(fields_count),0x0002,类型中声明了 2 个字段。

接下来是字段表(field_info)结构,用于描述字段。

表 2.2.5-2 字段表结构
类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

attributes 是 attribute 的集合,用来存储一些额外的信息,如 java 代码编译成的字节码指令、final 常量值、方法抛出的异常列表…

177 ~ 178:字段访问标志(access_flags),识别字段的修饰符,0x0000。

表 2.2.5-3 字段访问标志
标志名称 标志值 含义
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_VOLATILE 0x0040 volatile
ACC_TRANSIENT 0x0080 transient
ACC_FINAL 0x0010 final
ACC_SYNIHETIC 0x1000 字段由编译器自动生成
ACC_ENUM 0x4000 枚举类型

0x0000 的每位都是 0,此字段没有修饰符。

179 ~ 17A:字段名索引(name_index),用于确定字段名;0x000A,指向第 10 项常量;第 10 项常量是 CONSTANT_Utf8_info 类型,它的 bytes 经解码后为 i,所以此字段名为 i。

17B ~ 17C:字段的描述符索引(descriptor_index),代表字段的描述符,用来描述字段的数据类型。

表 2.2.5-4 描述符字符含义
标识字符 含义
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”。

0x000B,指向第 11 项常量,第 11 项常量是 CONSTANT_Utf8_info 类型,它的 bytes 经解码后为 I,所以此字段类型为 int。

17D ~ 17E:属性数量(attributes_count),0x0000,这里为 0,自然就没有属性表了。

综合,此字段为 int i;

接下来是第 2 个字段;

17F ~ 180:0x0000,此字段无修饰符。

181 ~ 182:0x000C,此字段的字段名索引,指向第 12 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “name”,字段名为 name。

183 ~ 184:0x000D,此字段的描述符索引,指向第 13 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “Ljava/lang/String;”,此字段的类型为 java.lang.String。

185 ~ 186:0x0000,此字段没有多余的属性信息。

综合,此字段为 String name;

码 2.2.5-1 class 反编译后的字段表内容
 int i;descriptor: Iflags:java.lang.String name;descriptor: Ljava/lang/String;flags:

2.2.6 方法表集合

表 2.2.6-1 方法表集合
Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000180: 00 02 00 01 00 0E 00 0F 00
00000190: 01 00 10 00 00 00 30 00 02 00 01 00 00 00 10 2A
000001A0: B7 00 01 2A 04 B5 00 02 2A 12 03 B5 00 04 B1 00
000001B0: 00 00 01 00 11 00 00 00 0E 00 03 00 00 00 01 00
000001C0: 04 00 02 00 09 00 03 00 09 00 12 00 13 00 01 00
000001D0: 10 00 00 00 25 00 02 00 01 00 00 00 09 B2 00 05
000001E0: 12 06 B6 00 07 B1 00 00 00 01 00 11 00 00 00 0A
000001F0: 00 02 00 00 00 05 00 08 00 06

187 ~ 188:方法数量(methods_count),0x0002,类型中声明了 2 个方法。

接下来是方法表(method_info)结构,用于描述方法。

表 2.2.6-2 方法表结构
类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

189 ~ 18A:方法访问标志,识别方法的修饰符,0x0001,此方法是 public 的。

表 2.2.6-3 方法访问标志
标志名称 标志值 含义
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_TRANSIENT 0x0080 transient
ACC_SYNIHETIC 0x1000 由编译器自动生成

18B ~ 18C:0x000E,此方法的方法名索引,指向第 14 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “<init>”。

18D ~ 18E:0x000F,此方法的描述符索引,指向第 15 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 ()V

当描述符用来描述方法的参数列表和返回值时,按照先参数列表,后返回值的顺序;如 void some() 的描述符为 ()Vjava.lang.String toString() 的描述符为 ()Ljava/lang/String;void main(String[] args) 的描述符为 ([Ljava/lang/String;)V

所以 ()V 代表此方法没有形参,返回值类型为 void。

18F ~ 190:0x0001,方法的属性数量为 1。

每个属性应满足如下结构:

表 2.2.6-4 属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 attribute_length
表 2.2.6-5 不同属性的含义
属性名称 使用位置 含义
Code 方法表 Java 代码编译成的字节码指令
ConstantValue 字段表 由 final 关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为 deprecated 的方法和字段
Exceptions 方法表 方法抛出的异常列表
EnclosingMethod 类文件 仅当一个类为局部内部类或匿名类时才能拥有此属性,用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表

191 ~ 192:属性名称的索引(attribute_name_index),0x0010,指向第 16 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “Code”,Code 属性是方法体中的代码的字节码描述。

方法体里面的代码经过 javac 编译后,最终变为字节码指令存储在 Code 属性内,结构如下:

表 2.2.6-6 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 attribute_count 1
attribute_info attributes attributes_count

193 ~ 196:属性值占用的长度(attribute_length),0x00000030,转十进制为 48;从 197 ~ 1C6 都是 Code 属性的内容,这个属性表的长度为 48 + 6 = 54 个字节。

197 ~ 198:0x0002,操作数栈(Operand Stack)深度的最大值(max_stack)。JVM 运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

199 ~ 19A:0x0001,局部变量表所需的存储空间(max_locals)。max_locals 的单位是变量槽(Slot),变量槽是 JVM 为局部变量分配内存所使用的最小单位。

不超过 32 位的数据类型:byte、char、float、int、short、boolean、returnAddress 等,每个局部变量占用一个变量槽。

double、long 这两种 64 位的数据类型,使用两个变量槽存放。[1]

19B ~ 19E:0x00000010,字节码长度(code_length)为 16。[2]

19F ~ 1AE:存储字节码指令的一系列字节流(code)[3],0x2AB700012A04B500022A1203B50004B1,每个字节码代表的指令:

表 2.2.6-7 指令含义
字节码 助记符 含义
0x2A aload_0 将局部变量表中的第 1 个变量槽中的引用类型的本地变量推送至栈顶[4]
0xB7 invokespecial 调用栈顶数据所指向的对象的实例初始化方法、父类方法、私有方法,后跟一个 u2 类型的参数说明调用哪个方法
0x0001 u2 类型 这是 invokespecial 指令的参数,代表一个符号引用,指向第 1 项 CONSTANT_Methodref_info 类型的常量:java/lang/Object."\<init>":()V,说明应调用 Object 的无参构造
0x2A aload_0 将第 1 个变量槽中为引用类型的本地变量推送至栈顶
0x04 iconst_1 将 int 类型的 1 推送至栈顶
0xB5 putfield 为栈顶数据指向的对象的实例变量赋值(显式初始化)
0x0002 u2 类型 这是 putfield 指令的参数,代表一个符号引用,指向第 2 项 CONSTANT_Fieldref_info 型常量:Happy.i:I,应为 Happy 类的 int 类型的变量名为 i 的变量赋值
0x2A aload_0 将第 1 个变量槽中为引用类型的本地变量推送至栈顶
0x12 ldc 将 int、float 或 String 类型的常量值从常量池中推送至栈顶
0x03 u1 类型 ldc 指令的参数,代表某字面量;此时推送的是第 3 项常量:“java”
0xB5 putfield 为栈顶数据指向的对象的实例变量赋值(显式初始化)
0x0004 u2 类型 这是 putfield 指令的参数,代表一个符号引用,指向第 4 项 CONSTANT_Fieldref_info 型常量:Happy.name:Ljava/lang/String;,应为 Happy 类的 String 类型的变量名为 name 的变量赋值
0xB1 return 从当前方法返回 void

这段字节码代表的含义:调用 Object 的无参构造方法,为 this 指向的对象的 int 类型的实例变量 i 赋值 1,为 this 指向的对象的 String 类型的实例变量 name 赋值 “java”,然后方法结束,返回值为 void。

1AF ~ 1B0:异常表长度(exception_table_length),0x0000,即方法抛出的异常个数为 0。

如果存在异常表,则结构如下:

表 2.2.6-8 异常表结构
类型 名称 数量 描述
u2 start_pc 1 开始位置(相对于方法体的位置,下同)
u2 end_pc 1 结束位置
u2 handler_pc 1 处理位置
u2 catch_type 1 此索引指向一个 CONSTANT_Class_info 型的常量

如果当字节码从第 i 行抛出了类型为 catch_type 或其子类的异常,i ∈\in∈ [start_pc,end_pc) ,则转到第 handler_pc 行继续处理;当 catch_type 为 0 时,表示任何异常情况都需要转到 handler_pc 处处理。

以下为例:

码 2.2.6-1 异常处理源码及反编译后的指令
public int some() throws Exception {int x;try {x = 1;return x;} catch (Exception e) {x = 2;return x;} finally {x = 3;}
}
public int some() throws java.lang.Exception;descriptor: ()Iflags: ACC_PUBLICCode:// {操作数栈最大深度 1} {局部变量表同时生存的局部变量所占最大的槽数 5} {参数个数 1}stack=1, locals=5, args_size=1// --- try ---// x = 1;对应两个指令0: iconst_1 // 将 int 类型的常量 1 加载到操作数栈顶 {1} {this}1: istore_1 // 将栈顶 1 从操作数栈取出,存储到局部变量表的第 2 个变量槽 {} {this, 1}// 将 1 重新读到操作数栈顶,准备返回    2: iload_1 // 将局部变量表的第 2 个变量槽中的值复制到操作数栈顶 {1} {this, 1}// --- finally ---    // x = 3;3: istore_2 // 将 1 取出,存储到第 3 个变量槽中 {} {this, 1, 1}4: iconst_3 // 将 int 类型的 3 复制到操作数栈顶 {3} {this, 1, 1}   5: istore_1 // 将 3 取出,存储到第 2 个变量槽 {} {this, 3, 1}// 将 1 重新读到操作数栈顶,准备返回    6: iload_2 // 将第 3 个变量槽的值复制到操作数栈顶 {1} {this, 3, 1}7: ireturn // 方法结束,返回操作数栈顶数 1// --- catch ---    // 捕获异常,给 catch 中定义的异常 e 赋值 8: astore_2 // 将异常取出,存储到第 3 个变量槽中 {} {this, ?, e}// x = 2;    9: iconst_2 // 将常量 2 复制到操作数栈顶 {2} {this, ?, e}10: istore_1 // 将 2 取出,存储到第 2 个变量槽 {} {this, 2, e}// 将 2 重新读到操作数栈顶,准备返回    11: iload_1 // 将第 2 个变量槽的值复制到操作数栈顶 {2} {this, 2, e}// --- finally ---    // x = 3;    12: istore_3 // 将 2 取出,存储到第 4 个变量槽 {} {this, 2, e, 2}13: iconst_3 // 将常量 3 复制到操作数栈顶 {3} {this, 2, e, 2}    14: istore_1 // 将 3 取出,存储到第 2 个变量槽 {} {this, 3, e, 2}// 将 2 重新读到操作数栈顶,准备返回  15: iload_3 // 将第 4 个变量槽的值复制到操作数栈顶 {2} {this, 3, e, 2}16: ireturn // 方法结束,返回 2// --- throw 异常 ---17: astore        4 // 将异常 e 取出,存储到第 5 个变量槽 {} {this, ?, ?, ?, e}// --- finally ---// x = 3;    19: iconst_3 // 将常量 3 复制到操作数栈顶 {3} {this, ?, ?, ?, e}20: istore_1 // 将 3 取出,存储到第 2 个变量槽 {} {this, 3, ?, ?, e}// 将异常 e 重新读到操作数栈顶,准备上抛给调用者21: aload         4 // 将第 5 个变量槽的值复制到操作数栈顶 {e} {this, 3, ?, ?, e}23: athrow // 抛出异常Exception table:from    to  target type// 如果 [0, 4) 出现 Exception 异常,跳转至 8:0     4     8   Class java/lang/Exception// 如果 [0,4) 出现 Exception 以外的异常,捕获不了,跳转至 17:0     4    17   any// 如果 [8, 13) 出现任意异常,转到 17:8    13    17   any

若 try 中没有抛出异常,返回 1;若 try 中抛出的异常可被 catch 捕获,跳转至 8:,返回 2;若抛出的异常没有被捕获,跳转至 17,将异常上抛给调用者;无论是否抛出异常,finally 都会执行。

紧接着是 Code 属性的属性;

1B1 ~ 1B2:0x0001,Code 的属性数量为 1。

1B3 ~ 1B4:属性名称的索引(attribute_name_index),0x0011,指向第 17 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “LineNumberTable”,此属性描述 Java 源码行号与字节码行号(字节码的偏移量)之间的对应关系。

不是运行时必须的属性,可以编译时使用 -g:none 或 -g:lines 选项取消这项信息。

如果不选择生成 LineNumberTable 属性,当抛出异常时,堆栈中将不会显示出错的行号;在调试程序时,也无法按照源码行来设置断点。

表 2.2.6-9 LineNumberTable 属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

1B5 ~ 1B8:0x0000000E,属性值长度为 14;从 1B9 ~ 1C6 都是此属性的内容。这个属性表的长度为 14 + 6 = 20 个字节,从 1B3 ~ 1C6。

1B9 ~ 1BA:0x0003,行号表长度为 3。

表 2.2.6-10 line_number_info 结构
类型 名称 数量
u2 start_pc 1
u2 line_number 1

1BB ~ 1BC:0x0000,字节码行号 0。

1BD ~ 1BE:0x0001,源码行号 1。

1BF ~ 1C0:0x0004,字节码行号 4。

1C1 ~ 1C2:0x0002,源码行号 2。

1C3 ~ 1C4:0x0009,字节码行号 9。

1C5 ~ 1C6:0x0003,源码行号 3。

Code 属性结束,此方法结束。

接着是下一个方法表;

1C7 ~ 1C8:方法的访问标志 0x0009,第 1、4 位为 1,此方法是 public、static 修饰的。

1C9 ~ 1CA:方法名索引 0x0012,指向第 18 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “main”。

1CB ~ 1CC:0x0013,此方法的描述符索引,指向第 19 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 ([Ljava/lang/String;)V,一个返回值类型为 void,形参为 String[] 类型的方法。

1CD ~ 1CE:0x0001,方法的属性数量为 1。

1CF ~ 1D0:0x0010,属性名称的索引,指向第 16 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “Code”。

1D1 ~ 1D4:0x00000025,属性值长度为 37:1D5 ~ 1F9。

1D5 ~ 1D6:0x0002,操作数栈的深度的最大值为 2。

1D7 ~ 1D8:0x0001,局部变量表的存储空间为 1。

1D9 ~ 1DC:0x00000009,存储字节码指令的字节长度为 9。

1DD ~ 1E5:0xB200051206B60007B1;

表 2.2.6-11 一系列字节码指令代表的含义
字节码 助记符 含义
0xB2 getstatic 获取指定类的类变量,并将其值压入操作数栈顶
0x0005 u2 getstatic 指令的参数,代表一个符号引用,指向第 5 项 CONSTANT_Fieldref_info 类型的常量:java/lang/System.out:Ljava/io/PrintStream;,说明获取的是 System 类型的 out 变量
0x12 ldc 将 int、float、String 型常量值从常量池中复制到操作数栈顶
0x06 u1 ldc 指令的参数,代表某字面量,指向第 6 项 CONSTANT_String_info 类型的常量:“Hello World”
0xB6 invokevirtual 调用栈顶引用类型的数据的实例方法
0x0007 u2 invokevirtual 指令的参数,指向第 7 项 CONSTANT_Methodref_info 类型的常量:java/io/PrintStream.println:(Ljava/lang/String;)V,代表调用 PrintStream 类的 println(String)方法。
0xB1 return 从当前方法返回 void

综上,这段字节码对应的源码应是 System.out.println("Hello World");

1E6 ~ 1E7:异常表长度,0x0000,即方法抛出的异常个数为 0。

紧接着是 Code 属性的属性;

1E8 ~ 1E9:0x0001,属性数量为 1。

1EA ~ 1EB:0x0011,属性名称的索引,指向第 17 项常量:“LineNumberTable”。

1EC ~ 1EF:0x0000000A,属性值长度为 10:1EF ~ 1F8。

1F0 ~ 1F1:0x0002,行号表长度为 2。

1F2 ~ 1F3:0x0000,字节码行号 0。

1F4 ~ 1F5:0x0005,源码行号 5。

1F6 ~ 1F7:0x0008,字节码行号 8。

1F8 ~ 1F9:0x0006,源码行号 6。

码 2.2.6-2 方法表集合
public Happy();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_05: iconst_16: putfield      #2                  // Field i:I9: aload_010: ldc           #3                  // String java12: putfield      #4                  // Field name:Ljava/lang/String;15: returnLineNumberTable:line 1: 0line 2: 4line 3: 9public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #6                  // String Hello World5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 5: 0line 6: 8
}
SourceFile: "Happy.java"

[1] 并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为 max_locals 的值。

操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。

Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所复用,javac 编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出 max_locals 的大小。

[2] 虽然它是一个 u4 类型的长度值,理论上最大值可以达到 232,但是《Java虚拟机规范》中明确限制了一个方法不允许超过 65535 条字节码指令,即它实际只使用了 u2 的长度,如果超过这个限制,javac 编译器就会拒绝编译。

[3] code 存储源代码编译后生成的字节码指令。每个字节码指令就是一个 u1 类型的单字节,当虚拟机读取到 code 中的一个字节时,就可以对应找出这个字节代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。

1 个字节最多表示 256 条指令。目前,《Java虚拟机规范》已经定义了其中约 200 条编码值对应的指令含义,编码与指令之间的对应关系可查阅《深入理解 Java 虚拟机》的附录 C:“虚拟机字节码指令表”。

[4] 表面上无参,其实在任何实例方法里,都可以通过 “this” 关键字访问到此方法所属的对象。

在 javac 编译时,把对 this 关键字的访问转变为对一个普通方法参数的访问,在 JVM 调用实例方法时自动传入此参数。

因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留第一个变量槽来存放对象实例的引用。

2.2.7 属性表集合

接下来是类文件的属性。

表 2.2.7-1 属性表集合
Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
000001F0: 00 01 00 14 00 00
000001E0: 00 02 00 15

1FA ~ 1FB:0x0001,属性个数为 1;

1FC ~ 1FD:0x0014,属性名称的索引(attribte_name_index),指向第 20 项 CONSTANT_Utf8_info 类型的常量:“SourceFile”,此属性用于记录生成 class 文件的源码文件名称。

此属性是可选的,可以使用 -g:none 或 -g:source 选项取消这项信息。

如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

表 2.2.7-2 SourceFile 属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

1FE ~ 1E1:0x00000002,属性值长度(attribute_length)为 2。

1E2 ~ 1E3:0x0015,源文件名索引(sourcefile_index),指向第 21 项 CONSTANT_Utf8_info 型常量:“Happy.java”;所以源文件名应为 “Happy”。

对 class 文件的解析到此结束,下面是从 《深入理解 Java 虚拟机》摘抄的一部分属性表。

Exceptions 属性

例举方法声明抛出的异常,位于方法表结构的属性表中。

表 2.2.7-3 Exceptions 属性结构
类型 名称 数量
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 中的每个成员都是对常量池的有效索引,指向 CONSTANT_Class_info 型常量,表示异常的类型。

LocalVariableTable 属性

描述栈帧中局部变量表的变量与源码定义的变量之间的关系,在 Code 属性的属性表中。

不是运行必需的属性,可以编译时使用参数 -g:none 或 -g:vars 取消此项信息。

影响:所有参数名称都会丢失,IDE 会使用 arg0、arg1 之类的占位符代替原有参数名,在调试期间无法根据参数名称从上下文中获取参数值。

表 2.2.7-4 LocalVariableTable 属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

local_variable_table_length:局部变量个数。

表 2.2.7-5 local_variable_table 表结构
类型 名称 数量
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1

start_pc:局部变量开始的字节码偏移量

length:作用范围覆盖的长度

局部变量的作用域在 [start_pc, start_pc + length) 内。

name_index:局部变量名索引

descriptor_index:局部变量的描述符索引

index:局部变量在栈帧的局部变量表中变量槽的位置。当变量数据类型大于 32 位时,它占用的变量槽为 index、index + 1 这两个。

JDK5 引入泛型后,新增属性 LocalVariableTypeTable,仅将 descriptor_index 替换了字段的特征签名(Signature)[1],signature_index 是一个索引,指向 CONSTANT_Utf8_info 型常量。

ConstantValue 属性

被 final 修饰的字段,在声明时使用字面量的方式赋值,field_info 结构的属性表中会生成此项属性,目的是通知 JVM 自动为类变量赋值。[2]

码 2.2.7-1 声明时不赋值
final static int I;
static {I = 4;
}

此字段表不会有 ConstantValue 属性,在 <clinit> 方法中赋值。

码 2.2.7-2 非字面量形式赋值:调用构造器
final static String STR = new String("Hello");

也不会有 ConstantValue 属性,在 <clinit> 方法中赋值。

码 2.2.7-3 非字面量形式赋值:调用方法
final static Integer I = 3;

隐式调用 Integer.valueOf(3)方法,不会生成 ConstantValue 属性,在 <clinit> 方法中赋值。

访问变量,必须通过指向 CONSTANT_Fieldref_info 的索引,定位到对象地址,再获取此对象的某个字段值。

访问常量,直接从常量池中获取值(只包括基本数据类型和字符串字面量,如 CONSTANT_Integer_info、CONSTANT_String_info 等字面量类型)。

码 2.2.7-4 变量赋值
int i1 = 3;
public void some() {int i2 = i1;
}
0: aload_0
1: getfield      #2                  // Field i1:I
4: istore_1
5: return
码 2.2.7-5 常量赋值
final int i1 = 3;
public void some() {int i2 = i1;
}
0: iconst_3
1: istore_1
2: return
表 2.2.7-6 ConstantValue 属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1

constantvalue_index:此索引指向常量池中一个字面量常量,根据字段类型不同,可以是 CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、 CONSTANT_Integer_info和CONSTANT_String_info 常量中的一种。

BootstrapMethods 属性

此属性用于保存 invokedynamic 指令引用的引导方法限定符。

表 2.2.7-7 BootstrapMethods 属性结构
类型 名称 数量 描述
u2 attribute_name_index 1 指向 CONSTANT_Utf8_info 型常量,值为 “BootstrapMethods”
u4 attribute_length 1 属性长度,不包括开始的 6 个字节
u2 num_bootstrap_methods 1 引导方法的数量
bootstrap_method_info bootstrap_method_table num_bootstrap_methods 引导方法表
表 2.2.7-8 bootstrap_method_table 结构
类型 名称 数量 描述
u2 bootstrap_method_ref 1 指向 CONSTANT_MethodHandle_info 型常量
u2 num_bootstrap_arguments 1 引导方法参数数量
u2 bootstrap_argument num_bootstrap_arguments 索引指向某项常量

0x0035:转十进制为 53,指向第 53 项 CONSTANT_Utf8_info 型常量,bytes 解码为 “BootstrapMethods”。

0x0000000C:转十进制为 12,属性剩余长度为 12 个字节。

0x0001:引导方法数量为 1。

接下来是第一个引导方法:

0x0036:转十进制为 54,指向第 54 项 CONSTANT_MethodHandle_info 型常量,代表一个方法句柄,REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(...)... 代表调用某静态方法。

0x0003:引导方法参数数量为 3。

0x003D:转十进制为 61,指向第 61 项 CONSTANT_MethodType_info 型常量,代表一个方法类型,(I)V 一个int 类型的形参、返回值类型为 void 的方法。

0x003E:转十进制为 62,指向第 62 项 CONSTANT_MethodHandle_info 型常量,REF_invokeStatic com/cqh/arr3/Test.lambda$main$0:(I)V 调用 Test 类型的 lambda… 方法。

0x003D:转十进制为 61…

[1] 引入泛型后,字段的描述符中泛型的参数化类型被擦除,不能准确描述泛型类型。如 List<String> list 的描述符 descriptor 为 “Ljava/util/List”;

特征签名(Signature)多了一项参数化类型的信息: “Ljava/util/List<Ljava/lang/String;>;”。

扩展:

在 Java 代码层面上的方法特征签名只包括方法名称和参数的个数、类型、顺序;而在字节码中的特征签名还包括方法返回值及受检异常表,方法的描述符包括参数列表和返回值。

方法特征签名最重要的任务就是作为方法独一无二不可重复的 ID。重载(Overload)即方法名相同,但特征签名不同。

[2] 如果 field_info 结构表示的非静态字段(如实例变量)包含了 ConstantValue 属性,那么这个属性必须被虚拟机所忽略。

2.3 类加载过程

内容导视:

  • 类的初始化时机
  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化

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

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。

图 2.3-1 类的生命周期

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始[1]。而解析阶段不一定,它在某些情况下可以在初始化阶段之后再开始,当运行中的程序真正使用某个符号引用时再去解析它[2],这是为了支持 Java 语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

[1] 例如加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分。说的是不同阶段的开始时间保持着固定的先后顺序。

[2] 对于方法而言,如果方法在程序运行之前就有一个可确定的调用版本,且在运行期间不可改变,如静态方法或私有方法,适合在类加载阶段时进行解析,也称静态绑定或前期绑定。

2.3.1 类的初始化时机

初始化时机

类加载过程的第一个阶段 “加载” 何时开始,《Java 虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

但是对于初始化阶段,《Java虚拟机规范》 则是严格规定了有且只有六种情况必须立即对类进行 “初始化”(而加载、验证、准备自然在此之前就开始了)。

1)遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型 Java 代码场景有:

  • 创建对象时
  • 访问类变量时(被 final 修饰、已在编译时就把值放入常量池的类变量除外)
  • 修改类变量时
  • 调用类方法时

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

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

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

5)当使用 JDK7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

6)当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

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

例子如下:

码 2.3.1-1 通过子类访问父类的类变量
class PassiveReference1 {public static void main(String[] args) {int i = Sub.i;// 只会初始化 Super 类}
}
class Super {static {System.out.println("父类初始化!");}static int i = 3;
}
class Sub extends Super {static {System.out.println("字类初始化!");}
}

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

至于是否要触发子类的加载和验证阶段,在《Java虚拟机规范》中并未明确规定,所以这点取决于虚拟机的具体实现。

对于 HotSpot 虚拟机来说,可通过 -XX:+TraceClassLoading 参数观察到此操作是会导致子类加载的。

码 2.3.1-2 通过数组定义引用类
Sub[] subArr = new Sub[1];
System.out.println(subArr);// [Lcom.cqh.loader.Sub;@1b6d3586

不会触发 Sub 类的初始化阶段,但触发一个名为 “[Lcom.cqh.loader.Sub” 的类的初始化阶段,它是一个由虚拟机自动生成的、直接继承于 java.lang.Object 的子类,创建动作由字节码指令 newarray 触发。

这个类代表了一个元素类型为 com.cqh.loader.Sub 的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为 public 的 length 属性和 clone 方法)都实现在这个类里。

码 2.3.1-3 访问常量
class PassiveReference2 {public static void main(String[] args) {String thank = ConstantClass.THANK;}
}
class ConstantClass {static {System.out.println("constant init!");}// 在准备阶段时已赋值,不需初始化;创建对象、调用方法赋值,则会触发初始化static final String THANK = "Are You OK!";
}

常量的值(仅包括字面量)已在编译阶段存入调用类的常量池中,访问此变量时,不是通过 getfield 等指令访问字段(通过访问 CONSTANT_Fieldref_info 型常量,获取字段位置,进而得到值),而是通过 ldc、 iconst_<i> 等指令直接从常量池中取值(访问 CONSTANT_Integer_info、CONSTANT_String_info 等字面量类型常量获取字面量),没有直接引用到定义常量的类。

接口的加载过程

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

接口中不能使用 “static{}” 语句块,但编译器仍然会为接口生成 “<clinit>” 类构造器,用于初始化接口中所定义的常量(不以字面量形式赋值时)。

2.3.2 加载

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

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

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

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

运行时数据区域面向对象时再讲,Class 反射时再讲。

数组类型的加载

数组类本身不通过类加载器创建,它是由 Java 虚拟机直接在内存中动态构造出来的。如果数组的组件类型[1]是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组将被标识在加载该组件类型的类加载器的类名称空间上。

如果数组的组件类型不是引用类型,Java 虚拟机将会把数组标记为与引导类加载器关联。

数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为 public,可被所有的类和接口访问到,如 int[]。


类加载器

类加载器通过一个类的全限定名获取描述该类的二进制字节流

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

对于任意一个类型,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

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

码 2.3.2-1 自定义类加载器
ClassLoader myLoader = new ClassLoader() {@Overridepublic 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("com.cqh.loader.TestClassLoader").newInstance();
System.out.println(obj.getClass());// class com.cqh.loader.TestClassLoader
System.out.println(obj instanceof TestClassLoader);// false

JDK 8及之前版本,绝大多数 Java 程序都会使用到以下 3 个系统提供的类加载器来进行加载。

启动类加载器

启动类加载器(Bootstrap Class Loader),使用 C/C++ 语言编写,也称引导类加载器

这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数所指定的路径中存放的,而且是 Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机的内存中。

启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用 null 代替即可。

扩展类加载器

扩展类加载器(Extension Class Loader),这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 java 代码的形式实现的。

它负责加载 <JAVA_HOME>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。

JDK 的开发团队允许用户将具有通用性的类库放置在 ext 目录里以扩展 JavaSE 的功能,在 JDK9,这种扩展机制被模块化带来的天然的扩展能力所取代。

应用程序类加载器

应用程序类加载器(Application Class Loader),这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现。

由于应用程序类加载器是 ClassLoader 类中的 getSystemClassLoader 方法的返回值,所以有些场合中也称它为 “系统类加载器”。

它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型

双亲委派模型(Parents Delegation Model)的工作过程:如果一个类加载器收到了类加载的请求,它会把这个请求委派给父类加载器,每个层次的类加载都是如此,因此所有的加载请求最终都到了最顶层的启动类加载器中。

只有当父类加载器无法完成这个加载请求时(搜索范围内没有找到所需的类),才会自己尝试去加载。

图 2.3.2-1 双亲委派模型

码 2.3.2-2 ClassLoader 类中双亲委派模型代码实现
private final ClassLoader parent;
/*** 加载指定名称的类,可以编写子类覆盖 loadClass 方法破坏双亲委派模型,* 正常情况下还是重写 findClass 方法*/
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// 首先,检查请求的类是否已经被加载过了Class<?> c = findLoadedClass(name);// 如果没被加载if (c == null) {long t0 = System.nanoTime();// 尝试让父类加载器加载此类try {if (parent != null) {c = parent.loadClass(name, false);// 如果父类加载器为空    } else {// 使用启动类加载器加载此类c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 到这里说明父类加载器无法完成加载请求,抛出了异常}if (c == null) {// 再调用自己的 findClass 方法进行类加载long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}

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

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

反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。 (安全机制禁止以 “java” 开头的包名,即使通过编译,运行时也会抛出 SecurityException)

JDK9 时,扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代,平台类加载器和应用程序类加载器都不再派生自 java.net.URLClassLoade,现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。

JDK9 中虽然仍然维持着三层类加载器和双亲委派的架构,但类加载的委派关系也发生了变动。

当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

[1] 组件类型(Component Type),指的是数组去掉一个维度的类型,如 int[][] 的组件类型是 int[] 类型。

元素类型(ElementType),指的是数组去掉所有维度的类型,如 int[][] 的元素类型是 int 类型。

2.3.3 验证

验证是连接阶段的第一步,这一阶段的目的是确保 class 文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

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

验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式验证

验证字节流是否符合 class 文件格式的规范,并且能被当前版本的虚拟机处理:

  • 是否以魔数 0xCAFEBABE 开头。
  • 主、次版本号是否在当前 Java 虚拟机接受范围之内。
  • 常量池中是否有不被支持的常量类型(检查常量 tag 标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF-8 编码的数据。
  • class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。

验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。

这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入 Java 虚拟机内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。

元数据验证

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

  • 这个类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同…)。

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

字节码验证

此阶段极其复杂,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。

这阶段对类的方法体(class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于 “在操作栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表中” 这样的情况。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

如果一个类型中有方法体的字节码没有通过字节码验证,那它肯定是有问题的;但如果一个方法体通过了字节码验证,也仍然不能保证它一定就是安全的。[1]

为了避免过多的执行时间消耗在字节码验证阶段中,JDK6 之后的 javac 编译器和 Java 虚拟机里进行了一项联合优化,把尽可能多的校验辅助措施挪到 javac 编译器里进行。

具体做法是给方法体 Code 属性的属性表中新增加了一项名为 “StackMapTable” 的新属性,这项属性描述了方法体所有的基本块(Basic Block,指按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,Java 虚拟机就不需要根据程序推导这些状态的合法性,只需要检查 StackMapTable 属性中的记录是否合法即可。

符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用时,这个转化动作将在连接的第三阶段 —— 解析阶段中发生。

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

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

  • 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当

    前类访问。

符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java 虚拟机将会抛出一个 java.lang.IncompatibleClassChangeError 的子类异常,典型的如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

[1] 不可能用程序来准确判定一段程序是否存在 Bug。

假设现在有一个 isRun 方法,可以检测任意方法是否抛出异常;参数是被检测的方法的名称,被执行的方法正常运行返回 true,抛出异常返回 false。

现有一个 paradox 方法:

码 2.3.3-1 悖论
public void paradox(String methodName) {// 如果被检测的方法没有抛出异常,paradox 就抛出异常if (isRun(methodName)) {throw new RuntimeException("与你作对!");// 如果被检测的方法抛出异常,paradox 就正常运行 } else {return;}
}

当被检测的 paradox 方法没有抛出异常时,paradox 方法就会抛出异常;paradox 抛出异常时,则 paradox 应正常运行。

它抛不抛异常都是错,只能是假设错误,根本没有这样的 isRun 方法。与 “理发师只给不给自己理发的人理发” 有异曲同工之妙。

2.3.4 准备

此阶段为类变量分配内存,并设置类变量初始值。(我习惯称默认初始化)

static int value = 123; value 在准备阶段之后的初始值为 0。

表 2.3.4-1 不同类型的初始值
数据类型 初始值
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
boolean false
char ‘\u0000’
reference null

从概念上讲,类变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在 JDK7 及之后,类变量则会随着 class 对象一起存放在 Java 堆中,这时候 “类变量在方法区中” 就完全是一种对逻辑概念的表述了。

如果此时类字段的字段属性表中存在 ConstantValue 属性,那在准备阶段变量值就会被初始化为 ConstantValue 属性所指定的初始值。如 static final int i = 5; 在准备阶段赋值 5。

2.3.5 解析

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

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

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

对同一个符号引用进行多次解析请求是很常见的事情,除 invokedynamic 指令以外,虚拟机实现可以对第一次解析的结果进行缓存,譬如在运行时直接引用常量池中的记录,并把常量标识为已解析状态,从而避免解析动作重复进行。

Java 虚拟机需要保证在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直能够成功;同样地,如果第一次解析失败了,其他指令对这个符号的解析请求也应该收到相同的异常,哪怕这个请求的符号在后来已成功加载进 Java 虚拟机内存之中。

对于 invokedynamic 指令,上面的规则就不成立了。当碰到某个前面已经由 invokedynamic 指令触发过解析的符号引用时,并不意味着这个解析结果对于其他 invokedynamic 指令也同样生效。

它对应的引用称为 “动态调用点限定符(Dynamically-Computed Call Site Specifier)”,这里 “动态” 的含义是指必须等到程序实际运行到这条指令时,解析动作才能进行,用于动态语言支持。

相对地,其余可触发解析的指令都是 “静态” 的,可以在刚刚完成加载阶段,还没有开始执行代码时就提前进行解析。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用和字符串类型字面量进行,分别对应于常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dynamic_info、CONSTANT_InvokeDynamic_info 和 CONSTANT_String_info。

类或接口的解析

码 2.3.5-1 CONSTANT_Class_info 型常量的结构
CONSTANT_Class_info {u1 tag;// 常量类型的标志位u2 name_index;// 指向 CONSTANT_Utf8_info 型常量,可按 UTF-8 解码 bytes 属性值得到全限定名
}

#8 = Class #10 // com/cqh/arr3/Sub

如果某符号引用在类 C 中,那虚拟机将会把符号引用代表的类 D 的全限定名(com.cqh.arr3.Sub)传递给 C 的类加载器去加载这个类 D。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败。[2]

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

字段的解析

码 2.3.5-2 CONSTANT_Fieldref_info 型常量结构
CONSTANT_Fieldref_info {u1 tag;// 常量类型的标志位u2 class_index;// 指向 CONSTANT_Class_info 型常量,解析此常量得到字段所在类或接口u2 name_and_type_index;// 指向 CONSTANT_NameAndType_info 型常量,可以得到字段名和类型
}

#13 = Fieldref #7.#14 // com/cqh/arr3/Sub.A:I

getfield #13 // Field com/cqh/arr3/Sub.A:I

先对字段所属的类或接口 C 的符号引用(class_index 索引指向的 CONSTANT_Class_info 型常量)解析,如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。

如果解析成功完成,搜索字段的步骤:

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

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

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

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

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

在实际情况中,javac 编译器往往会采取比上述规范更加严格一些的约束,譬如有一个同名字段同时出现在某个类的接口和父类当中,或者同时在自己或父类的多个接口中出现,按照解析规则仍是可以确定唯一的访问字段,但 javac 编译器就可能直接拒绝其编译为 class 文件。

码 2.3.5-3 同名字段在接口和父类中
interface Interface {int A = 0;
}static class Parent {public static int A = 3;
}static class Sub extends Parent implements Interface {}public static void main(String[] args) {// 对 A 的引用不明确System.out.println(Sub.A);
}
码 2.3.5-4 同名字段在接口和父类的接口中
interface Interface {int A = 0;
}
interface Interface1 {int A = 1;
}
static class Parent implements Interface1 {}static class Sub extends Parent implements Interface {}public static void main(String[] args) {// 对 A 的引用不明确System.out.println(Sub.A);
}

方法解析

码 2.3.5-5 CONSTANT_Methodref_info 型常量结构
CONSTANT_Fieldref_info {u1 tag;// 常量的标志位u2 class_index;// 方法所在类u2 name_and_type_index;// 方法名和形参列表
}

#10 = Methodref #7.#11 // com/cqh/arr3/Sub.some:()V

invokevirtual #10 com/cqh/arr3/Sub.some : ()V
invokestatic #17 com/cqh/arr3/Sub.some2 : ()V

先对实例方法所属的类或接口 C 的符号引用解析,如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致方法符号引用解析的失败。

如果解析成功,接下来虚拟机将会按照如下步骤进行后续的方法搜索:

1)如果 C 是个接口,那就直接抛出 java.lang.IncompatibleClassChangeError 异常。

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

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

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

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

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

接口方法解析

码 2.3.5-6 CONSTANT_InterfaceMethodref_info 型常量结构
CONSTANT_InterfaceMethodref_info {u1 tag;u2 class_index;u2 name_and_type_index;
}

#9 = InterfaceMethodref #10.#11 // com/cqh/arr3/Interface.some:(Ljava/lang/String;)V

invokestatic #9 // InterfaceMethod com/cqh/arr3/Interface.some:(Ljava/lang/String;)V

先对接口方法所属的接口 C 的符号引用解析,如果解析成功,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:

1)如果 C 是个类而不是接口,抛出 java.lang.IncompatibleClassChangeError 异常。

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

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

4)对于规则 3,由于 Java 的接口允许多重继承,如果C的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找,《Java虚拟机规范》中并没有进一步规则约束应该返回哪一个接口方法。但与之前字段查找类似地,不同发行商实现的 javac 编译器有可能会按照更严格的约束拒绝编译这种代码来避免不确定性。

码 2.3.5-7 不兼容的方法
// 类型 com.cqh.arr3.Interface1 和 com.cqh.arr3.Interface2 不兼容;
interface Interface extends Interface1, Interface2 {}
interface Interface1 {default void some() {System.out.println("1");}
}
interface Interface2 {default void some() {System.out.println("2");}
}

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

在 JDK9 中增加了接口的私有静态方法,也有了模块化的访问约束,所以从 JDK9 起,接口方法的访问也完全有可能因访问权限控制而出现 java.lang.IllegalAccessError 异常。

方法类型解析

方法类型表示方法句柄接受和返回的参数和返回类型。

码 2.3.5-8 CONSTANT_MethodType_info 型常量结构
CONSTANT_MethodType_info {u1 tag;u2 descriptor_index;// 此索引指向 CONSTANT_Utf8_info 型常量,表示方法的描述符
}

#61 = MethodType #21 // (I)V,即形参为 int 类型,返回值为 void 的一个方法。

先对此方法类型所封装的方法描述符中所有的类型符号引用进行解析,在解析过程中如果有任何异常发生,也会当作解析方法类型的异常而被抛出。

解析方法类型的结果是得到一个对 java.lang.invoke.MethodType 实例的引用,它可用来表示一个方法的描述符。

MethodType mt = MethodType.methodType(void.class, int.class);// 第一个参数是返回值类型,后面的参数是形参的类型。

方法句柄解析

方法句柄类的实质是将某个具体的方法映射到 MethodHandle 上,通过 MethodHandle 直接调用该句柄所引用的底层方法,实际就是对可执行方法的引用。

码 2.3.5-9 CONSTANT_MethodHandle_info 型常量结构
CONSTANT_MethodHandle_info {u1 tag;u1 reference_kind;// 方法句柄的类型,代表执行的指令u2 reference_index;// 此索引指向某项常量
}
// 方法句柄类型为 6,调用静态方法
// 调用返回值类型为 CallSite 的静态方法 LambdaMetafactory.metafactory
#56 = MethodHandle 6:#57 // REF_invokeStatic
java/lang/invoke/LambdaMetafactory.metafactory:
(Ljava/lang/invoke/MethodHandles$Lookup;
Ljava/lang/String;Ljava/lang/invoke/MethodType;
Ljava/lang/invoke/MethodType;
Ljava/lang/invoke/MethodHandle;
Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;

[3]每个被 Java 虚拟机解析的方法句柄都有一个被称为字节码行为(Bytecode Behavior)的等效指令序列(Equivalent Instruction Sequence),它由方法句柄的类型(reference_kind)值来标识。

reference_kind 项的值必须在 [1,9] 之间,它决定了方法句柄的类型。

指令序列到字段或方法的符号引用被标记为:C.x:T。C 表示字段或方法所属的类或接口,x 表示字段 f 或方法 m 的名称,T 表示字段或方法的描述符。

com/cqh/arr3/FieldResolution.i:Icom/cqh/arr3/FieldResolution.some:()V

表 2.3.5-1 九种方法句柄的类型值和描述
类型 描述 字节码行为
1 REF_getField(访问对象实例变量) getfield C.f:T
2 REF_getStatic(访问静态变量) getstatic C.f:T
3 REF_putField(设置对象实例变量) putfield C.f:T
4 REF_putStatic(设置静态变量) putstatic C.f:T
5 REF_invokeVirtual(调用实例方法) invokevirtual C.m:(A*)T
6 REF_invokeStatic(调用静态方法) invokestatic C.m:(A*)T
7 REF_invokeSpecial(调用私有方法和实例初始化方法、super.方法) invokespeical C.m:(A*)T
8 REF_newInvokeSpecial(创建对象并调用实例初始化方法) new C; dup; invokespecial C.<init>:(A*)void
9 REF_invokeInterface(调用接口方法) invokeinterface C.m:(A*)T

如果 reference_kind 为 1、2、3、4,则 reference_index 指向 CONSTANT_Fieldref_info 类型的常量,表示由一个字段创建的方法句柄。

码 2.3.5-10 访问字段
// 产生一个访问静态字段的方法句柄,此静态字段在 FieldResolution 类,字段名为 i,类型是 int
MethodHandle mh = MethodHandles.lookup().findStaticGetter(FieldResolution.class, "i", int.class);
// 访问此静态字段的值
Object invoke = mh.invoke();
System.out.println(invoke);// 33
// 通过反射获取字段
Field field = Sub.class.getDeclaredField("i");
// 打破封装,可以访问 private 修饰的字段
field.setAccessible(true);
// 生成一个方法句柄,授予对反射字段的读取访问权限
// bindTo 方法将某引用绑定到方法句柄的第一个参数,也就是 “this”
MethodHandle methodHandle = MethodHandles.lookup().unreflectGetter(field).bindTo(new Sub());
// 访问 new Sub() 对象的实例变量 i
int i = (int) methodHandle.invoke();
System.out.println(i);// 3
// 生成请求的返回类型的方法句柄,每次调用它时都会返回给定的常量值
MethodHandle constant = MethodHandles.constant(int.class, 5);
System.out.println(constant.invoke());// 5
// 访问 Sub 类的 int 类型的实例变量 i
VarHandle vh = MethodHandles.lookup().findVarHandle(Sub.class, "i", int.class);
int i = (int) vh.get(new Sub());

如果 reference_kind 为 5、6、7、8,则 reference_index 指向 CONSTANT_Methodref_info 型常量,表示由类的方法或构造函数创建的方法句柄。

码 2.3.5-11 调用构造方法
// 使用无参构造(构造器返回值类型都是 void)生成一个方法句柄
MethodHandle mh = MethodHandles.lookup().findConstructor(FieldResolution.class, MethodType.methodType(void.class));
// 创建 FieldResolution 类型的对象,并调用无参的实例初始化方法(init)
FieldResolution invoke = (FieldResolution) mh.invoke();

如果 reference_kind 项的值是 9,则 reference_index 指向 CONSTANT_InterfaceMethodref_info 型常量,表示由接口方法创建的方法句柄。

码 2.3.5-12 调用接口方法
/*** 为虚方法(通常指实例方法)生成方法句柄,此方法在 Consumer 接口中,* 方法名为 access,无返回值类型,有一个 int 类型的形参,(this 为隐藏参数)* 并绑定到某个接口实现类对象。*/
MethodHandle mh = MethodHandles.lookup().findVirtual(Consumer.class, "access",MethodType.methodType(void.class, int.class)).bindTo(new ConsumerImpl());
// 调用实现类对象的 access 方法,传入实参 3
mh.invoke(3);
/*如果没有调用 bindTo 方法,调用实例方法需要传入对象,放在第一个位置,实际调用的是 obj.access(param) 方法invoke(obj, param)调用静态方法使用 findStatic,就不需要 obj 参数
*/
码 2.3.5-13 调用私有方法
/*** privateLookupIn 方法返回目标类 Sub.class 的 lookup 对象,以模拟所有支持的字节码行为* 包括私有访问。*/
MethodHandle some = MethodHandles.privateLookupIn(Sub.class, MethodHandles.lookup()).findVirtual(Sub.class, "some", MethodType.methodType(void.class, int.class)
).bindTo(new Sub());
some.invoke(3);
码 2.3.5-14 访问私有字段
// 访问 Sub 类的私有实例变量 i
MethodHandle some = MethodHandles.privateLookupIn(Sub.class, MethodHandles.lookup()).findGetter(Sub.class, "i", int.class
).bindTo(new Sub());
System.out.println(some.invoke());

解析方法句柄的符号引用 MH(从 CONSTANT_MethodHandle_info 结构得来):

  • 设 R 是 MH 中字段或方法的符号引用(从 CONSTANT_Fieldref、CONSTANT_Methodref… 结构中得来)
  • C 是 R 所处的类的符号引用
  • f 或 m 是 R 所引用的字段或方法的名称
  • T 是 R 所引用的方法的返回值类型或字段的数据类型
  • A* 是参数类型列表(如果 R 是方法)

先对 MH 中类、字段或方法的符号引用都进行解析,解析过程抛出异常,都看作是解析方法句柄抛出的异常。

解析所有符号引用后,获得一个对 java.lang.invoke.MethodType 实例的引用 o,此引用表示着方法句柄 MH。

如果方法 m 有 ACC_VARARGS 标志(接受可变长参数),则 o 是一个可变元方法句柄,否则 o 是固定元方法句柄[4]

如果 o 是可变元方法句柄,且 m 的参数列表为空,或参数列表的最后的一个参数不是数组类型,那么方法句柄解析就会抛出 IncompatibleClassChangeError 异常(这表示创建可变元方法句柄失败)。

o 所引用的 java.lang.invoke.MethodHandle 实例的类型描述符是一个 java.lang.invoke.MethodType 的实例,它是之前由方法类型解析时产生的。

调用点限定符解析

码 2.3.5-13 CONSTANT_InvokeDynamic_info 型常量结构
CONSTANT_InvokeDynamic_info {u1 tag;u2 bootstrap_method_attr_index;// 此索引指向 BootstrapMethods 属性表集合的第 index+1 项(index 从 0 开始)u2 name_and_type_index;// 此索引指向 CONSTANT_NameAndType_info 型常量,代表方法名和要实现的接口类型
}
Constant pool:#8 = NameAndType #9:#10 // access:()Lcom/cqh/arr3/Consumer;/*CONSTANT_InvokeDynamic_info 型常量,两个属性分别指向:#1:BootstrapMethods:1:#8:方法名:access,要实现的接口名:Consumer,无形参    */     #17 = InvokeDynamic #1:#8 // #1:access:()Lcom/cqh/arr3/Consumer;/*调用静态方法 LambdaMetafactory.metafactory*/#56 = MethodHandle 6:#57
/*
REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
*/  #63 = MethodType #22 //  (I)V#67 = MethodHandle 6:#68 // REF_invokeStatic com/cqh/arr3/Test.lambda$main$1:(I)V#68 = Methodref #12.#69 // com/cqh/arr3/Test.lambda$main$1:(I)V
{                            53: invokedynamic #17 // InvokeDynamic #1:access:()Lcom/cqh/arr3/Consumer;
}
BootstrapMethods:// 调用 LambdaMetafactory.metafactory,此方法作为引导方法,参数如下:/*Lookup lookup:查找上下文与调用者的访问权限,由 JVM 自动填充String name:要实现的方法名,InvokeDynamic.NameAndType.Name:“access”MethodType mt:调用点期望的方法参数的类型和返回值的类型,InvokeDynamic.NameAndType.Type:“()Lcom/cqh/arr3/Consumer”,无参,返回值类型为 Consumer,代表 invokedynamic 执行完后会返回一个 Consumer 类型的实例。MethodType mt2:函数对象要实现的方法类型,#63:(I)V,int 类型的形参,返回值类型为 voidMethodHandle mh:一个直接方法句柄, 描述在调用时将被执行的具体实现方法,#67MethodType mt3:函数接口方法替换泛型为具体类型后的方法类型, 通常和 mt2 一样, 不同的情况为泛型,#63*/1: #56 REF_invokeStatic 略// 引导方法参数Method arguments:#63 (I)V#67 REF_invokeStatic com/cqh/arr3/Test.lambda$main$1:(I)V#63 (I)V
// 生成的实现接口的内部类
InnerClasses:public static final #70= #66 of #68;
// Lookup=class java/lang/invoke/MethodHandles$Lookup of
// class java/lang/invoke/MethodHandles
// 实际调用的方法
private static void lambda$main$1(int);descriptor: (I)Vflags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETICCode:stack=2, locals=1, args_size=10: getstatic     #22 // Field java/lang/System.out:Ljava/io/PrintStream;3: iload_04: invokevirtual #28 // Method java/io/PrintStream.println:(I)V7: return
}

每一个 invokedynamic 指令的实例叫做一个动态调用点(dynamic call site), 动态调用点依靠引导方法来链接到具体的方法。

引导方法是由编译器生成,运行期当 JVM 第一次遇到 invokedynamic 指令时, 会调用引导方法来将 invokedynamic 指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来,引导方法的返回值永久的决定了调用点的行为。

解析调用点限定符的三个步骤:

  • 调用点限定符提供了对方法句柄的符号引用,它作为引导方法(Bootstrap Method)向动态调用点提供服务。解析这个方法句柄是为了获取一个对 java.lang.invoke.MethodHandle 实例的引用。
  • 调用点限定符提供了一个方法描述符,记作 TD。它是一个 java.lang.invoke.MethodType 实例的引用。可以通过解析与 TD 有相同的参数及返回值的方法类型的符号引用而获得。
  • 调用点限定符提供零至多个静态参数,用于传递与特定应用相关的元数据给引导方法。静态参数只要是对类、方法句柄或方法类型的符号引用,都需要被解析。

在解析调用点限定符的方法句柄符号引用,或解析调用点限定符中方法类型的描述符的符号引用时,或是解析任何的静态参数的符号引用时,任何与方法类型或方法句柄解析有关的异常都可以被抛出。

对不起,这里的 String 解析我搞错了,删掉了,重新写了文。

解析其它类型

对于 CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info 型常量而言,自身就包含它们所表示的常量值,可以直接使用,但还有一些实现可能做一些处理,比如小端存储,需要交换字节顺序。[6]

CONSTANT_Utf8_info、CONSTANT_NameAndType_info 型常量永远不会被指令直接引用,它们只有通过其它常量才能被引用,并且在那些引用常量被解析时才被解析。


[1] 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

[2] 如果 D 是一个数组类型,并且数组的元素类型为引用类型,加载此数组元素类型。接着由虚拟机生成一个代表该数组维度和元素的数组对象。

[3] Lookup

lookup 对象是创建方法句柄的工厂。方法句柄在调用时不执行访问检查,而是在创建时执行访问检查。

因此当创建方法句柄时,必须强制执行方法句柄访问限制,这些限制被强制执行的调用类被称为 Lookup 类 。

需要创建方法句柄的 Lookup 类将调用 MethodHandles.lookup 方法为自己创建一个工厂。

当创建 Lookup 工厂对象时,查找类的标识,并安全地存储在 Lookup 对象中。 查找类可以使用 Lookup 对象上的工厂方法来创建访问检查成员的方法句柄。 这包括允许查找类的所有方法,构造函数和字段,甚至是私有的。

CallSite

一个 invokedynamic 指令关联一个 CallSite,将所有的调用委托到 CallSite 的 target(MethodHandle)。

LambdaMetafactory.metafactory 返回 CallSite,再调用 target 方法获取方法句柄。

LambdaMetafactory

public static void main(String[] args) throws Throwable {String methodName = "accept";// 获取创建方法句柄的工厂MethodHandles.Lookup lookup = MethodHandles.lookup();// 返回值类型为 void,形参为 T(Object) 类型的方法类型MethodType type = MethodType.methodType(void.class, Object.class);// 获取方法句柄,对应 ConsumerImpl 类中的 accept 方法,方法类型为 typeMethodHandle mh = lookup.findVirtual(ConsumerImpl.class, methodName, type);// 实际方法类型:返回值类型为 void,形参为 IntegerMethodType actualType = MethodType.methodType(void.class, Integer.class);// 包含了要实现的接口类型,实际调用方法所处类MethodType factoryType = MethodType.methodType(Consumer.class, ConsumerImpl.class);// 获取创建代理对象的方法句柄/*"toAccept":要实现的方法名factoryType:包含要实现的接口、实际调用方法所在类,用于返回接口类型的实例type:要实现的方法类型mh:具体调用方法对应的方法句柄actualType:替换泛型为具体类型后的方法类型,通常情况等同 type*/MethodHandle mh2 = LambdaMetafactory.metafactory(lookup, "toAccept",factoryType, type, mh, actualType).getTarget();// 生成实现 Consumer 接口的内部类Consumer consumer = (Consumer) mh2.invoke(new ConsumerImpl());consumer.toAccept(3);
}
interface Consumer<T> {void toAccept(T i);
}
static class ConsumerImpl<T> {public void accept(T i) {System.out.println("ConsumerImpl.accept" + i);}
}

[4] 可变元方法句柄在调用 invoke 方法时,参数列表会有装箱动作(JLS),而调用 invokeExact 方法就像没有 ACC_VARAGS 标志一样。

MethodHandle mh = MethodHandles.lookup().findVirtual(Consumer.class, "access",MethodType.methodType(void.class, int[].class)).bindTo(new ConsumerImpl());
// 调用 ConsumerImpl 类的 void access(int[]) 方法或 void access(int...) 方法
mh.invoke(new int[] {2,3,4,5});
// 接受一个可变长参数适配器,它的作用是把形参列表中的最后一个数组类型的参数转换成对应类型的可变长度的参数
MethodHandle avc = mh.asVarargsCollector(int[].class);
/*使用 invokeExact 调用时,适配器将调用目标而不更改参数。当使用普通的、不精确的 invoke 调用时,如果调用者类型与适配器相同,则适配器将像 invokeExact 一样调用目标。否则将适配器将尾随的几个参数转为数组类型类型的参数(装箱),再进行调用。*/
avc.invoke(2,6,7,1);
// 抛出异常 java.lang.invoke.WrongMethodTypeException:预期 (int[]),结果传递 (int,int,int,int)
// avc.invokeExact(2,6,7,1);/*创建一个数组收集方法句柄,它接受给定数量的尾随位置参数并将它们收集到数组类型的参数中。只需传递给定数量的参数,当被调用时,该适配器替换 arrayLength 个尾随位置参数为数组类型的参数。比如调用 some(String str, int[] arr) 传入实参("zs", 98, 97, 99),尾随参数 98, 97, 99 封装成一个 int 类型的数组,实际传入的实参是("zs", new int[]{98, 97, 99})*/
MethodHandle ac = mh.asCollector(int[].class, 3);
ac.invokeExact(8,2,6);

[5] 我所指的拷贝是其引用保存的内容,而不是拷贝引用其本身在内存中的地址。

int[] intArr = new int[5];
int i = 3;
// intArr[0] 保存的是 0x00000003
intArr[0] = i;
Object[] objArr = new Object[5];
/** 假设 obj 保存的是 0x12345678,不管是对象地址,还是句柄地址,反正能通过此地址* 定位到对象。*/
Object obj = new Object();
// objArr[0] 保存的是 0x12345678
objArr[0] = obj;

[6] 大端表示法(Big-Endian):高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

小端表示法:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

读取数据时从低地址端到高地址端。

如:存储整数 43981,对应十六进制 0xABCD,一个格子存储不下,拆成 0xAB、0xCD 后再存储。0xAB 是高位字节,0xCD 是低位字节。

大端表示法,|0xAB|0xCD|,先读取高位 0xAB,可以直接判断正负。(最高位是符号位,二进制表示下 0 代表 正,1 代表 负)

小端表示法:|0xCD|0xAB|,在强制转换时不需要调整字节内容,直接把前 2 个字节 0xCD 赋给小容量。

CPU 做数值运算时从内存中依顺序依次从低位到高位取数据进行运算,直到最后刷新最高位的符号位,这样的运算方式会更高效。

  1899
+ 1232
= 从高位 1+1 开始计算比较难,需要考虑进位,从低位 9+2 开始计算容易。

取出两个字节后,计算 0xAB << 8 | 0xCD 得到原值。

参考文献

  • https://blog.csdn.net/weixin_57907028/article/details/117367380
  • https://zhuanlan.zhihu.com/p/360037797

2.3.6 初始化

初始化是类加载过程的最后一个阶段。执行类初始化方法 <clinit> 的过程。

<clinit> 方法是由编译器自动收集类中的所有类变量在声明时的赋值动作与静态语句块(static {})中的语句合并产生的。

编译器收集的顺序由语句在源文件中出现的顺序决定。

码 2.3.6-1 非法前向引用变量
/*   准备阶段时,设置类变量 i 默认值 0;初始化阶段,<clinit> 方法执行顺序:i = 234;i = 4;System.out.println(i);使用阶段,调用 main 方法(调用 main 方法前必须先初始化此类)
*/
class Clinit {static {i = 234;// 可以赋值,但不能访问:“非法向前引用”// System.out.println(i);}static int i = 4;static {System.out.println(i);}public static void main(String[] args) {}
}

由于初始化子类前会初始化父类,所以在子类的 <clinit> 方法执行前,父类的 <clinit> 方法已经执行完毕。

如果一个类中没有静态语句块,也没有在声明时给类变量赋值,则不会生成 <clinit> 方法。

接口中不能使用静态语句块,但接口与类一样都会生成 <clinit> 方法,执行接口的 <clinit> 方法不需要先执行父接口的 <clinit> 方法;

因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的 <clinit> 方法。

码 2.3.6-2 实现类或子接口初始化不会导致父接口初始化
class Test {public static void main(String[] args) {// 子类初始化System.out.println(new InterfaceImpl());}
}
interface Interface {public final static int i = some();public static int some() {System.out.println("接口初始化");return 2;}
}
class InterfaceImpl implements Interface {static {System.out.println("子类初始化");}
}

Java虚拟机必须保证一个类的 <clinit> 方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 <clinit> 方法[1],其他线程都需要阻塞等待,直到活动线程执行完 <clinit> 方法。

如果在一个类的 <clinit> 方法中有耗时很长的操作,那就可能造成多个进程阻塞。


[1] 需要注意,其他线程虽然会被阻塞,但如果执行 <clinit> 方法的那条线程退出 <clinit> 方法后,其他线程唤醒后则不会再次进入 <clinit> 方法。在同一个类加载器下,一个类型只会被初始化一次。

<init> 方法内可以有多个线程同时进入。

2.4 代码书写规范

内容导视:

  • 代码书写

  • 代码规范

  • 标识符与关键字

2.4.1 代码书写

看不懂没关系的,只挑能理解的。

1)字母严格区分大小写,如 class 不等于 Class。

2)任何有效的代码必须写在 “类体” 中,就是 public class Hello 后的一对大括号 {} 内。

3)大括号必须要成对写,防止漏掉。

4)为了增加代码的可读性,{} 里的内容需要使用 tab 缩进,如 main 方法就比 class Hello 低一个层次。

码 2.4.1-1 低一层级,需要缩进
class Hello {// 被 {} 包裹的部分选中,按下 tabpublic void some1() {// 被 {} 包裹的部分选中,按下 tabint i = 10;if (i > 10) {// 同理System.out.println("为何");}}
}

5)main 方法后的 {} 包裹的内容称为方法体,由一行行的 java 语句构成,任何一条 java 语句必须以分号结尾。若无特别说明,默认为英文状态下的符号。

6)方法体中代码遵循自上而下的顺序依次逐行执行,不可随意颠倒顺序。

System.out.println(i);// 执行到这句时,还没有 i 变量
int i = 10;

7)一个 java 源文件可以定义多个类型。编译后,每一个类型对应一个 class 文件,如以下编译后会生成 A.class、B.class、C.class 三个字节码文件

class A {}class B {}class C {}

8)有 public 修饰的类可以没有,但如果有,被 public 修饰的类名必须与源文件名一致。
例:源文件名为 Hello,则 public class 后的类名也应该为 Hello。

9)被 public 修饰的类如果有,则最多只能有一个。

10)运行时,只会调用 main 方法(也称入口方法)里面的内容,入口方法有固定的书写格式:

public static void main(String[] args) {}

入口方法所在类称为入口类。

2.4.2 代码规范

规范与规则不一样,不是必须遵守。但是如果你不遵守,代码可读性会很差。反例如下,虽然可以通过编译,但你看起来感觉如何?

码 2.4.2-1 毫无章法
class
Hello
{public           static
voidmain(String a[]){System.out. println("你好"
); int i=2;if(i>1&&i<29){System.out.println("e");}
}}

1)类和方法上的注释,以 javadoc 的方式,方便生成文档注释。

码 2.4.2-2 文档注释
class Test {/*** 这个注释下节有讲,现在只是测试*/public void some() {}
}

2)非 javadoc 注释,用于给代码的维护者和读者看。(为什么这么写,如何修改,注意事项)

码 2.4.2-3 单行注释
// 这就是注释,解释下面是将 10 赋给了 int 类型的变量 i;
int i = 10;

3)运算符如 <、= 的两边使用空格与字符隔开,举出正反例:

int a = 10;
int b=10;if (a < 20 && b > 5) {}if(a<20&&b>5){}

4)代码编写使用次行风格或行尾风格。

码 2.4.2-4 行尾风格
// 行尾风格
public void add(int num) {if (num < 0) {this.age = 3;} else if (num < 6) {this.age = 33;} else {this.age = 333;}
}
码 2.4.2-5 次行风格
// 次行风格
public void add(int num)
{if (num < 0){this.age = 3;} else if (num < 6){this.age = 33;} else{this.age = 333;}
}

2.4.3 标识符与关键字

声明:由于每次使用 import、带上完整类、main 方法比较繁琐。于是进行了简化,只提供关键部分,外面的类、方法等其它代码以后自己加吧,别直接复制代码,发现怎么跑不起来啊?

class Hello {public static void main(String[] args) {}public int add(int num1, int num2) {}
}

凡是程序员有权利命名的单词都是标识符。

比如:类名、方法名、参数名、变量名、接口名、常量名。(这些是什么,以后就知道了,现在只是看看)

// add 是方法名有权利命名
public int add(int num1, int num2){// n 是变量名int n = num1;
}

你可以试试哪里可以修改,比如修改 public、int 等单词,是否会报错。你就明白,你可以动的地方有哪些。可以动而不报错的那些单词就是标识符,不可以动的就是关键字。

标识符命名规则

  • 标识符只能由数字、字母、_、$ 组成。(标识符中不能有空格)
  • 标识符不能以数字开头。
  • 关键字和保留字不能做标识符。
  • 标识符严格区分大小写(如 Public 与 public 不一样),占用字节数不得超过 64 KB。

可以使用 java.lang.Character 类的方法检验,isJavaIdentifierStart 方法可以确定某字符是否可以作为变量名的开头;

isJavaIdentifierPart 方法确定某字符是否可以作为变量名的一部分,不包括开头。

标识符命名规范

一、见名知意,如 day 代表天数。

1)别用拼音与英语混合,那样很难理解。

反例:tianKongBlue

2)杜绝完全不规范的英文缩写,避免望文不知义。

反例:AbstractClass “缩写”成 AbsClass;
condition “缩写”成 condi;
Function “缩写”成 Fu;
此类随意缩写严重降低了代码的可阅读性。

二、驼峰命名,多个单词连在一起时,单词首字母大写,增加识别和可读性,下面是阿里巴巴开发手册(黄山版)详细:

1)类名使用 UpperCamelCase 风格(首字母大写,后面每个单词首字母大写),以下情形例外:DO / PO / DTO / BO / VO / UID 等。

正例:ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion
反例:forcecode / UserDo / HTMLDto / XMLService / TCPUDPDeal / TAPromotion

2)方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格(首字母小写,后面每个单词首字母大写)。

正例:localValue / getHttpMessage() / inputUserId

3)常量名全部大写,单词间用_连接,力求语义表达完整,不要嫌名字长。

正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME

枚举成员实际上也是常量,名称需要全大写,单词间用_连接。

4)抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾;枚举类名带上 Enum 后缀。

5)如果是形容能力的接口名称,取对应的形容词为接口名(通常是 –able 结尾的形容词)。

正例:AbstractTranslator 实现 Translatable 接口。

6)在常量与变量命名时,表示类型的名词放在词尾,以提升辨识度。

正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD

7)POJO 类中定义的布尔类型的变量,不要加 is 前缀,否则部分框架解析会引起序列化错误。

备注:POJO(Plain Ordinary Java Object):普通的 Java 对象。

反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted()。
框架在反向解析时,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

8)包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。

正例:应用工具类包名为 com.alibaba.ei.kunlun.aap.util;类名为 MessageUtils
(此规则参考 Spring 的框架结构)。

9)避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。

// 反例
public class ConfusingName { protected int stock; protected String alibaba;// 非 setter/getter 的参数名称,不允许与本类成员变量同名public void access(String alibaba) {if (condition) {final int money = 666;// ...}for (int i = 0; i < 10; i++) {// 在同一方法体中,不允许与其它代码块中的 money 命名相同final int money = 15978;// ...} } }
class Son extends ConfusingName {// 不允许与父类的成员变量名称相同private int stock;
}

说明:子类、父类成员变量名相同,即使是 public 也是能够通过编译,而局部变量在同一方法内的不同代码块中同名也是合法的,但是要避免使用。对于非 setter / getter 的参数名称也要避免与成员变量名称相同。

关键字

Java 关键字是对 Java 编译器有特殊含义的字符串,是编译器和程序员的一个约定,程序员利用关键字来告诉编译器其声明的变量类型、类、方法特性等信息。

关键字一律用小写字母标识,按其用途划分为如下几组。

1)用于数据类型的关键字有:

boolean、byte、char、 double、 false、float、int、long、new、short、true、void、instanceof。

2)用于语句的关键字有:

break、case、 catch、 continue、 default 、do、else、 for、 if、return、switch、try、while、finally、 throw、this、 super。

3)用于修饰的关键字有:

abstract、final、native、private、protected、public、static、synchronized、transient、volatile。

4)用于方法、类、接口、包和异常的关键字有:

class、 extends、 implements、interface、 package、import、throws。

5)保留字:

cat、 future、 generic、inner、 operator、 outer、rest、var、goto、byValue、cast、const 等都是 Java 尚未使用,但以后可能会作为关键字使用。

true、false 是布尔字面量,null 是空字面量。但它们和关键字一样,不可以作为标识符使用。

2.5 注释

内容导视:

  • 单行注释
  • 多行注释
  • 文档注释
  • IDEA 工具自动快捷键添加注释
  • 使用 javac 命令编译时出现的"错误: 编码 GBK 的不可映射字符"
  • 查看系统编码
  • 使用 API 文档

注释是对代码的解释说明,方便理解代码的含义,提高代码的可读性。
注释不是编程语句,因此被编译器忽略。
如果不写注释,时间久了或代码过长本人也看不懂,所以编写注释是一个良好的习惯。

注释有三种,依次介绍。

2.5.1 单行注释

使用双斜杠 //,// 后的就是注释(仅一行),不会被编译器当成 java 语句。

码 2.5.1-1 单行注释
public class A {public static void main(String[] args) {// 输出一句话到控制台System.out.println("Hello World A");}
}

2.5.2 多行注释

对于很多内容,单行放不下,可以使用多行注释,在 /**/ 内写下内容,星号别省略。

码 2.5.2-1 多行注释
/*System 是类,通过类名.调用 out 这个静态变量,这个静态变量保存的是堆内存中的对象地址,被称为对象引用再通过对象引用,调用对象的 println 方法
*/
System.out.println("Hello World A");

2.5.3 文档注释

下面看看就行,以后使用开发工具 IDE 自动生成,没必要手动敲。

文档注释可以被 javadoc 命令解析,生成以网页形式(html)显示的 API 文档。(Application Programming Interface:应用程序编程接口)。

当类又多又杂,一个个找类、看注释很麻烦,于是提取出来生成网页。

用来说明类、成员变量和方法的功能。不用在一个一个 java 文件中查看注释,直接打开 html 查看想要的方法。

javadoc 默认只提取 public、protected 修饰的部分,javadoc -help 查看可加的选项。

文档注释必须写在类、接口、方法、构造器、成员字段前面,写在其他位置无效。

文档注释中可以识别的标签如下:

表 2.5.3-1 标签
标签 说明
@version 指定类的版本,用于类上
@author 标注类的作者
@since 从哪个版本起有了这个方法
@param 参数详细信息
@return 说明返回值
@throws 可能抛出的异常
@deprecated 表示废弃,不建议使用,JDK5 时被注解 @Deprecated 取代
@see 另请参阅,引用其他类的文档

文档注释的标签区分大小写,别写错了,也可以看看源码上的注释是怎么写的。(别省略 * 号,注意观察下面的格式)

码 2.5.3-1 文档注释
/*** @author 是在座的每一个人* @version 0.0.1*/
public class Hello {/*** 这个方法用来求两数之和** @param  num1    第一个参数* @param  num2    第二个参数* @return 返回两个数的和* @throws RuntimeException 测试用而抛出的异常* @since  0.0.1*/public int add(int num1, int num2) throws RuntimeException {return num1 + num2;}
}

javadoc 命令只能提取文档注释

图 2.5.3-1 javadoc 命令提取文档注释

javadoc Hello.java -encoding UTF-8 -version -author -private -charset UTF-8 -docencoding GBK

当前目录会生成 index.html 文件,双击此文件,交给浏览器解析(会自动打开浏览器),右键查看页面源代码,API 文档里的信息是读取 Hello.java 文件得来的。

使用此命令还可以追加其他 java 源文件的文档注释,例:javadoc -encoding UTF-8 Hello.java H.java

图 2.5.3-2 javadoc 命令提取文档注释

javadoc 命令中的参数说明

-encoding 是告知 java 源代码所用的字符编码;

-version、-author 是显示版本和作者;

-private 是显示所有类和成员;(包括显示私有,一般显示 protected 以上级别就行,可以不加此选项)

-charset 是告知浏览器此文件采用什么编码方式读取这个文件;
即 html 文件生成 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

-docencoding 是指定生成的 html 文件的字符编码,不写此选项,默认 UTF-8。


由于文件被保存时的编码和读取时采用的编码不一致会出现乱码,所以别掉坑。

指定文档生成的位置

使用 -d 指定文件在 E:\a\b\c 下生成。

javadoc Hello.java -d E:\a\b\c -version -author

代码一改,注释说明也得跟着改;为了避免无用功,代码中给标识符命名,见名知意,最好让所有人看到这段代码就知道它是干什么的,避免冗余无效注释。

示例如下:

码 2.5.3-1 文档注释示例
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.CharsetDecoder;/*** 这是测试如何制作 API 文档的类** <p> p 标签包围的文字代表一段,ul 和 li 标签是无序列表</p>* <ul>* <li>呵呵 {@code test} 呵呵</li>* <li>呵呵 <code>test</code> 呵呵</li>* <li>被上面 code 标签包裹的内容会更显眼,更细,一般用于标识符</li>* <li>see 标签另请参阅,格式:引用类的全类名#方法名(形参类型, 形参类型...)*     java.lang 包下的类可以省略包名。如果引用的类在 API 文档上,点击后会自动跳转*     这个 H 类我也打包到了此文档</li>* </ul>** @author  cqh* @author  作者甲* @version 1.0.0* @see     Object#wait(long)* @see     Object#toString()* @see     StringBuffer* @see     H#hh()* @since   0.8.0*/
public class Test {/*** 介绍属性的作用** @see java.util.Scanner#makeReadable(ReadableByteChannel, CharsetDecoder)* @since 0.8.1*/public String[] name;/*** 说明方法的作用** @param name 这个name是 {@code String} 类型*           see参阅本类中其它方法省略类名。link如*             {@link #getAge(int, int)}也可以跳转,例*             {@link H#hh()},{@link H}* @return 返回一个人名* @throws ArrayIndexOutOfBoundsException 下标越界抛出此异常* @see #getAge(int, int)* @see H* @since 0.8.1*/public String getName(String name) throws ArrayIndexOutOfBoundsException{return name;}/*** 另一种格式,把解释放到下一行 {@link java.lang.Object#notify()} 你好** @param age*        这是人的年龄** @param count*        总个数        ** @throws IndexOutOfBoundsException*         如果满足如下几种条件之一就抛出该异常*         <ul>*           <li> {@code age} 不是整数*           <li> {@code age} 没有填入*           <li> {@code age+(end-begin)} 超过了 *                {@code name.length}*         </ul>*/public void getAge(int age, int count) throws IndexOutOfBoundsException{}
}

2.5.4 IDEA 工具自动快捷键添加注释

以后有了 IDEA 工具再试,现在不用看,直接跳过;放心 IDEA 工具中会更详细地说明。

在 IDEA 中每创建一个类时,自动在类上加注释

左上角 File/Settings/Editor/File and Code Templates,includes/File Header

里面粘贴:

/*** 现在北京时间:${YEAR}/${MONTH}/${DAY} ${TIME}* 本类用于某某某** @author  作者* @version 版本* @see     另请参阅* @since   从哪个版本有的*/

点击 apply 和 OK。

图 2.5.4-1 文件头自动添加注释

在方法上加注释

左上角 File/Settings/Editor/Live Templates

点击加号 Template Group 自己创建个组后,再选择你刚刚创建的组点击加号 Live Template。

解释:

Abbreviation:**Description:解释快捷键的描述,自己填写Options
Expand with EnterTemplate text:
**** @param   $param$* @return  * @throws*/

当输入 Abbreviation 所写内容 ** 时,按下 Enter 就把 ** 转成 Template text 中的内容。

$$ 包围的变量需要点击 Edit vaiables 设置。

图 2.5.4-2 自定义变量

选择 Define 勾选 Java,就可以定义此模板只有在编写 java 文件才能使用。

图 2.5.4-3 设置应用范围

点击 apply 和 OK。

示例:

缩写词:psvt;设置好后 OK 返回。

在 java 文件输入 psvt 后按下 Tab 键,光标停在 $VAR$ 等我们输入;按下回车,光标到了 $END$ 处。

2.5.5 使用 javac 命令编译时出现的"错误: 编码 GBK 的不可映射字符"

内容如下:

  • 解决 GBK 乱码问题
  • 普及编码格式知识

本节片段提取:

友情提示,先复制文本,转换后直接粘贴。目的是避免转换编码后造成中文乱码,又得一个个改。

有 2 种方式解决:

1)文件/另存为副本,更改编码为 ANSI。(如果使用的是 notepad++,就在上方的工具栏的编码,转成 ANSI 编码)

图 2.5.5-1 使用 ANSI 编码

2)编译时指定 java 文件使用的编码格式。(编码格式在文本编辑器的右下方)-encoding UTF-8 是指定文件编码格式是 UTF-8。

图 2.5.5-2 查看当前编码

正文如下:

可能编译时由于代码(包括注释里面)中有中文,编译时说什么含有 GBK 的不可映射字符,不让通过。

图 2.5.5-3 GBK 的不可映射字符

为何报错

计算机只认得二进制数,也就是 0 和 1,存储和读取数据时都要使用二进制数表示。

0 和 1 可以有很多的组合,如 011、1001010 等等。可以用它们表示不同的数据,字符编码就是人为定义的一套转换表,规定一系列文字与二进制的映射关系。

有多种字符编码,比如 ASCII 码,一般是 8 位二进制表示一个字符,如字符 ‘a’ 对应 01100001,这个二进制码转为十进制是 96;

学过概率的知道,8 位数,每位是 0 或 1,最多可以表示 2 ^ 8 = 256 个字符,应对 26 个英文字母绰绰有余。这 8 位称为 1 个字节(byte),每位称为 bit。

2 ^ 8 即 2 的 8 次方,只是为了方便表达。有的网站不支持上标、下标语法时,你会看到 2^8^、2~8~,分别对应 28、28

ASCII 是美国人定义的,没有考虑其它国家。但我们国家用的不是英文,有很多字符是 ASCII 码没有的,1个字节也表示不了那么多的汉字,于是国人粗略规定了 GB2312,使用 2 个字节(16位)表示一个汉字。

如在 GB2312 编码中 “中国” 对应的二进制数 11010110 11010000 10111001 11111010,要是系统采用 ASCII 码读取这段二进制,由于它们定义的字符的映射关系都不一样,ASCII 编码也不可能有中文对应的二进制码,所以解码不可能正确,解析不出来就会乱码。

这里二进制数每 8 位隔开,只是方便观看。

来,我们使用记事本试试看。此处文件编码为 ANSI,使用 ANSI 编码读取文件,没有乱码,显示 “天下”。

ANSI 代表系统默认编码方式,在中国是 GBK,GBK 兼容 GB2312,使用 GBK 可以解析 GB2312。

举个例子,如果 ASCII 中所有字符对应的二进制码,在 GBK 中,对应关系也同样如此,就说明 GBK 兼容 ASCII 码。说人话就是 01100001 在 ASCII 中表示 ‘a’,在 GBK 中也表示 ‘a’。那么使用 GBK 编码读取 ASCII 文件没有问题,反过来则不一定,因为 GBK 还多了 ASCII 中没有的汉字。

图 2.5.5-4 ANSI 编码的字符

让我们把文件编码转成 ISO-8859-1,也使用此编码读取文件。

图 2.5.5-5 转为 ISO-8859-1 编码

图 2.5.5-6 乱码

现在再看看,这不就乱码了吗?

所以解码和编码都要使用同一套字符编码规则,notapad++ 可以使用不同的编码方式解析,可以测试哪些编码是兼容的,不会乱码。

编码:字符 -> 二进制码

解码:二进制码 -> 字符

底层存储的实际是二进制码。

使用 javac 命令编译时,如果不指定 -encoding 选项,一般默认采用操作系统的字符编码方式,我的是 GBK。

在编写代码时,文本编辑器的右下角会显示当前文件的编码格式。编译时,若与系统当前编码不一致,不是同一套字符编码规则,解析不出来,就会报错。

多种方式解决

  • 告知此文件的编码方式。
    例:当前文件的编码格式是 UTF-8,文件是 Hello.java,编译时添加 -encoding 参数指定文件编码 javac Hello.java -encoding UTF-8
  • 修改文件编码方式为 ANSI,文件另存时可以看到编码。(notepad++ 在工具栏/编码/转为…记得先全选复制,改编码后再粘贴,否则乱码没法撤回)
  • 所有文件使用统一的编码方式,如 UTF-8,新建环境变量 JAVA_TOOL_OPTIONS,值为 -Dfile.encoding=UTF-8,不建议,以后使用 IDE 工具统一使用 UTF-8 编码,不需要自己在外面配。

2.5.6 查看系统编码

通过 java 代码查看

码 2.5.6-1 查看系统默认编码
public class Hello {public static void main(String[] args) {String encoding = System.getProperty("file.encoding");System.out.println(encoding);}
}

通过 DOS 窗口查看

打开 DOS 窗口(Windows + R,输入 cmd 回车),点击 DOS 窗口左上角图标/属性

图 2.5.6-1 当前代码页 936

当前代码页是 936,对应简体中文编码 GBK。

代码页是字符集编码的别名,也称内码表,下面是代码页与编码的对应关系:

表 2.5.6-1 代码页与编码的对应关系
代码页        国家(地区)或语言
437          美国
708          阿拉伯文(ASMO 708)
720          阿拉伯文(DOS)
850          多语言(拉丁文 I)
852          中欧(DOS) - 斯拉夫语(拉丁文 II)
855          西里尔文(俄语)
857          土耳其语
860          葡萄牙语
861          冰岛语
862          希伯来文(DOS)
863          加拿大 - 法语
865          日耳曼语
866          俄语 - 西里尔文(DOS)
869          现代希腊语
874          泰文(Windows)
932          日文(Shift-JIS)
936          中国 - 简体中文(GB2312)现在是 GBK 了,GBK 是在国家标准 GB2312 基础上扩容后兼容 GB2312 的标准。
949          韩文
950          繁体中文(Big5)
1200         Unicode
1201         Unicode (Big-Endian)
1250         中欧(Windows)
1251         西里尔文(Windows)
1252         西欧(Windows)
1253         希腊文(Windows)
1254         土耳其文(Windows)
1255         希伯来文(Windows)
1256         阿拉伯文(Windows)
1257         波罗的海文(Windows)
1258         越南文(Windows)
20866        西里尔文(KOI8-R)
21866        西里尔文(KOI8-U)
28592        中欧(ISO)
28593        拉丁文 3 (ISO)
28594        波罗的海文(ISO)
28595        西里尔文(ISO)
28596        阿拉伯文(ISO)
28597        希腊文(ISO)
28598        希伯来文(ISO-Visual)
38598        希伯来文(ISO-Logical)
50000        用户定义的
50001        自动选择
50220        日文(JIS)
50221        日文(JIS-允许一个字节的片假名)
50222        日文(JIS-允许一个字节的片假名 - SO/SI)
50225        韩文(ISO)
50932        日文(自动选择)
50949        韩文(自动选择)
51932        日文(EUC)
51949        韩文(EUC)
52936        简体中文(HZ)
65000        Unicode(UTF-7)
65001        Unicode(UTF-8)

也可通过 DOS 命令查看当前编码:chcp

修改当前 DOS 窗口的编码:chcp 对应编码的代码页,如 chcp 936,当控制台不支持中文时,可以试试。(重新打开 DOS 窗口时会失效,恢复原来默认编码)

修改系统编码

如果是 Windows10,打开设置(Windows + i),时间和语言/语言/管理语言设置/更改系统区域设置,

老版本的使用控制面板/时钟和区域/区域/管理/更改系统区域设置。

图 2.5.6-2 更改语言

下面还有 beta 版(即测试版),使用 UTF-8 编码提供全球语言支持;但有些地方会莫名其妙的乱码,不建议尝试,还是 GBK 靠谱。

参考文献

  • https://blog.csdn.net/gulang03/article/details/81771343

2.5.7 使用 API 文档

解决了中文乱码后,来看看 rt.jar 包中的类、方法、字段上的文档注释生成的 API 文档。

JDK17 API 文档:https://docs.oracle.com/en/java/javase/17/docs/api/index.html

JDK8 API 文档:https://docs.oracle.com/javase/8/docs/api/

JDK8 API 中文文档:https://www.matools.com/api/java8

记不住方法名怎么办,翻阅 API 文档查看方法的作用,你得首先记住哪个类好像有这个方法。

Java 语言提供了大量可供使用的基础类,Oracle 为这些类提供了对应的 API 文档,告诉开发者如何使用此类,以及方法。

通过包名 -> 类名 -> 方法这样的方式寻找。

以 JDK8 为例,如果不知道类在哪个包下,点击最上方的索引(INDEX)

如我要用 Math 类的求绝对值的方法,如果事先知道它在 java.lang 下(第一横线处下翻),找到后点击 java.lang,继续下翻(第二个横线处);找到后点击 Math,再看右边的页面,下翻;点击 abs 方法,就可以看到此方法的详细说明。

图 2.5.7-1 使用 API 文档查找方法详细说明

打开 rt.jar(jre 的 lib 下) 也可以看到 Math.class。

图 2.5.7-2 查看源码上的注释

你说这是字节码文件,根本看不懂,要看源码上的文档注释。好吧,在 JDK 安装目录下,打开 src.zip\java\lang\Math.java

以这种方式,的确没有 API 文档访问方便不是吗?(养成在方法上写文档注释的好习惯,这样方便提取出来)

下面是谷歌翻译:

返回 {@code float} 值的绝对值。
* 如果参数不是负数,则返回参数。
* 如果参数是否定的,则返回参数的否定。
* 特别案例:
* <ul><li>如果参数是正零或负零,则
* 结果为正零。
* <li>如果参数为无穷大,则结果为正无穷大。
* <li>如果参数为 NaN,则结果为 NaN。</ul>
* 换句话说,结果与表达式的值相同:
* <p>{@code Float.intBitsToFloat(0x7fffffff & Float.floatToIntBits(a))}
*
* @param a 要确定其绝对值的参数
* @return 参数的绝对值。

2.x 总结回顾

程序员可以命名的单词是标识符,如类名、方法名、变量名、接口名、常量名。

命名规则

  • 只能由数字、字母、_、$组成。
  • 不能以数字开头。
  • 不能以关键字作为标识符。

命名规范

  • 类名、接口名首字母大写,之后每个单词首字母大写。StringBuffer
  • 方法名、变量名首字母小写,之后每个单词首字母大写。getName
  • 常量名全部大写,不同单词之间使用 “_” 隔开。ALL_MODES

2.y 课后习题

2.1 为什么 Java 代码可以做到一次编译,到处运行?

2.2 一个源文件可以生成多个 class 文件吗?

2.3 Java 源文件定义的类名必须与文件名一致吗?

2.z 习题答案

2.1 为什么 Java 代码可以做到一次编译,到处运行?

编译生成的 .class 文件与具体平台、系统无关。

JVM 虚拟机是关键。程序运行时,由 JVM 负责解释执行 .class 文件,将其翻译成不同平台下的机器码并运行。需要注意的是,.class 文件是与操作系统平台无关的中间代码,不能直接运行。

JVM 不跨平台,不同平台需要安装不同的 JVM。

这么做的目的,在可移植的同时兼顾执行效率。此外还加入了 JIT(just in time)编译器,把运行频繁的代码直接编译成与本地平台相关的机器码,需要时直接使用,不用再次翻译。


2.2 一个源文件可以生成多个 class 文件吗?

可以,源文件只要按规则定义了多个类,编译后会生成多个对应的 class 文件。


2.3 Java 源文件定义的类名必须与文件名一致吗?

不是,只有被 public 修饰的类名必须与源文件名一致。

cqh-class 文件内容,类加载过程,编写第一个程序相关推荐

  1. 蒙特卡罗MCNP学习汇总(一)-----MCNP简介及编写第一个程序

    目录 简介: 什么是MC模拟 介绍 应用 运行 编写第一个程序 格式 程序 讲解 现象 简介: 什么是MC模拟 一种通过随机抽样解决数学问题的一种数值计算方法. MC方法解决的主要数学问题 -数值积分 ...

  2. 正点原子STM32F103精英版开发环境搭建并编写第一个程序(使用串口下载,基于HAL库)

    一:软件下载: 使用图形化配置软件:STM32cubeMX,支持HAL库. 1,下载stm32cubeMX; 2,下载STM32f103器件包: 3,下载IED:keil5 arm版本: 4,下载 s ...

  3. C++第一天(编写第一个程序,变量与常量)

    采用VS2019编写C++的第一个程序 创建项目 选择C++ 空项目 创建文件 在解决资源管理器中的,源文件,右键添加,新建项 编写程序 C++框架 main是一个程序的入口,有且仅有一个, #inc ...

  4. 使用汇编语言编写第一个程序

    编写逻辑 使用工具: editPlus 运行工具: 用Debug跟踪程序的执行. 程序模板: assume cs:codesg // cs代表代码段codesg segment mov ax,0123 ...

  5. scala学习-1-eclipse编写第一个程序unknown scala version problem

    1.概述 0.准备工作,安装scala-2.11.8(安装windows板的http://www.scala-lang.org/download/2.11.8.html),下载spark2.1.1-b ...

  6. 用eclipse编写第一个程序详解(hello world)

    1.首先我们打开eclipse,显示的是下面这个画面 2.我们找到左上角的File,进行操作:File->new->Project,出现下面这个画面 3.双击Java Project,这里 ...

  7. 29.类加载机制、类加载过程、加载、验证、准备、解析、初始化、总结

    29.类加载机制 29.1.类加载过程 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 其中类加载 ...

  8. Java class类文件和类加载器详解以及代码优化

    JVM就是Java虚拟机,它是Java程序运行的载体. 计算机只识别0和1.Java是⾼级语⾔.⾼级语⾔编写的程序要想被计算机执⾏,需要变成⼆进制形式的本地机器码.能直接变成机器码的语义是C++,它的 ...

  9. 深入理解Java虚拟机——JVM类加载机制(类加载过程和类加载器)

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

最新文章

  1. LeetCode 709:To Lower Case
  2. pyecharts 应用5:视觉映射配置项VisualMapOpts
  3. gsmake —— golang版gradle
  4. ADO.NET连接字符串
  5. 第七章 脚本参数的传递
  6. 《MySQL——redo log 与 binlog 写入机制》
  7. 汇编:用户登录以及简单数据加密
  8. Kyligence 行业峰会成功举办,“智能数据云”引领数字化转型未来
  9. 看完Mate 10拍下的精美空中照片后,你是否也想坐次飞机试拍下?
  10. 搜索 + 剪枝 --- POJ 1101 : Sticks
  11. 调用百度地图 API 移动地图时 maker 始终在地图中间 并根据maker 经纬度 返回地址...
  12. 检测到 Mac 文件格式: 请将源文件转换为 DOS 格式或 UNIX 格式
  13. 线性代数 05.07 用合同变换法化二次型为标准形
  14. 笔记本装linux费电,关于linux在笔记本下耗电的解决方案(只写我实践的部分)...
  15. Android展开悬浮窗功能,Android 悬浮窗 (附圆形菜单悬浮窗)
  16. oracle 断电起不来,解决方案
  17. [不明所以]android 5.0 couldn't find libmsc.so
  18. 2020-2021的瞻前顾后
  19. 面试官如何考察你的思维方式?学会这个思维方式面试很简单!
  20. 数据可视化-豆瓣影评数据分析(FineBI)

热门文章

  1. 读书笔记 | 张五常 经济解释 (卷一) 科学说需求
  2. ikbc机械键盘打字出现重复_机械键盘按键 按一次触发多次
  3. 【ACM】HDU.2094 产生冠军 【STL-map】
  4. Content Provider启动浅析
  5. 怎么更改计算机用户名网络密码怎么办,怎么改wifi密码和名称(电脑修改wifi密码步骤)...
  6. FPGA基础设计(二):PS2键盘控制及短按、长按
  7. 计算机基础知识(基础入门小白专属)五
  8. VMware vCenter突然无法正常登陆,出现[503 Service Unavailable]
  9. Win10激活失败的原因之一
  10. vue、Steps 步骤条、Steps 属性、vue Steps 所有步骤条样式、vue Steps 步骤条全部属性