前言

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

<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中追加即可

 <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)、数组长度(如果对象是数组),字段信息以及空间损失等。

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、数组长度(如果对象是数组),字段信息以及空间损失等。

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

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 头中的最后三位,也就是锁和锁的状态

/**
     * 重写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

测试代码

方法一

调用新增方法

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());
}

输出如下:

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

方法二

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

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()方法打印在下

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然后引入到项目中即可