0%

平时我们在代码中打印 log 基本都是直接调用 logger.info() 方法,官方推荐在外面再包一层 if (logger.isInfoEnabled()) 判断,Why?

以 log4j 为例:

1
2
3
4
5
private static final Logger logger = Logger.getLogger(ExampleBeanWithSetter.class);

if (logger.isInfoEnabled()) {
logger.info("Hello world...");
}

如果没有加外层的 isInfoEnabled 判断,那个将会直接执行 logger.info(String) 方法, 相对于有判断的形式,多了一步拼接字符串的步骤。换句话说,最大的区别有两点:

  1. 拼接字符串耗时
  2. 这些字符串也会消耗内存空间

对一般的小系统当然是没什么影响,如果是高并发或者 log 很多的系统,可以作为一个优化的方向。

HTML + CSS + JS = 骨 + 肉 + 灵

Pink 前端视频教程 学习笔记

VSCode

在 html 文件中 ! + tab 自动生成网页骨架

插件:

  • open in browser
  • auto rename tag
  • JS-CSS-HTML Formatter
  • CSS Peek

Html 常用标签 p12-60

<!DOCTYPE html> 文档类型声明标签,表明时 html5 格式。

<html lang="en"> 显示语言,只起提示作用,浏览器遇到不同语言会给你翻译提示

<meta charset="UTF-8"> 字符集

<hn> n=1-6 总共支持 6 级标签

<p> 段落

<br/> 换行,单标签

文本效果:

  • <strong>, <b> 加粗
  • <em>, <i> 倾斜
  • del, s 删除线
  • <ins>, <u> 下划线

<div> 盒子标签,没有语义,div 分割,分区,占一整行

<span> 盒子标签,跨度,跨距

<img src='' alt='' title='' width='' height='' border='边框,一般通过 css 改'/> 图像

超链接, _blank 新标签页打开

锚点链接: +

注释

特殊字符:

name value
空格  
小于 <
大于 >

表格

作用:显示,展示数据,table->tr->th/td

属性:align(left/center/right) 对齐;border 边框;cellpadding 单元格和内容之间的距离;cellspacing 单元格之间的距离;width 表宽;

table 可以分成 theader + tbody, theader 包含 tr + th,tbody 包含 tr + td 内容。

合并单元格:rowspan=”合并单元格数” 跨列合并,左侧的为目标单元格, colspan=”合并单元格数” 跨行合并 上面的为目标单元格

列表

作用:布局

分类:无序列表,有序列表,自定义列表

无序:ul + li, ul 下第一级只能包含 li,li 下可以包含任何标签

有序:ol + li

自定义:dl + dt + dd,dl 下只能包含 dt 和 dd, 适用于小标题加说明的情况

1
2
3
4
5
<dl>
<dt>关注我们</dt>
<dd>微信</dd>
<dd>微博</dd>
</dt>

表单

作用:收集用户信息

组成

  • 表单域:
    将域范围内的内容提交到后台
  • 提示信息
  • 表单控件(元素):input/select/textarea

<input type='类型'/>, 类型有很多种,text, boolean, button, radio, reset, file, image 等, 还有 name, value, checked 和 maxlength 四个属性

label 标签:绑定页面元素,点击 label 光标会自动聚焦到绑定元素,增加用户体验 – 这个之前一直没注意(~ ̄▽ ̄)~

示例:

1
2
<!-- 通过 label 里面的 for 属性生效 -->
<label for="text"> 用户名:</label> <input type="text" id="text">

下拉列表:多选一

1
2
3
4
<select>
<option selected="selected">xxx</option>
<option>aaa</option>
</select>

文本域 textarea: cols, rows

CSS P60-

css=选择器 + 一条或多条声明, 放在 head 下的 <style/>

1
2
3
4
5
6
<style>
p {
color: red;
font-size: 12px;
}
</style>

选择器分为 基础选择器 和 复合选择器

基础选择器:由单个选择器组成,包括:标签选择器,类型选择器, id 选择器 和通配符选择器

1
2
3
4
5
6
7
8
9
10
11
<style>
/* 标签选择器 */
/* 标签名做选择标的 */
p {
color: green;
}

div {
color: pink;
}
</style>
1
2
3
4
5
6
<style>
/* 类型选择器, 样式(class)做选择标的 */
.red {
color: green;
}
</style>
1
2
3
4
5
6
<style>
/* id选择器, id 做选择标的 */
#pink {
color: green;
}
</style>
1
2
3
4
5
6
<style>
/* 通配符选择器, 通配符做选择标的 */
* {
color: green;
}
</style>

字体属性

  • font-family: “Microsoft YaHei”;
  • font-size: 20px;
  • font-weight: blod; 文字效果,加粗,也可以用数字 blod=700 normal=400, 范围 100-900
  • font-style: normal/italic; 斜体

字体的复合属性

1
2
3
4
5
6
7
<style>
div {
/* font: font-style font-weight font-size/font-height font-family; */
/* font-size 和 font-family 必须有,其他可以省略,不然不起作用 */
font: italic 700 16px 'Microsoft YaHei';
}
</style>

文本属性 P72-83

  • color, 表示方式,blue,16进制(#)或者 RGB(RGB(X,X,X))
  • text-align, 对齐, left, right, center
  • text-decoration 文本装饰,下划线之类的效果 none(取消超链接的下划线效果), underline, overline, line-through
  • text-indent: 2em; 段落首行缩进
  • line-height:26px 行间距, 行间距=上间距+文字高度+下间距

测量行高小工具 FSCapture.exe

CSS 引入方式

  1. 内部样式表
  2. 外部样式表
  3. 行内样式表

内部样式:放到 header 下的 style tag 下

行内样式: <p style="color: pink;">fense</p>

外部样式:

  1. 新建 .css 文件
  2. 使用 标签引入到 thml <link rel="stylesheet" href="css_file_path">

Emment 语法

前身时 Zen coding, 使用缩写提高 html/css 编写速度,VSCode 已经内置了

  1. 标签 + tab 自动生成
  2. tag*n + tab 生成多个
  3. ul>li 生成父子级关系 tag
  4. div+p 兄弟级 tag
  5. p.one 生成 <p class="one">, # 生成 id
  6. .demo$*5 带序号的 div
  7. div{123} -> <div>123</div>

CSS 复合选择器

即基础选择器的组合形式,有后代选择器,子类选择器,并集选择器,伪类选择器等。。。

后代选择器影响所有后代

1
2
3
4
5
6
7
8
9
10
11
12
<style>
/*父子级中间加空格*/
ol li {
/* 后代选择器 */
color: pink;
}

.nav ul li {
/* 后代选择器 */
color: blue;
}
</style>

子选择器,只对亲儿子起作用(第一层),其他不起作用, 父子间不用空格

1
2
3
4
5
6
7
<style>
/*父子级中间加空格*/
ol > li {
/* 后代选择器 */
color: pink;
}
</style>

并集选择器,使用都好分割, 用于删选多组数据

1
2
3
4
5
6
7
8
<style>
/*父子级中间加空格*/
div,
p {
/* 后代选择器 */
color: pink;
}
</style>

伪类选择器,冒号(:), 比如 :hover, :first-child

链接伪类选择器:

  1. a:link 选择所有未被访问的链接
  2. a:visited 选择所有已被访问过的链接
  3. a:hover 选择鼠标指针位于其上的链接
  4. a:active 选择活动链接,鼠标按下未弹起

注意点:

  1. 一定要按照 LVHA 顺序写,不然会胡问题 (Love Hate / LV 包包 hao)
  2. a 需要单独指定样式,直接指定 body 没用
  3. 实际使用一般指定 a + a:hover 即可

:focus 伪类选择器,选择表单中光标作用的元素

1
2
3
4
5
6
7
<style>
/*父子级中间加空格*/
input:focus {
/* 后代选择器 */
background-color: pink;
}
</style>

CSS 的元素显示模式

HTML 一般分为块元素, 占一行(比如 div)和行内元素(比如 span),一行多个

常见块元素

h1~h6, p, div, ul, ol, li 等

特点:

  1. 霸道,独占一行
  2. 高,宽,外边距以及内边距都可控
  3. 宽度默认时容器(父级宽度)的 100%
  4. 是一个容器及盒子,里面可以放行内或者块级元素

PS: 文字类元素,比如 h, p 种不能再放块级元素

常见行内/内联元素

a, strong, b, em, i, s, u, span

特点:

  1. 相邻元素在一行上,一行可以显示多个
  2. 高宽直接设置是无效的
  3. 默认宽度是它本身内容的宽度
  4. 行内元素只能容纳文本或其他行内元素

PS: 链接里面不能套链接;特殊情况下链接里面可以放块级元素,但是给 a 转换一下块级模式最安全

行内块元素

img, input, td

特点:

  1. 一行可以多个 (行内元素特点)
  2. 默认宽度是它本身内容的宽度 (行内元素特点)
  3. 高度,行高,外边距以及内边距都可以控制 (块级元素特点)

元素显示模式转换

特殊情况下,需要元素模式转换,即一个模式元素需要另一种模式的特性,比如想要增加 的触发范围。

做法在样式种加入 display:block, 或 display:inline, display: inline-block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<style>
a {
width: 150px;
height: 50px;
background-color: pink;
display: block;
}

div {
background-color: blue;
display: inline;
}

span {
width: 150px;
height: 50px;
background-color: yellow;
display: inline-block;
}
</style>

小工具介绍 Snipaste

微软商城可以直接下载,貌似免费,mac 上类似的是 snappy

  1. F1 截图,同时测量大侠,设置箭头,文字等功能
  2. F3 桌面置顶
  3. 点击图片 alt 拾取颜色
  4. esc 取消图片显示
  5. alt + c 直接复制拾取的颜色

单行文字垂直居中

行高=上间隙 + 字高 + 下间隙,设置 行高=盒子高度 line-height: 行高 实现居中

行高 < 盒子高度, 偏上
行高 > 盒子高度, 偏下

CSS 的背景 P115-126

背景属性可以给页面元素添加背景样式,比如背景颜色,背景图片,背景平铺,背景图片为止,背景图片固定等。

background-color: transparent | color

background-image: none | url(), 适用于 logo, 装饰性小图片,超大背景图片,优点是便于控制位置

background-repeat: repeat | no-repeat | repeat-x | repeat-y

background-position: x y; 重点,x, y 可以是像素值,也可以是方位名词(left, center, right)

background-attachment: scroll | fixed 背景图片固定/附着,是否更正页面滚动变化,视差滚动效果

复合写法,直接在 background: 属性1, 属性2 … 没有顺序要求

CSS3 半透明效果

background: rgba(x, x, x, 0.3) 最后一位就是透明度, 0.3 可以直接写 .3

CSS 的三大特性

层叠性

  1. 样式冲突,遵循就近原则,执行离的近的那个
  2. 样式不冲突,不层叠

继承性

子标签会继承父标签的某些样式,比如文字颜色和字号等

  • 恰当的使用继承合一简化代码,降低 CSS 复杂性
  • 子元素可以继承父元素的样式 (text-, font-, line- 和 color)
1
2
3
4
5
6
<style>
body {
/*文字大小/行高 行高可以指定像素,也可以是字号的倍数,这里是 1.5 倍*/
font: 12px/1.5 'Microsoft YaHei';
}
</style>

优先级

  1. 选择器相同,则执行层叠性
  2. 不同,则根据权重来

权重排行:

类型 权重
继承或者 * 0,0,0,0
元素选择器 0,0,0,1
类选择器,伪类选择器 0,0,1,0
id 选择器 0,1,0,0
行内样式 style=”” 1,0,0,0
!important 重要的 无穷大

复合选择器会有权重叠加 .nav li = 0010 + 0001 = 0011

盒子模型 P136

网页布局本质

  1. 准备好页面元素,基本都是盒子
  2. CSS 设置盒子样式,摆放到相应位置
  3. 往盒子里装内容

组成:

  1. 边框 border
  2. 内容 content
  3. 内边距 padding
  4. 外边距 margin

边框

三部分:边框粗细,样式 和颜色

border-width, border-style, border-color 可以写成复合形式: border: 5px solid red;

单条边框设置:border-top/bottom/left/right

border-collapse: collapse; 表格边框线合并

边框会影响盒子实际大小

内边距 padding

padding-left/top/right/bottom

复合写法:

写法 意义
padding: 5px 上下左右都是 5px
padding: 5px 10px 上下 5px, 左右 10 px
padding: 5px 10px 20px 上,左右,下
padding: 5px 10px 20px 30px 上,右,下,左 顺时针

padding 也撑大盒子

外边距 margin

控制盒子与盒子之间的距离

margin-left/top/right/bottom

块级盒子实现水平居中:

  1. 盒子必须有宽度
  2. margin: 0 auto;

行内元素实现水平居中:给父级元素添加 text-align: center;

外边距合并-嵌套快元素塌陷,解决方案:

  1. 可以为父元素定义上边框
  2. 可以为父元素定义上边距
  3. 可以为父元素添加 overflow: hidden 属性

不同的网页元素默认都会带有不同的 内外边距,而且不同 browser 值不同,清除如下

1
2
3
4
5
6
7
<style>
/*一般都会先写这一句*/
* {
margin: 0;
padding: 0;
}
</style>

行内元素尽量只设置左右的内外边距

示例

li { list-style: none;} 去掉 list 的原点

圆角边框

border-radius: npx 或 百分比;

盒子阴影

box-shadow: h-shadow v-shadow blur spread color inset; rgba(0,0,0,.3)

文字阴影

text-shadow: h-shadow v-shadow blur color;

浮动 float

传统三种布局方式

  1. 标准流 - 默认方式
  2. 浮动
  3. 定位

浮动可以改变元素的默认排列方式

网页布局第一准则:多个块级元素纵向排列找标准流,多个块级元素横向排列找浮动

float属性用于创建浮动框,将其移动到一边,知道左边缘或右边缘触及包含块或另一个浮动框的边缘。

浮动的特性

设置浮动的元素最重要的特性:

  1. 脱离标准普通流的控制,移动到指定位置,俗称 脱标
  2. 浮动的盒子不在保留原先的位置

多个盒子设置浮动,则它们会按照属性值一行内显示并且顶端对齐排列

PS: 浮动元素相互没有间隙,父级宽度装不下,另起一行对齐

浮动元素有行内块特性

一浮全浮,如果一个父元素下有一个加了浮动,一般其他所有多要加浮动属性

浮动指挥影响浮动盒子后面的标准流,前面的不会受影响

清除浮动

为什么要清除浮动:

父盒子在很多情况下不方便给高度,但是盒子浮动之后就不占有位置,最后父级盒子高度为 0 时就会影响像下面的标准流盒子。

本质:清除元素浮动造成的影响

选择器 {clear: 属性值(left/right/both);} , 实际种都用 both

策略:闭合浮动,只让浮动在父盒子内部影响,不影响父盒子外部的其他盒子

方法:

  1. 额外标签法,也称为隔墙法,W3C 推荐做法
  2. 父级添加 overflow 属性
  3. 父级添加 after 伪属性
  4. 腹肌添加双伪属性

额外标签法:

在最后一个浮动子元素后面添加一个额外标签,添加清除浮动样式,实际工作可能会遇到但是不常用

  • 优点:通俗易懂,书写方便
  • 缺点:添加许多无意义的标签,结构差

示例: <div style="clear:both"></div>

PS: 最后的这个元素必须是块级元素,行内元素不行

父元素 overflow:

.body {overflow: hidden;}

属性可选项: hidden, auto 和 scroll

  • 优点: 代码简洁
  • 缺点:无法显示溢出部分

:after 伪元素

声明属性并在父盒子内添加,原理和隔墙法一致,只不过使用 CSS 实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.clearfix:after {
/* 内容为“.”就是一个英文的句号而已。也可以不写 */
content: ".";
/* 加入的这个元素转换为块级元素 */
display: block;
/* 清除左右两边浮动 */
clear: both;
/* -可见度设为隐藏。注意它和display:none;是有区别的。仍然占据空间,只是看不到而已 */
visibility: hidden;
height: 0;
font-size:0;
}
.clearfix {
/* 兼容 IE 6, 7 */
*zoom: 1;
}
  • 优点:没有增加标签,结构接单
  • 缺点:代码多

双伪元素清除浮动

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 声明清除浮动的样式 */
.clearfix:before,
.clearfix:after {
content: "";
display: table;
}
.clearfix:after {
clear: both;
}
/* ie6 7 专门清除浮动的样式*/
.clearfix {
*zoom:1;
}

PS 切图

  • jpg: 色彩信息保留较好,高清,颜色多。产品类图片经常用 jpg 格式
  • gif: 256 色,动态
  • png 可以保存透明背景,切透明背景图片
  • PSD,PS 的图片格式

cutterman 切图神器

CSS 书写规范

  1. 布局定位属性: display/position/float/clear/visibliity/overflow, display 第一个写,关系到模式
  2. 自身属性:width/height/margin/padding/border/background
  3. 文本属性: color/font/text-decoration/text-align/vertical-align/white-space/break-word
  4. 其他属性(CSS3): context/cursor/border-radius/box-shadow/text-shadow/background:linear-gradient…

布局思路

  1. 必须确定版面的版心(可视区),我们测量可得知
  2. 分析页面中的行模块,以及每个行模块种的列模块
  3. 一行中的列模块经常浮动布局,先确定每个列的大小之后,确定列的位置,页面布局第二准则
  4. 制作 html, 我们还是遵循,先有结构,后又样式的原则。结构永远最重要。

实际开发中,我们不会直接用链接 a 而是用 li 包含链接(li+a)的做法。主要是针对 SEO 搜索的优化。

练习先跳过了,后面要用在看

定位 P221

自由的定位,标准流和浮动做不出这种效果

定位 = 定位模式(position) + 边偏移

position = static/relative/absolute/fixed

边偏移 = top/bottom/left/right

静态定位,即默认形式,了解即可 选择器 {position: static;}

相对定位:

  • 自恋型,移动时参照原来的位置
  • 相对于浮动,原来的位置继续保留,不脱标
  • 选择器 {position: relative;}

绝对定位 absolute:

相对与祖先元素来说的,拼爹型 选择器 {position: absolute;}

  1. 如果没有祖先元素或祖先元素没有定位,则以 document 为标准
  2. 如果祖先有定位(相对,绝对,固定定位),则以最近一级有定位祖先元素为参考点移动位置
  3. 绝对定位会脱标,不占有原来位置

子绝父相

固定定位 fixed:

元素可以停在浏览器的可视区域的固定位置,滚动不影响 选择器 {position: fixed;}, 不占用原先的位置

固定定位小技巧:固定在版心右侧位置

  1. 让固定定位的盒子 left: 50% 走到可视区域的一般位置
  2. 让固定定位盒子 margin-left: 版心宽度一般距离

就可以实现该效果

粘性定位 sticky:

  1. 以可是窗口为参照点移动元素
  2. 占有原先位置
  3. 必须设置 top, bottom, left, right 其中一个才会生效

IE 不支持

定位叠放次序 z-index

选择器 {z-index: 1;}

数值可以是正整数,负整数或0,默认是 auto, 数值越大,盒子越靠上

只有定位的盒子才有 z-index 属性

绝对定位居中

left: 50% + margin-left: -盒子本身宽度px;

定位拓展

绝对定位和固定定位也和浮动类似

  1. 行内元素添加绝对定位或者固定定位,可以直接设置宽度和高度
  2. 块级元素添加绝对或者固定定位,如果不给宽度或高度,默认大小是内容大小

脱标的元素不会触发外边距合并

浮动元素只会压住标准流的盒子,不会压住文字,定位会压住文字。

浮动最初产生的动机时用来做环绕效果得,所以不会压住文字

刚好最近遇到需要重写 equals 和 hashcode 的情况,总结记录一下,加深印象。

官方对 hashCode 方法的描述

1
2
3
4
5
6
7
8
9
Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by HashMap.

The general contract of hashCode is:

1. Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
2. If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
3. It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.

As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.)

概括起来就是 equals 相等的两个对象 hashcode 必须相同,反过来,hashcode 相等的像个对象,equals 可以不想等。

hashcode 是需要结合集合类才能体现出来的。试想一下,如果没有 hashcode, 那么我们在一个存了 1000 个对象的 HashSet 中添加一个新的对象就要进行 1000 次的 equals 比较,这样的性能消耗无疑是巨大的。所以 HashXXX 的数据结构引入 hash 算法来简化比较。

hash 的数据结构中是允许存在 hash 值相同的对象的,这种情况下,他会在 hash 的地址位置创建一个链表存储 hash 值相同的对象。

当集合存入一个对象时,他会先根据 hash 值判断是否有重复的元素, 如果 hash 值已经存在,那么他会找到对应的链表然后一次进行对象的 equals 判断重复。好的 hash 算法要尽量减少 hash 冲突来提高检索效率。

2020-11-27

Cross pillar 的改动,我们 team 在做重构的时候改了其他 team 的 code,半年过去了,突然告诉我上线出问题了,也不知道他们之前都是怎么做的测试,无力吐槽。打了 emergency 的 patch。也是各种折腾,第一次 fix 还导致了更大的 issue,瑟瑟发抖。搞到将近一点,release 成功,等第二天的测试结果。索性一切 OK。发现老板工作是真的拼,晚上守到近两点,早上七点多又起来 verify,大老板也是,佩服佩服。敬业没得说,值的学习。

目标:通过阅读 深入理解 JVM 虚拟机 第三版 的第 6 章,结合 ASM 里的 Reader 和 Visitor 对 class 文件有个一比较深入的了解。

6.2 无关性的基石

Java 虚拟机不与包括 Java 语言在内的任何语言绑定,它只与 Class 文件这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集,符号表以及若干其他辅助信息。

6.3 Class 类文件的结构

Idea 安装 BinEd 插件可以查看 Class 文件在各种进制下的值

Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

Class 文件结构中只有两种数据类型:无符号数 + 表

  • 无符号数:是基本数据类,以 u1, u2, u4, u8 分别表示 1,2,4,8 个字节的无符号数。无符号数可以用来描述数字,索引引用,数量值或者按照 UTF-8 编码构成的字符串值
  • 表:n 个无符号数 + 其他表构成的复合数据类型,命名习惯性的以 _info 结尾。表用于描述有层次关系的复合结构数据,整个 Class 可以看作一张表。

Class 文件格式表:

Type Name Count
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 attributes_count 1
attribute_info attributes attributes_count

6.3.1 魔数与 Class 文件的版本

Class 文件前 4 个字节被称为魔数,值为 0xCAFEBABE,用来表明文件类型。紧接着 4 个字节为主次版本号。

Java 虚拟机规范在 Class 文件校验部分明确要求,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。

JDK version version number
JDK 13 57
JDK 12 56
JDK 11 55
JDK 10 54
JDK 9 53
JDK 8 52
JDK 7 51
JDK 6.0 50
JDK 5.0 49
JDK 1.4 48
JDK 1.3 47
JDK 1.2 46
JDK 1.1 45

仿照参考书写下测试代码, 不知道是不是编译器版本不一样,结果从常量池开始有些许偏差,不过无伤大雅,学习路径,方法还是一样的。

1
2
3
4
5
6
7
8
9
package c631;

public class TestClass {
private int m;

public int inc() {
return m + 1;
}
}

Class 文件 16 进制表达式

line 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f hex value
0000000000 ca fe ba be 00 00 00 32 00 16 0a 00 04 00 12 09 …….2……..
0000000010 00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00 ………….m..
0000000020 01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 .I……()
0000000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e V…Code…LineN
0000000040 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 umberTable…Loc
0000000050 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 alVariableTable.
0000000060 00 04 74 68 69 73 01 00 10 4c 63 36 33 31 2f 54 ..this…Lc631/T
0000000070 65 73 74 43 6c 61 73 73 3b 01 00 03 69 6e 63 01 estClass;…inc.
0000000080 00 03 28 29 49 01 00 0a 53 6f 75 72 63 65 46 69 ..()I…SourceFi
0000000090 6c 65 01 00 0e 54 65 73 74 43 6c 61 73 73 2e 6a le…TestClass.j
00000000a0 61 76 61 0c 00 07 00 08 0c 00 05 00 06 01 00 0e ava………….
00000000b0 63 36 33 31 2f 54 65 73 74 43 6c 61 73 73 01 00 c631/TestClass..
00000000c0 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 .java/lang/Objec
00000000d0 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00 t.!………….
00000000e0 06 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00 …………….
00000000f0 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1 ../……..*….
0000000100 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 03 …………….
0000000110 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d …………….
0000000120 00 00 00 01 00 0e 00 0f 00 01 00 09 00 00 00 31 ……………1
0000000130 00 02 00 01 00 00 00 07 2a b4 00 02 04 60 ac 00 ……..*….`..
0000000140 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 07 00 …………….
0000000150 0b 00 00 00 0c 00 01 00 00 00 07 00 0c 00 0d 00 …………….
0000000160 00 00 01 00 10 00 00 00 02 00 11 ………..

魔数值 cafe, 版本号 00 00 00 32 转化后位 50 和我在 pom 指定的 1.6 版本 JDK 编译一致

6.3.2 常量池

第 9-8 个字节表示常量池。常量池是从 1 开始的。示例中对应的值位 00 16 - 22 表明常量池总共有 21 个值。

PS: 常量池的 0 位空余,是为了考虑特殊情况。当指向常量池的数据需要表达 不引用任何常量池中的项目 这样的意思时,可以将索引值设置位 0 表示。

常量池主要存放两大类的常量:字面量 Literal + 符号引用 Symbolic References。字面量接近于 Java 语言层面的常量概念,符号引用则属于编译原理的概念主要包括下面几类常量:

  1. 被模块导出或者开放的包 package
  2. 类和接口的全名限定 Fully Qualified Name
  3. 字段名称和描述符 Desciptor
  4. 方法名称和描述符
  5. 方法句柄和方法类型 Method Handle, Mehtod Type, Invoke Dynamic
  6. 动态调用点和动态常量 Dynamically-Computed Call Site, Dynamically-Computed Constant

Class 文件中没有类似 C 语言中的链接,只有当 Class 文件在虚拟机中加载后才能确定内存分布。

常量池中每一项都是一个表,到 JDK13 为止有 17 种表结构

Type Flag Desc
CONSTANT_Utf8_info 1 UTF-8 编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型型字面量
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 - 10 开头,查看上表得知为 CONSTANT_Methodref_info 类型的表,查询可知对应的表结构为

Const Name Item Length desc
CONSTANT_Methodref_info tag u1 值为 10
- index u2 指向声明方法的类描述符 CONSTANT_Class_info 的索引项
- index u2 指向名称及类型描述符 CONSTANT_NameAndType 的索引项

所以第一个常量值总共 5 个字节组成 0a 00 04 00 12,表示的是方法引用,类引用地址为 4,方法名称和类型地址为 18。

为了反向验证这样分析是否正确可以通过反编译命令 javap -verbose TestClass 查看 class 文件。

第一个常量值内容为 #1 = Methodref #4.#18 // java/lang/Object."<init>":()V 和分析结果一致

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
C:\Users\jack\Downloads\helloworld\understanding-the-jvm\c6-file-structure\target\classes\c631>javap -verbose TestClass
警告: 文件 .\TestClass.class 不包含类 TestClass
Classfile /C:/Users/jack/Downloads/helloworld/understanding-the-jvm/c6-file-structure/target/classes/c631/TestClass.class
Last modified 2020年11月19日; size 363 bytes
MD5 checksum 16826804824a30e99e96960a47c3a47a
Compiled from "TestClass.java"
public class c631.TestClass
minor version: 0
major version: 50
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // c631/TestClass
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // c631/TestClass.m:I
#3 = Class #20 // c631/TestClass
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lc631/TestClass;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 TestClass.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 c631/TestClass
#21 = Utf8 java/lang/Object
{
public c631.TestClass();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lc631/TestClass;

public int inc();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lc631/TestClass;
}
SourceFile: "TestClass.java"

第二个常量为 09 开头,查表可知为 field 的引用

Const Name Item Length desc
CONSTANT_Fieldref_info tag u1 值为 9
- index u2 指向声明字段的类或接口类描述符 CONSTANT_Class_info 的索引项
- index u2 指向字段描述符 CONSTANT_NameAndType 的索引项

值为 09 00 03 00 13 对应 #2 = Fieldref #3.#19 // c631/TestClass.m:I

第三个常量为 07 开头,为 Class 常量表

Const Name Item Length desc
CONSTANT_Class_info tag u1 值为 7
- index u2 指向全限定名常量的索引项

07 00 14 对应 #3 = Class #20 // c631/TestClass

第四个也是 07 开头

07 00 15 - #4 = Class #21 // java/lang/Object

第五个为 01 开头, 表示 Utf8 类型的常量

Const Name Item Type desc
CONSTANT_Utf8_info tag u1 值为 1
- index u2 UTF-8 编码的字符串占用的字节数
- bytes u1 长度为 length 的 UTF-8 编码字符串

01 00 01 6d, 占用字节数 1,内容为 6d 的 UTF 内容 m,对应关系可以通过各种在线工具查看,很常用 #5 = Utf8 m

第六个常量 01 00 01 49 - #6 = Utf8 I

第七个常量 01 00 06 3c 69 6e 69 74 3e 占用字节数 6 个 - #7 = Utf8 <init>

第八个 01 00 03 28 29 56 - #8 = Utf8 ()V

第九个 01 00 04 43 6f 64 65 - #9 = Utf8 Code

第十个 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 - #10 = Utf8 LineNumberTable

第十一个 01 00 12 4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 - #11 = Utf8 LocalVariableTable

第十二个 01 00 04 74 68 69 73 - #12 = Utf8 this

第十三个 01 00 10 4c 63 36 33 31 2f 54 65 73 74 43 6c 61 73 73 3b - #13 = Utf8 Lc631/TestClass;

第十四个 01 00 03 69 6e 63 - #14 = Utf8 inc

第十五个 01 00 03 28 29 49 - #15 = Utf8 ()I

第十六个 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 - #16 = Utf8 SourceFile

第十七个 01 00 0e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 - #17 = Utf8 TestClass.java

第十八个 0c 开头,为 NameAndType 类型

Const Name Item Length desc
CONSTANT_NameAndType_info tag u1 值为 12
- index u2 指向该字段或方法名称常量项的索引项
- index u2 指向该字段或方法描述符常量项的索引项

0c 00 07 00 08 - #18 = NameAndType #7:#8 // "<init>":()V

第十九 0c 00 05 00 06 - #19 = NameAndType #5:#6 // m:I

第二十 01 00 0e 63 36 33 31 2f 54 65 73 74 43 6c 61 73 73 - #20 = Utf8 c631/TestClass

第二十一 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 - #21 = Utf8 java/lang/Object

到此为止,常量池分析完毕

6.3.3 访问标志

紧跟在常量池之后,由两个字节组成,有 16 个标志位,当前只定义了 9 种。

Name flag value 含义
ACC_PUBLIC 0x0001 是否为 public 类型
ACC_FINAL 0x0010 是否为 final 类型, 只有类可设置
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指定的新语义,
invokespecial 语义在 JDK 1.0.2 发生过改变,
为了区别这条指令使用哪种语义,
JDK 1.0.2 之后编译出来的类这个标志必须为真
ACC_INTERFACE 0x0200 是否是一个接口
ACC_ABSTRACT 0x0400 是否为 abstract 类型,对于接口或者抽象类来说,此标志必须为真,其他类型为假
ACC_SYNTHETIC 0x1000 表示这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模块

示例种值为 00 21 即 0020 & 0001 所以是 public + super 类型

6.3.4 类索引,父索引和接口索引集合

  • 类索引(this_class) - u2 类型数据
  • 父索引(super_class) - u2 类型数据
  • 接口索引集合(super_class) - u2 类型数据

这些所以确定类的继承关系,实例中数据 00 03 00 04 00 00 表示 类所以指向常量池第三个常量,父索引指向第四个常量,接口集合数量为 0

#3 = Class #20 // c631/TestClass

#4 = Class #21 // java/lang/Object

6.3.5 字段表集合

用来描述接口或类中声明的变量。这里的变量只包括类级变量以及实例级变量,不包含局部变量。

字段表结构

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

字段修饰符 access_flags 和类的访问修饰符很想都由一个 u2 的数据类型表示

名称 标志值 含义
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_SYNTHTIC 0x0100 字段是否由编译器产生
ACC_ENUM 0x0400 字段是否 enum
  • 作用域修饰符: public/private/protected
  • 是否是类级字段:static
  • 是否可变:final
  • 是否强制主从内存读写:volatile
  • 是否可序列化:transient

name_index 和 descriptor_index 都指向常量池引用,表示字段简单名称以及字段和方法描述符。

  • 全名限定:用斜线分割的 路径+类名
  • 简单名称:只有名字,没有路径信息
  • 方法和字段描述符:参数列表+返回值类型,例如 ()V, (Lcom/lang/Object;)V

基本数据类型含义表

字符 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 对象类型

表示数组类型时,每一维度将使用一个前置的 [ 字符描述,比如 String[][] 表示为 [[Ljava/lang/String;, 整形数组 int[] 表示为 [I

实例中对应的字段表集合内容为 00 01 00 02 00 05 00 06 00 00, interface 之后紧接着为 fields_count 的表示位, 00 01, 表示只有一个 field。

00 02 表示方位权限 private,00 05 表示名字指向常量池第五个常量 m, 00 06 表示描述符指向第六个常量 I00 00 属性表个数位 0 个。

6.3.6 方法表集合

方法表和之前的属性表,class 表是一个套路的, 方法表结构如下

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

方法表的 access_flag 相对 field 少了 volatile 和 trasient, 多了 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 字段是否由编译器自动产生

方法中的具体实现经过 javac 编译成字节码指令后存在属性表集合中一个名为 Code 的属性里面。

实例内容 00 02 00 01 00 07 00 08 00 01 00 09

  • 00 02 - 有两个方法
  • 00 01 - public 类型的方法
  • 00 07 - name 指向常量池7 -
  • 00 08 - 描述符指向8 - ()V
  • 00 01 - 属性数量 1
  • 00 09 - 属性表索引 9,指向 Code

方法签名:Java 语法中的方法签名可以从重载(Overload)理解。Java 中重载要求方法名一致,参数列表及参数类型不同。返回值并不在比较范围内。方法除了返回值不同的重载是会编译错误的。但是在字节码的语义中,只有返回值不同的重载是合法的。

6.3.7 属性表集合

属性表集合的限制比前面那些结构要宽松一些,对虚拟机不认识的属性,会自动跳过。到 java 12 一共有 29 种预定义的属性

属性名称 使用位置 含义
Code 方法表 Java代码编译成的自己吗指令
ConstantValue 字段表 由 final 关键字定义的常量值
Deprecated 类,方法,字段表 被声明为 deprecated 的方法和字段
Exceptions 方法表 方法抛出的异常列表
EnclosingMethod 类文件 仅当一个类为局部类或匿名类是才拥有这个属性,用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java 源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK6 新增,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需的类型是否匹配
Signature 类,方法表和字段表 JDK5新增,用于支持泛型情况下的方法签名
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 JDK5新增,存储额外的调试信息
Synthetic 类,方法表,字段表 标识是否由编译器产生
LocalVariableTypeTable JDK5新增,使用特征签名代替描述符,为了支持泛型
RuntimeVisibleAnnotations 类,方法表,字段表 JDK5新增,为动态注解提供支持
RuntimeInVisibleAnnotations 类,方法表,字段表 JDK5新增,为动态注解提供支持,标识不可见
RuntimeVisibleParameterAnnotations 方法表 JDK5新增,作用对象为方法参数
RuntimeInvisibleParameterAnnotations 方法表 JDK5新增,作用对象为方法参数
AnnotationDefault 方法表 JDK5新增,注解类元素默认值
BootstrapMethods 类文件 JDK7新增,保存 invokedynamic 指令引用的引导犯法限定符
RuntimeVisibleTypeAnnotations 类,方法表,字段表, Code属性 JDK8新增
RuntimeInvisibleTypeAnnotations 类,方法表,字段表, Code属性 JDK8新增
MethodParameters 方法表 JDK8新增
Module JDK9新增
ModulePackages JDK9新增
ModuleMainClass JDK9新增
NestHost JDK11新增
NestMembers JDK11新增

属性表结构

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

attribute_name_index 指向常量池中的一个引用,属性值结构完全自定义,attribute_length 说明属性值所占的位数。

Code

Java 方法体种的代码经过 javac 编译器处理之后都会转化为字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合中,但并非所有方法表都必须存在这个属性,比如抽象方法或接口中就可以不存在 Code 属性。

Code 属性表的结构

类型 名称 数量 含义
u2 attribute_name_index 1 指向 CONSTANT_Utf8_info 常量的索引,为固定值 Code
u4 attribute_length 1 属性值长度
u2 max_stack 1 操作数栈深度的最大值
u2 max_locals 1 局部变量表存储空间,单位-变量槽(Slot)
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 attribute_count -

对于 byte, char, float, int, short, boolean 和 returnAddress 等长度不超过 32 byte 的数据类型,每个局部变量占用一个变量槽,double, long 这两个 64 位的占两个槽。

同时生存的最大局部变量和类型计算出 max_locals

字节码指令长度 u1。u1 可以最多表达 255 个指令,现在大约已经定义了 200 条。

测试案例中 init 方法对应的 code 代码块为 00 09 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 02

00 09 前面已经说过,指向固定的 Code 字符地址

00 00 00 31 属性表长度 3*16 + 1 = 49

00 01 栈深 1

00 01 本地变量表大小 1

00 00 00 05 code 长度 5

2a b7 00 01 b1 code 内容

  • 2a: aload_0 将第一个变量推送至栈顶
  • b7 invokespecial, 后面接一个 u2 类型引用数据,执行构造方法或 private 方法,或它的父类方法
  • 00 01 方法引用,指向 init
  • b1 return 指令

对应的 javap 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public c631.TestClass();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lc631/TestClass;

args_size=1 方法虽然没有参数,但是 Java 编译时会把 this 作为第一个默认参数塞入 code 代码块中。

00 00 00 02 异常表长度 0, 属性表长度 2

异常表结构

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

异常代码案例

1
2
3
4
5
6
7
8
9
10
11
12
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}

对应的 javap 代码

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
28
29
30
31
32
33
public int inc();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: iload_2
7: ireturn
8: astore_2
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4
19: iconst_3
20: istore_1
21: aload 4
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any

和书上的结果略有差别,但基本一致

Exceptions 属性

和 Code 平级的概念,并不是上一章节里 Code 下面的 exception 表。这里表示的是方法可能抛出的异常,就是 throws 后面的那些东西。属性结构如下:

type name count
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 索引

LineNumberTable 属性

描述 Java 源码行号和字节码行号之间的对应关系。可以在编译时指定不生成行号,但是会影响异常信息显示和 debug, 表结构如下:

type name count
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

line_number_info: 包含 start_pc 和 line_number 两个 u2 类型的数据项,前者是字节码行号,后者是 Java 源码行号。

LocalVarableTable 及 LocalVarableTypeTable 属性

LocalVarableTable 描述局部变量表的变量与 Java 源码中定义的变量之间的关系。非必须,可以指定 javac 参数去除且不影响运行。但是去除后方法参数名称会变为类似 arg0, arg1 的表示,不方便,表结构如下:

type name count
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_info 代表栈帧与源码中局部变量的关联,结构如下:

type name count
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1
  • start_pc + length: 限定了局部变量的作用范围,即作用域
  • name_index + descriptor_index: 指向常量池中 CONSTANT_Utf8_info 类型索引
  • index: 栈帧局部变量槽位置,当数据类型为 64 位则占用 index 和 index+1 两个

LocalVarableTypeTable 是 JDK5 时为了支持泛型而引入的,基本功能和 LocalVarableTable 一样。

SourceFile 及 SourceDebugExtension 属性

SourceFile 记录生成 Class 文件的源码文件名称,可选,通常与类名同,特殊情况除外(如内部类)。表结构如下:

type name count
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

sourcefile_index: 指向常量池中 CONSTANT_Utf8_info 型常量的索引,值问文件名。

SourceDebugExtension 是 JDK5 中加入的新特性,存储额外调试信息,支持类似 JSP 这种使用 Java 编译器但是语法不同的语言,类中最多只允许一个该属性。表结构如下:

type name count
u2 attribute_name_index 1
u4 attribute_length 1
u2 debug_extension[attribute_length] 1

ConstantValue 属性

ConstantValue 通知虚拟机自动为静态变量赋值。只有被 static 修饰的变量才能使用这个属性。虚拟机中对非 static 变量在 () 方法总进行,对于静态变量则有两种方式,一种是构造器 () 另一种是 ConstantValue。Oracle 的 javac 中的实现方式为:static + final + 基本类型/String 在 ConstantValue 中赋值, 没有 final 或者是其他数据类型则在 () 中赋值。表结构如下:

type name count
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。

InnerClasses 属性

InnerClasses 记录内部类与宿主类之间的关联。结构如下:

type name count
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 结构如下

type name count
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:指向常量池中 CONSTANT_Class_info 常量索引,分别代表内部类和宿主类

inner_name_index:指向常量池 CONSTANT_Utf8_info 引用,代表内部类名称,如果是匿名内部类,值为 0

inner_class_access_flags:和 class 定义相似,类的访问标示符,取值范围如下

标志名称 标志值 含义
ACC_PUBLIC 0x0001 内部类是否为 public
ACC_PRIVATE 0x0002 内部类是否为 private
ACC_PROTECTED 0x0004 内部类是否为 protected
ACC_STATIC 0x0008 内部类是否为 static
ACC_FINAL 0x0010 内部类是否为 final
ACC_INTERFACE 0x0020 内部类是否为 接口
ACC_ABSTRACT 0x0400 内部类是否为 abstract
ACC_SYNTHETIC 0x1000 内部类是否并非由用户代码产生
ACC_ANNOTATION 0x2000 内部类是否为一个注解
ACC_ENUM 0x4000 内部类是否为一个枚举

Deprecated 及 Synthetic 属性

都是标志符类型的布尔属性,只有存在有和没有的区别,没有属性概念。Deprecated 对应 @deprecated 注解,表示不推荐使用。

Synthetic 标示字段或方法由编译器产生,JDK5之后同样的功能可以通过设置 ACC_SYNTHETIC 标志位达到。通过这种方式甚至可以越权访问或绕开语言限制功能。典型例子是枚举类中自动生成枚举元素数组和嵌套类的桥接方法(Bridge Method)。

type name count
u2 attribute_name_index 1
u4 attribute_length 1

attribute_length 必须为 0x00000000,因为诶呦任何属性需要设置。

StackMapTable 属性

JDK6 增加到 Class 文件规范,一个相当复杂的变长属性,位于 Code 属性表中,用来代替原来的类型检查验证器,提升性能。实现很复杂,Java SE7 新增 120 页篇幅讲解描述。

type name count
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame entries number_of_entries

SE7 之后规定,版本号 >= 50.0 的 class 文件都必须带有 StackMapTable 属性。一个 Code 属性最多只能有一个 StackMapTable 不然抛错 ClassFormatError。

Signature 属性

在 JDK5 中和泛型一起加入的,记录泛型签名信息。Java 中的泛型是伪泛型。

type name count
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

signature_index 指向常量池的一个 CONSTANT_Utf8_info 索引。

BootstrapMethods 属性

JDK7 时新增,JDK8 中通过 lambda 发扬光大。位于类文件属性表中,用于保存 invokeDynamic 指令引用的引导方法限定符。类文件常量池中出现过 CONSTANT_InvokeDynamic_info 类型的常量,那么属性表中必有 BootstrapMethods 属性,一个类文件中至多只能有一个 BootstrapMethods 属性。

BootstrapMethods 属性结构

type name count
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_bootstrap_methods 1
bootstrap_method bootstrap_methods num_bootstrap_methods

bootstrap_methods[]: 每个成员包含一个指向常量池 CONSTANT_MethodHandle 结构的索引,代表一个引导方法。

bootstrap_method 属性结构

type name count
u2 bootstrap_method_ref 1
u2 num_bootstrap_arguments 1
u2 bootstrap_arguments num_bootstrap_arguments
  • bootstrap_method_ref:对常量池的一个有效索引,索引处必须是一个 CONSTNAT_MethodHandle_info 结构
  • num_bootstrap_arguments:arg 数量
  • bootstrap_arguments:每个成员必须是对常量池的有效引用,指向的结构必须是:CONSTANT_String_info,CONSTANT_Class_info, CONSTANT_Integer_info, CONSTANT_Long_info, CONSTANT_Float_info, CONSTANT_Double_info, CONSTANT_MethodHandle_info 或 CONSTANT_MethodType_info 之一

MethodParameters 属性

JDK8 时加入,之前没有这个属性, jar 包反编译时缺少参数信息,不方便理解,影响传播。之前还有个替代方案,通过 ‘-g:var’ 存入 LocalVariableTable, 但是他时 Code 的字表,在接口方法这类没有具体实现的方法时不生效。

type name count
u2 attribute_name_index 1
u4 attribute_length 1
u1 parameters_count 1
parameter parameters parameters_count

parameter 属性

type name count
u2 name_index 1
u2 access_flags 1

name_index 指向常量池 CONTANT_Utf8_info 的索引值,代表名称

access_flags 有三种 0x0001-ACC_FINAL, 0x1000-ACC_SYNTHETIC, 0x8000-ACC_MANDATED(原文件中隐式定义,典型用法 this)

模块化相关属性

TBD 怎是没用到就不记了,以后用到再看看

运行时注解相关属性

JDK5 时加入了注解相关信息到 Class 文件,他们是 RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations 和 RuntimeInvisibleParameterAnnotations。JDK8 时新家了 RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations。这些属性功能和结构都很雷同。

RuntimeVisibleAnnotations 属性结构

type name count
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_annotations 1
annotation annotations num_annotations

annotations 属性结构

type name count
u2 type_index 1
u2 num_element_value_pairs 1
element_value_pair element_value_pairs num_element_value_pairs

type_index 指向常量池 CONSTANT_Utf8_info 常量的索引, num_element_value_pairs 数组计数器,element_value_pair 为键值对

6.4 字节码指令简介

虚拟机指令 = 操作码(opcode) + 操作数(oprand)

操作码为一个字节长度,操作数为 0 至 n 个,虚拟机执行模型

1
2
3
4
5
6
do {
自动计算 PC 寄存器的值加 1;
根据 PC 寄存器指示的位置,从字节码流中取出操作码;
if (字节码存在操作数) 从字节码流中取出操作数;
执行操作吗所定义的操作;
} while (字节码流长度 > 0)

6.4.1 字节码与数据类型

大多数操作码都包含对应操作数类型信息,比如 iload。

  • i - int
  • l - long
  • s - short
  • b - byte
  • c - char
  • f - float
  • d - double
  • a - reference

boolean, byte, short, char 在编译时会被扩展成 int 类型再处理。

6.4.2 加载和存储指令

用于将数据在栈帧中的局部变量表和操作数栈之间来回传输

将局部变量加载到操作栈:(i/l/f/d/a)load, (i/l/f/d/a)load_

将一个数值从操作数栈存储到局部变量表:(i/l/f/d/a)store, (i/l/f/d/a)store_

将一个常量加载到操作数栈:bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, icont_, lconst_, fconst_, dconst_

扩充局部变量表的访问索引指令: wide

iload_ 代表了 iload_1, iload_2, iload_3

6.4.3 运算指令

算术指令用于对 操作数栈 上的两个值进行某种特定运算,并把 结果 重新存入操作栈 顶。byte, short, char 和 Boolean 会转化为 int 计算

  • 加法指令: (i, l, f, d)add
  • 减法指令: (i, l, f, d)sub
  • 乘法指令: (i, l, f, d)mul
  • 除法指令: (i, l, f, d)div
  • 求余指令: (i, l, f, d)rem
  • 取反指令: (i, l, f, d)neg
  • 位移指令: ishl, ishr, iushr, lshl, lshr, lushr
  • 按位或指令: ior, lor
  • 按位与指令: iand, land
  • 按位异或指令: ixor, lxor
  • 局部变量自增指令: iinc
  • 比较指令: dcmpg, dcmpl, fcmpg, fcmpl, lcmp

6.4.4 类型转换指令

该指令可以将两种不同数值类型的数据互相转化,这些转化操作一般用于用户代码中的显示类型转化,或者前面提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

虚拟机直接支持宽化类型转化,及小范围向大范围转换

  1. int 类型到 long, float, double
  2. long 到 float, double
  3. float 到 double

窄化转化指令: i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f。

窄化转化可能发生上限溢出,下限溢出 或精度丢失,但是这些问题都不会抛出运行时异常。

6.4.5 对象创建与访问指令

  • 创建类实例 new
  • 创建数组 newarray, anewarray, multianewarray
  • 访问类字段和实例字段的指令:getfield, putfield, getstatic, putstatic
  • 把一个数组元素加载到操作数栈中的指令:baload, caload, saload, iaload, laload, faload, daload, aaload
  • 将一个操作数栈的值存储到数组元素中:bastore, castore, sastore, iastore, fastore, dastore, aastore
  • 取数组长度的指令:arraylength
  • 检查类实例类型的指令:instanceof, checkcast

6.4.6 操作数栈管理指令

  • 将操作数栈栈顶的一个或两个元素出栈:pop, pop2
  • 复制栈顶的一个或两个数值并将复制或双份复制值重新压入栈顶:dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2

6.4.7 控制转移指令

可以让 Java 虚拟机有条件或五天见的从指定位置指令的吓一跳指令继续执行程序。

  • 条件分支: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icompgt, if_icomple, if_icompge, if_acmpeq, if_acmpne
  • 复合条件分支:tableswitch, lookupswitch
  • 无条件分支:goto, goto_w, jsr, jsr_w, ret

6.4.8 方法调用和返回指令

  • invokevirtual: 调用对象的实例方法,根据对象的世纪类型进行分派,Java 中最常见的分派方式
  • invokeinterface: 调用接口方法,运行时搜索一个实现了该接口方法的对象,找出适合的方法进行调用
  • invokespecial: 调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
  • invokestatic: 调用静态方法
  • invokedynamic: 运行时动态解析出调用点限定符所应用的方法,并执行该方法。

返回指令:当返回值是 boolean, byte, char, short, int 时使用 ireturn, 其他还有 lreturn, freturn, dreturn 和 areturn。还有为 void 准备的 return。

6.4.9 异常处理指令

Java 中显示的排除异常操作由 athrow 指令实现,虚拟机中异常处理不是由字节码指令实现,而是通过 异常表

6.4.10 同步指令

虚拟机支持方法级别的同步和方法内部一段指令序列的同步,这两种同步结构都是用管程,也叫锁。方法级别的管程是隐示的无需通过字节码指令控制。他的实现在方法调用和返回之间。虚拟机可以重常量池方法表结构中的 ACC_SYNCHRONIZED 得知是否被声明为同步方法。如果执行时出现异常,同步方法所持有的锁会在异常抛到同步方法边界之外时自动释放。对应的指令为 monitorenter 和 monitorexit。

虚拟机必须保证每条 monitorenter 指令都有一条 monitorexit 指令与之对应。

6.5 公有设计,私有实现

Class 文件格式和字节码集是完全独立于操作系统和虚拟机实现的,任何一款虚拟机实现都必须能够读取 Class 文件并精确实现包含在其中的 Java 虚拟机代码的语义。虚拟机规范鼓励在满足约束的条件下修改和优化实现。虚拟机实现方式主要有两种:

  1. 将输入的 Java 虚拟机代码在加载或执行时翻译成另一种虚拟机代码
  2. 将输入的 Java 虚拟机代码在加载或执行时翻译成宿主机本地指令集,即 即时编译器代码生成技术

6.6 Class 文件结构的发展

相对与 Java 技术体系的变化,Class 文件结构可谓是相当的稳定了。。。

记录下收集到的 ASM 的基础概念和例子

参考资料

ASM 官方 repo

Type

Type 类代 field 和 method 的 descriptor 属性。就是类似 (Ljava/lang/String;)V 这种表达式。该类中所有的方法几乎都是静态的,提供的功能也基本一致,传入某种类型的参数,然后返回代表 descriptor 的 type 对象。当传入为 method 类型的参数时,type 中会包含 参数 + 返回值 类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test() throws NoSuchMethodException {
System.out.println(Type.getType(String.class));
System.out.println(Type.getType(this.getClass().getMethod("testGetTypeFromDescriptor", String.class)));
System.out.println(Arrays.toString(Type.getArgumentTypes("(Ljava/lang/String;)V")));
System.out.println(Type.getType(this.getClass().getMethod("getList")));
System.out.println(Type.getType(this.getClass().getMethod("getArr")));
}

public void testGetTypeFromDescriptor(final String descriptor) {}
public List<String> getList() {return Collections.emptyList();}
public String[] getArr() {return new String[]{"A"};}

// output:
// Ljava/lang/String;
// (Ljava/lang/String;)V
// [Ljava/lang/String;]
// ()Ljava/util/List;
// ()[Ljava/lang/String;

Junit5 是怎么实现 Excutable 接口的? 看不懂

1
2
3
4
5
6
@Test
public void testConstructor_validApi() {
Executable constructor = () -> new ClassVisitor(Opcodes.ASM4) {};

assertDoesNotThrow(constructor);
}

ClassReader

这个类可以看作字节码文件的读操作的入口,只负责读取,其他处理逻辑是在 XXXVisitor 里面实现的。

PS: 该类方法打印的类信息都是斜线 / 分割的

官方测试是怎么测试 ClassReader, ClassVisitor 等类的

ClassReader 其核心功能有两个,一个是解析文件流,拿到基本信息,比如编译版本,常量池信息等。另一个是定义 Visitor 接解析顺序。

测试时官方也分两类,第一类就是测试解析出来的信息,比如 ‘testGetClassName’ 等,第二类就是测试流程的,比如 ‘testAccept_emptyVisitor’。

测试 Visitor 时也很简单,定义一个自己的 Visitor,官方示例中是定义一个 LogMethodVisitor,然后直接调用对应 visitor.visitMethodInsn 等方法,测试自定义在 LogMethodVisitor 里的逻辑是不是符合预期就行了。这个还是很有启发的,可以用这种方法来完善 TraceSonar 的项目。

Class 文件的格式

以 ASM 官方例子的 ClassVisitorTest.class 为例,注意查看的是 .class 文件,不是 .java 文件。下载插件 BinEd 可以查看文件在不同进制下的内容。右键 class 文件 open as Binary 即可。

Class 文件在格式上有特殊的规定,比如以 16 进制打开 class 可以看到前 4 位值为:0xCAFE(B1100 1010 1111 1110/202 254)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testReadByte() throws IOException {
ClassReader classReader = new ClassReader(getClass().getName());
System.out.println(bytesToHex(Arrays.copyOf(classReader.classFileBuffer, 2)));
}

private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
// output: CAFE

最近遇到一个需求需要在 Spring web service 启动之后立即执行,类似一个初始化的工作,搜出来有好多实现方式,稍微记录一下他们的区别

ApplicationListener

1
2
3
4
5
6
7
@Component
public class InitTraceSourceEventListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
System.out.println("InitTraceSourceEventListener triggered...");
}
}

或者

1
2
3
4
5
6
7
8
9
10
@Configuration
public class ProjectConfiguration {
private static final Logger log =
LoggerFactory.getLogger(ProjectConfiguration.class);

@EventListener(ApplicationReadyEvent.class)
public void doSomethingAfterStartup() {
log.info("hello world, I have just started up");
}
}

SpringBootServletInitializer

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application extends SpringBootServletInitializer {

@SuppressWarnings("resource")
public static void main(final String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);

context.getBean(Table.class).fillWithTestdata(); // <-- here
}
}

@PostConstruct

1
2
3
4
5
6
7
8
9
10
@Component
public class Monitor {
@Autowired private SomeService service

@PostConstruct
public void init(){
// start your monitoring in here
}
}

ApplicationRunner

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.boot.ApplicationRunner;

@Component
public class ServerInitializer implements ApplicationRunner {

@Override
public void run(ApplicationArguments applicationArguments) throws Exception {

//code goes here

}
}

CommandLineRunner

1
2
3
4
5
6
7
8
9
@Component
public class CommandLineAppStartupRunner implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(CommandLineAppStartupRunner.class);

@Override
public void run(String...args) throws Exception {
logger.info("Application started with command-line arguments: {} . \n To kill this application, press Ctrl + C.", Arrays.toString(args));
}
}

InitializingBean

1
2
3
4
5
6
7
8
9
10
11
@Component
class MyInitializingBean implements InitializingBean {

private static final Logger logger = ...;

@Override
public void afterPropertiesSet() throws Exception {
logger.info("InitializingBean#afterPropertiesSet()");
}

}

Springboot 项目中怎么添加本地 jar 包记录

  1. 在项目资源目录下创建一个文件夹,用来存放本地 jar 包
  2. 在 pom.xml 中添加 本地 jar 包的引用,引用目录为第一步创建的文件目录
  3. 在 pom.xml 的 plugins 中添加编译打包的目录,使本地jar包能打到项目中去
1
2
3
4
5
6
resources
├── application.properties
├── lib
│ └── tracesonar-0.1-SNAPSHOT.jar (本地包)
├── static
└── templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- add local lib reference -->
<dependency>
<groupId>com.github.TraceSonar</groupId>
<artifactId>TraceSonar</artifactId>
<version>0.0.3</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/tracesonar-0.1-SNAPSHOT.jar</systemPath>
</dependency>

<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<extdirs>${project.basedir}/src/main/resources/lib</extdirs>
</compilerArguments>
</configuration>
</plugin>
</plugins>

最近在看 TraceSonar 的源码的时候,看到生成树相关的代码, 感觉我自己徒手写应该是没戏了,至少他的这个方案是可以 work 的,同时像收集一下网上能找到的生成树的一些优秀代码

实践

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import java.util.ArrayList;
import java.util.List;

public class Node<T> {
private T data;
private Node<T> parent = null;

private List<Node<T>> children = new ArrayList<>();

public Node(T data) {
this.data = data;
}

public Node<T> addChild(Node<T> child) {
child.setParent(this);
this.children.add(child);
return child;
}

public void addChildren(List<Node<T>> children) {
children.forEach(each -> each.setParent(this));
this.children.addAll(children);
}

public List<Node<T>> getChildren() {
return children;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

private void setParent(Node<T> parent) {
this.parent = parent;
}

public Node<T> getParent() {
return parent;
}

public Node<T> getRoot() {
if (parent == null) {
return this;
}
return parent.getRoot();
}
}

@Test
public void test() {
Node<String> root = new Node<>("root");

Node<String> node1 = root.addChild(new Node<>("node 1"));

Node<String> node11 = node1.addChild(new Node<>("node 11"));
node11.addChild(new Node<>("node 111"));
node11.addChild(new Node<>("node 112"));

node1.addChild(new Node<>("node 12"));

Node<String> node2 = root.addChild(new Node<>("node 2"));

node2.addChild(new Node<>("node 21"));
node2.addChild(new Node<>("node 22"));

printTree(root, " ");
}

private static <T> void printTree(Node<T> node, String appender) {
System.out.println(appender + node.getData());
node.getChildren().forEach(each -> printTree(each, appender + appender));
}

// output:
// root
// node 1
// node 11
// node 111
// node 112
// node 12
// node 2
// node 21
// node 22

这种解法,首先把树这种结构解析出来,单独作为一个载体,你可以根据自己的需求填充树中的内容,其次打印的时候用的 lambda 表达式也很简洁,很喜欢这个例子。

参考

javagists

今天突然发现了一个 class loader 的新用法

1
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources("my.xml");

getResources 竟然连 jar 包中的资源文件也会加载,以前一只以为只会加载当前项目的资源的,孤陋寡闻了,哈哈哈哈 ε-(´∀`; )