Java扩展第三方Jar包

前言

今天在B站大学学习并发编程的时候,老师引用了一个第三方的jar包(jol-core),maven坐标如下,来打印锁对象的Mark Word字节码,从而更直观察地多线程下加偏向锁的情况。

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>

这些都是挺常规的操作,接着老师的骚操作来了,扩展了jar包里面的方法。弹幕里面都是惊呼声,老师改了jar包,我们学习者没法复现了呀。

我这人就喜欢钻牛角尖,评论区翻遍了也没见得有这个Jar包修改方法的 好心人,那就只能自己动手咯。

视频地址

Jol-Core依赖加载失败

如果你没有这个问题可直接跳过

在加载Maven依赖的时候,Jol-Core这个Jar包就是拉不下来,因此我重新更新了一下Maven settings.xml中的 mirror,有需要的直接在你的 mirrors中追加即可

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
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/repository/central</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>sprintio</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>https://repo.spring.io/libs-snapshot/</url>
</mirror>
<mirror>
<id>huaweicloud</id>
<name>mirror from maven huaweicloud</name>
<url>https://mirror.huaweicloud.com/repository/maven/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>maven-default-http-blocker</id>
<mirrorOf>external:http:*</mirrorOf>
<name>Pseudo repository to mirror external repositories initially using HTTP.</name>
<url>http://0.0.0.0/</url>
<blocked>true</blocked>
</mirror>

拉取源码

我这边目前是用的最新版本,直接在Maven除选中依赖右击 Download Soures

image-20231031180032110

在左侧项目的外部库下面展开对应jar包,然后右键选打开于Explore

image-20231031181021905

找到jol-core-0.17-sources.jar包右击解压出来,我这边是用的7-Zip

image-20231031181148520

新建项目

  1. 新建一个项目
  2. 将解压出来的org文件夹复制到新建项目中的 src/main/Java
  3. META-INF文件夹放入resource文件夹中
  4. META-INF下的pom.xml文件复制到项目根目录下
  5. 加载pom.xml, 运行Maven加载依赖即可

image-20231031181509946

修改源码

打开ClassLayout文件,双击两下shift可快速查找文件,在Ctrl+O查找toPrintable方法,可以看到默认没有改源码之前只有一个toPrintable方法

image-20231031182309171

这里我是参考toPrintable()方法进行重写的,看似吊炸天其实也就这样,没啥太多的代码,也就一百多行而已,那么我就来带大家一起来解读一下这个方法的作用吧

源码解读

如果您不需要解读请往下滑

toPrintable 主要用于输出一个对象的内部布局信息,包括对象的标头(Mark Word 和 Class Word)、数组长度(如果对象是数组),字段信息以及空间损失等。

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
public String toPrintable(Object instance) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);

// 计算最长的类型名称
int maxTypeLen = "TYPE".length();
for (FieldLayout f : fields()) {
maxTypeLen = Math.max(f.typeClass().length(), maxTypeLen);
}
maxTypeLen += 2;

// 定义一些描述信息的字符串
String MSG_OBJ_HEADER = "(object header)";
String MSG_MARK_WORD = "(object header: mark)";
String MSG_CLASS_WORD = "(object header: class)";
String MSG_ARR_LEN = "(array length)";
String MSG_FIELD_GAP = "(alignment/padding gap)";
String MSG_OBJ_GAP = "(object alignment gap)";

// 计算最长的描述信息的长度
int maxDescrLen = "DESCRIPTION".length();
maxDescrLen = Math.max(maxDescrLen, MSG_OBJ_HEADER.length());
maxDescrLen = Math.max(maxDescrLen, MSG_MARK_WORD.length());
maxDescrLen = Math.max(maxDescrLen, MSG_CLASS_WORD.length());
maxDescrLen = Math.max(maxDescrLen, MSG_FIELD_GAP.length());
maxDescrLen = Math.max(maxDescrLen, MSG_OBJ_GAP.length());
for (FieldLayout f : fields()) {
maxDescrLen = Math.max(f.shortFieldName().length(), maxDescrLen);
}
maxDescrLen += 2;

// 定义格式化字符串
String format = "%3d %3d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n";
String formatS = "%3s %3s %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n";

// 检查传入的实例是否为null
if (instance != null) {
try {
// 加载类,用于检查实例类型是否匹配
Class<?> klass = ClassUtils.loadClass(classData.name());
if (!klass.isAssignableFrom(instance.getClass())) {
throw new IllegalArgumentException("Passed instance type " + instance.getClass() + " is not assignable from " + klass + ".");
}
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Class is not found: " + classData.name() + ".");
}
}

// 输出类的名称和对象内部信息的标题行
pw.println(classData.name() + " object internals:");
pw.printf(formatS, "OFF", "SZ", "TYPE", "DESCRIPTION", "VALUE");

// 初始化标头、类标头和数组长度的字符串
String markStr = "N/A";
String classStr = "N/A";
String arrLenStr = "N/A";

// 获取标头、类标头和数组长度的大小
int markSize = model.markHeaderSize();
int classSize = model.classHeaderSize();
int arrSize = model.arrayLengthHeaderSize();

// 计算标头、类标头和数组长度的偏移
int markOffset = 0;
int classOffset = markOffset + markSize;
int arrOffset = classOffset + classSize;

// 如果实例不为null,获取标头信息
if (instance != null) {
VirtualMachine vm = VM.current();
if (markSize == 8) {
long mark = vm.getLong(instance, markOffset);
String decoded = (classSize > 0) ? parseMarkWord(mark) : "(Lilliput)";
markStr = toHex(mark) + " " + decoded;
} else if (markSize == 4) {
int mark = vm.getInt(instance, markOffset);
String decoded = (classSize > 0) ? parseMarkWord(mark) : "(Lilliput)";
markStr = toHex(mark) + " " + decoded;
}

// 如果类标头有大小,获取类标头信息
if (classSize == 8) {
classStr = toHex(vm.getLong(instance, classOffset));
} else if (classSize == 4) {
classStr = toHex(vm.getInt(instance, classOffset));
}

// 如果类是数组,获取数组长度信息
if (classData.isArray()) {
arrLenStr = Integer.toString(vm.getInt(instance, arrOffset));
}
}

// 输出标头、类标头和数组长度的信息
pw.printf(format, markOffset, markSize, "", MSG_MARK_WORD, markStr);
if (classSize > 0) {
pw.printf(format, classOffset, classSize, "", MSG_CLASS_WORD, classStr);
}
if (classData.isArray()) {
pw.printf(format, arrOffset, arrSize, "", MSG_ARR_LEN, arrLenStr);
}

long nextFree = headerSize();

// 输出字段信息
for (FieldLayout f : fields()) {
if (f.offset() > nextFree) {
pw.printf(format, nextFree, (f.offset() - nextFree), "", MSG_FIELD_GAP, "");
}

Field fi = f.data().refField();
pw.printf(format,
f.offset(),
f.size(),
f.typeClass(),
f.shortFieldName(),
(instance != null && fi != null) ? ObjectUtils.safeToString(ObjectUtils.value(instance, fi)) : "N/A"
);

nextFree = f.offset() + f.size();
}

long sizeOf = (instance != null) ? VM.current().sizeOf(instance) : instanceSize();

// 输出总大小和空间损失信息
if (sizeOf != nextFree) {
pw.printf(format, nextFree, lossesExternal, "", MSG_OBJ_GAP, "");
}

pw.printf("Instance size: %d bytes%n", sizeOf);
pw.printf("Space losses: %d bytes internal + %d bytes external = %d bytes total%n", lossesInternal, lossesExternal, lossesTotal);

pw.close();

return sw.toString();
}

重构方法

可以看出toPrintable方法不止输出了MarkWord信息还有 Class Word、数组长度(如果对象是数组),字段信息以及空间损失等。

所以我们新增一个方法参考上述方法写就行了

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
public String toPrintableSimpleSerMs() {
return toPrintableSimpleSerMs(classData.instance());
}

private String toPrintableSimpleSerMs(Object instance) {
StringBuilder sb = new StringBuilder();
String markStr = "";
String remind = "";

// 获取标头的大小
int markSize = model.markHeaderSize();

// 设置标头的偏移
int markOffset = 0;

// 如果传入的实例不为null,获取标头信息
if (instance != null) {
VirtualMachine vm = VM.current();
if (markSize == 8) {
// 如果标头大小为8字节,获取标头信息并将其转换为二进制字符串
long mark = vm.getLong(instance, markOffset);
markStr = Long.toBinaryString(mark);
// 解析标头,获取额外的信息
remind = parseMarkWord(mark);
} else if (markSize == 4) {
// 如果标头大小为4字节,获取标头信息并将其转换为二进制字符串
int mark = vm.getInt(instance, markOffset);
markStr = Integer.toBinaryString(mark);
// 解析标头,获取额外的信息
remind = parseMarkWord(mark);
}
}

// 高位补0,确保二进制字符串长度为标头大小的倍数
int i = 1;
for (; i <= 8 * markSize - markStr.length(); i++) {
sb.append('0');
if (i % 8 == 0) {
sb.append(" ");
}
}

// 将标头的二进制字符串添加到输出中
for (; i <= 8 * markSize; i++) {
sb.append(markStr.charAt(i - (8 * markSize - markStr.length()) - 1));
if (i % 8 == 0) {
sb.append(" ");
}
}

// 添加解析后的标头信息
sb.append(remind);

// 返回包含标头信息的字符串
return sb.toString();
}

另外我还写了另外的一个简约版本, 只会输出MarkWord 头中的最后三位,也就是锁和锁的状态

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
/**
* 重写toPrintable方法,只输出Mark word二进制形式 Opt优化
*
* @return
*/
public String toPrintableSimpleSimplicity() {
return toPrintableSimpleSimplicity(classData.instance());
}


/**
* 重写toPrintable方法,只输出Mark word二进制形式 Opt优化
*
* https://serms.top
*
* @param instance
* @return
*/
public String toPrintableSimpleSimplicity(Object instance) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);

if (instance != null) {
VirtualMachine vm = VM.current();
long markWord = vm.getLong(instance, 0); // Assuming the Mark Word is a long (64 bits)

pw.println("Mark Word Simplicity (binary):");
pw.println(toBinary(markWord));
} else {
pw.println("Mark Word: N/A");
}

pw.close();

return sw.toString();
}

private String toBinary(long value) {
return Long.toBinaryString(value);
}

重构Jar包

  1. 修改完代码之后Ctrl+ F9编译,编译之后找到原来的Jar包打开

    image-20231031183619237

  2. 找到ClassLayout.class文件删除

    image-20231031183728890

  3. 将修改好的ClassLayout.class文件复制进去

    image-20231031183906267

测试代码

方法一

调用新增方法

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws NoSuchFieldException, InterruptedException, IllegalAccessException {
test1();
}
public static void test1() {
A a = new A();
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSerMs());
synchronized (a) {
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSerMs());
}
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSerMs());
}

输出如下:

1
2
3
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 (biasable; age: 0)
00000000 00000000 00000001 11111011 01001010 10110100 01100000 00000101 (biased: 0x000000007ed2ad18; epoch: 0; age: 0)
00000000 00000000 00000001 11111011 01001010 10110100 01100000 00000101 (biased: 0x000000007ed2ad18; epoch: 0; age: 0)

image-20231031185810713

方法二

上述说到我写了两个方法,一个是打印得比较全得,一个是简约的,这里做个对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void test2() throws NoSuchFieldException, IllegalAccessException, InterruptedException {
A a = new A();
out.println("befor hash");
//没有计算HASHCODE之前的对象头
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSimplicity());
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSerMs());
//JVM 计算的hashcode
out.println("jvm‐‐‐‐‐‐‐‐‐‐‐‐0x" + Integer.toHexString(a.hashCode()));
//当计算完hashcode之后,我们可以查看对象头的信息变化
out.println("after hash");
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSimplicity());
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSerMs());
synchronized (a) {
out.println("对象a 已加锁 ---------");
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSimplicity());
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSerMs());
}
out.println("对象a 解锁 ---------");
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSimplicity());
out.println(ClassLayout.parseInstance(a).toPrintableSimpleSerMs());
}

打印结果:

  • toPrintableSimpleSimplicity()方法打印在上
  • toPrintableSimpleSerMs()方法打印在下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
befor hash
Mark Word Simplicity (binary):
101

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 (biasable; age: 0)
jvm‐‐‐‐‐‐‐‐‐‐‐‐0x573f2bb1
after hash
Mark Word Simplicity (binary):
101011100111111001010111011000100000001

00000000 00000000 00000000 01010111 00111111 00101011 10110001 00000001 (hash: 0x573f2bb1; age: 0)
对象a 已加锁 ---------
Mark Word Simplicity (binary):
1101110100101111111111111001000111000

00000000 00000000 00000000 00011011 10100101 11111111 11110010 00111000 (thin lock: 0x0000001ba5fff238)
对象a 解锁 ---------
Mark Word Simplicity (binary):
101011100111111001010111011000100000001

00000000 00000000 00000000 01010111 00111111 00101011 10110001 00000001 (hash: 0x573f2bb1; age: 0)

Process finished with exit code 0

image-20231031192001040

导出Jar包

Maven直接Install然后引入到项目中即可

image-20231031192247760