0%

我打算对 Java 网络的底层进行拆解,将 Java 网络操作与系统调用关联起来,探究那些在公众号上频繁出现的知识点的实际运作方式。通过 strace 虽能追踪到系统调用,然而却难以清晰辨别具体是由哪行 Java 代码发起的系统调用,这中间缺失了一层 Native 层。因此,我期望把调用过程中的 Native 方法全部打印出来。

原理解析

众所周知,Java Agent 能够用于实现字节码增强。但 Java Native 方法不存在字节码,所以无法直接进行字节码增强。
我们需要采取迂回的策略:

  1. 对 Native 方法进行重命名,并添加一个前缀。
  2. 构建一个与之前 Native 方法名称相同的普通方法。
  3. 借助字节码增强这个普通方法,再由该普通方法去调用 Native 方法。
    例如,假设原本的 Native 方法名为 intern ,我们可以将其重命名为 enhance_intern ,然后创建一个名为 intern 的普通方法,在字节码增强这个新创建的普通方法后,由它来调用 enhance_intern 。这种方式就巧妙地解决了 Native 方法无法直接字节码增强的问题。

举个具体🌰:
修改前方法:

1
public native String intern();

修改后:

1
2
3
4
5
public native String enhance_intern();

public String intern(){
enhance_intern();
}

实践细节

Native 方法名一旦更改,其与具体 Native 实现的映射关系便会遭到破坏,此时就需要借助 java.lang.instrument.Instrumentation#setNativeMethodPrefix 来重新构建映射关系。这个方法的作用在于,当 Native 方法无法被找到时,通过去除前缀再次尝试寻找。

然而,JVM 默认并不支持设置 Native 方法前缀,需要在 JavaAgent 的 manifest 中,设定 Can-Set-Native-Method-Prefix: true 。

看看代码

实现这么一个 Java Agent 只需要两个类, 那就直接上代码和注释了:

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
public class Premain {
// premain 方法,在 Java 代理的预初始化阶段被调用
public static void premain(String args, Instrumentation inst) {
// 调用 recoverMain 方法进行相关操作
recoverMain(inst);
}

// agentmain 方法,在 Java 代理的启动阶段被调用
public static void agentmain(String args, Instrumentation inst) {
// 调用 recoverMain 方法进行相关操作
recoverMain(inst);
}

// recoverMain 方法,用于执行恢复主流程的操作
public static void recoverMain(Instrumentation inst) {
// 输出 inst 是否支持本地方法前缀
System.out.println(inst.isNativeMethodPrefixSupported());
// 创建 NativeWrappingClassFileTransformer 类的对象
NativeWrappingClassFileTransformer transformer = new NativeWrappingClassFileTransformer();
// 向 inst 添加转换器,并设置为可以重转换
inst.addTransformer(transformer,true);
// 为 inst 设置本地方法前缀
inst.setNativeMethodPrefix(transformer, NativeWrappingClassFileTransformer.PREFIX);
}
}
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
138
139
140
141
142
143
144
145
146
class NativeWrappingClassFileTransformer implements ClassFileTransformer {

// 定义一个表示 BlockHound 运行时类型的 Type 对象
static final Type BLOCK_HOUND_RUNTIME_TYPE = Type.getType("Lreactor/blockhound/BlockHoundRuntime;");
// 定义本地方法的前缀字符串
static final String PREFIX = "$$BlockHound$$_";

// 构造函数
NativeWrappingClassFileTransformer() {
}

/**
* 实现 ClassFileTransformer 接口的 transform 方法
*
* @param loader 类加载器
* @param className 类名
* @param classBeingRedefined 正在被重定义的类
* @param protectionDomain 保护域
* @param classfileBuffer 类文件缓冲区
* @return 转换后的字节数组,如果不进行转换则返回 null
*/
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer
) {
// if (!className.startsWith("java/net/")) {
// return null;
// }

// 创建 ClassReader 对象来读取类文件缓冲区
ClassReader cr = new ClassReader(classfileBuffer);
// 创建 ClassWriter 对象用于写入转换后的类
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);

try {
// 接受 NativeWrappingClassVisitor 进行类的访问和转换
cr.accept(new NativeWrappingClassVisitor(cw, className), 0);

// 获取转换后的字节数组
classfileBuffer = cw.toByteArray();
} catch (Throwable e) {
// 打印异常堆栈跟踪
e.printStackTrace();
// 抛出异常
throw e;
}

// 返回转换后的类文件字节数组
return classfileBuffer;
}

// 内部静态类 NativeWrappingClassVisitor,继承自 ClassVisitor
static class NativeWrappingClassVisitor extends ClassVisitor {

// 存储类名
private final String className;

// 构造函数
NativeWrappingClassVisitor(ClassVisitor cw, String internalClassName) {
// 调用父类构造函数
super(ASM7, cw);
// 初始化类名
this.className = internalClassName;
}

/**
* 重写 visitMethod 方法,处理方法访问
*
* @param access 方法的访问标志
* @param name 方法名
* @param descriptor 方法描述符
* @param signature 方法签名
* @param exceptions 方法抛出的异常
* @return 方法访问器
*/
@Override
public MethodVisitor visitMethod(int access,
String name,
String descriptor,
String signature,
String[] exceptions) {
// 如果方法不是本地方法
if ((access & ACC_NATIVE) == 0) {
// 调用父类的 visitMethod 方法
return super.visitMethod(access, name, descriptor, signature, exceptions);
}

// 访问修改后的本地方法
super.visitMethod(
ACC_NATIVE | ACC_PRIVATE | ACC_FINAL | (access & ACC_STATIC),
PREFIX + name,
descriptor,
signature,
exceptions
);

// 访问原始的非本地方法
MethodVisitor delegatingMethodVisitor = super.visitMethod(
access & ~ACC_NATIVE, name, descriptor, signature, exceptions);
delegatingMethodVisitor.visitCode();

// 返回自定义的方法访问器
return new MethodVisitor(ASM7, delegatingMethodVisitor) {

@Override
public void visitEnd() {

// 添加打印语句打印方法名
visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
visitLdcInsn("Method called: " + className + "." + name);
visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);


Type returnType = Type.getReturnType(descriptor);
Type[] argumentTypes = Type.getArgumentTypes(descriptor);
boolean isStatic = (access & ACC_STATIC)!= 0;
if (!isStatic) {
visitVarInsn(ALOAD, 0);
}
int index = isStatic? 0 : 1;
for (Type argumentType : argumentTypes) {
visitVarInsn(argumentType.getOpcode(ILOAD), index);
index += argumentType.getSize();
}

visitMethodInsn(
isStatic? INVOKESTATIC : INVOKESPECIAL,
className,
PREFIX + name,
descriptor,
false
);

visitInsn(returnType.getOpcode(IRETURN));
visitMaxs(0, 0);
super.visitEnd();
}
};
}

}
}

背景

最近在调研跨线程 trace, 调研了跨线程 skywalking-java (skywalking 的 java 客户端) 的跨线程 trace 实现.

本文的内容包括:

  1. skywalking-java api 的简单使用
  2. skywalking-java api 重要概念及对应的实现
  3. skywalking-java 跨线程 trace 是如何实现的
  4. skywalking 的瓶颈以及我遇到的问题.

前置知识:

Skywalking-java api 的简单使用

普通用户API

官方文档中 Tracing APIs 一节中有用户API详细的说明. 简单来说,加一个 @Tracer 注解就能完成一般用户的需求:

1
2
3
4
5
@Trace
public User methodYouWantToTrace(String param1, String param2) {
// ActiveSpan.setOperationName("Customize your own operation name, if this is an entry span, this would be an endpoint name");
// ...
}

中间件用户 API

对于普通用户来说,上面这个接口已经够用了, 但对于中间件用户来说, 要需要深入看下非切面的API:

Span 分类

Skywlking 中将 Span 分为三类 :

  1. EntrySpan: 代表一个服务提供者.
  2. LocalSpan: 代表一个普通方法.注意,它既不代表一个服务提供者, 也不代表服务消费者.
  3. ExitSpan: 代表一份服务消费者

创建上面三个 Span 的方法分别为 ContextManager.java 中的:

1
2
3
public static AbstractSpan createEntrySpan(String endpointName, ContextCarrier carrier)
public static AbstractSpan createLocalSpan(String endpointName)
public static AbstractSpan createExitSpan(String endpointName, ContextCarrier carrier, String remotePeer)

上文中 @Tracer 注解生成的就是 LocalSpan

跨应用传递

APM 需要将不同应用间的 Span 串联起来, 通常来说是在请求中加上 APM 相关的信息来实现的.
Skywalking 的实现直接看官方文档 ContextCarrier

Span 的操作

可以通过 AbstractSpan 接口 来操作 Span ,向其中添加信息. 可以通过 Async Span APIs 来操控异步 Span.

举个例子

SkyWalking-java 使用 java agent 在运行时动态修改字节码来做到埋点的注入. 不同的中间件需要实现一个 SkyWalking 插件才能接入. 这部分内容可以参考 Byte Buddy Agent 初探–以 SkyWalking-java 为例 中 SkyWalking-java 简要分析 一节.

客户端埋点代码包含如下逻辑:

  1. 获取请求信息
  2. 构造 ExitSpan
  3. 向请求中注入 SkyWalking header

服务端包含如下逻辑:

  1. 获取请求信息
  2. 构造 EntrySpan
  3. 向请求中的 SkyWalking header 提取出来, 注入到本地的 ContextCarrier

可以参考这两个文件: SOFA RPC 客户端埋点SOFA RPC 服务端埋点

skywalking-java api 重要概念及对应的实现

Trace 相关概念

Trace Data Protocol v3 描述了SkyWalking 客户端和服务端之间的通信协议. 其中将链路追踪划分为三个层次:

  1. Trace: 代表整个链路
  2. Segment: 代表一个进程(应用)中所有的 Span 集合
  3. Span: 代表一个操作,比如读取一次DB

分别看下这三个对象的唯一标识是怎么产生的, 就能比较好理解他们分别代表什么维度:

Trace Id

Trace Id 通过 GlobalIdGenerator.generate() 产生,包含三个部分:

  1. PROCESS_ID: 代表应用程序实例 ID
  2. Thread.currentThread().getId(): 代表线程 ID
  3. THREAD_ID_SEQUENCE: 包含两个部分 1) 以毫秒记的时间戳 2) 线程级别的 0 到 9999 的序号

可以看出, Trace Id 是 实例维度+线程维度+毫秒维度+序号维度 组成的.

注意: Trace Id 是会通过请求传递到上游服务的,如果下游传递了 Trace Id 给上游, 上游会继续使用这个 Trace Id

Segment Id

Segment Id 的生成算法和 Trace Id 是一样的.

Span Id

SpanId 是一个在 TracingContext 中维护的,从 0 开始的序列.

注意: 这里的 TracingContext 是一个 ThreadLocal 变量, 生命周期和一个 Segment 想通.

实现

一些类和数据结构

  1. Span: 保存了 spanId,parentSpanId,tags 等标签, 用来表示一个操作.
  2. Segment: 保存了 traceSegmentId,relatedGlobalTraceId, Span 数组 等信息, 用来表示一组同线程同 traceId 的 Span 集合
  3. TracingContext: 用来操作 Segment 和 Span 的工具类,持有 TraceSegment 引用, 维护了一个 List<Span> 表示的栈. TracingContext 和单个线程对应, 单个TracingContext中只保存该线程对应的 Segment 和 Span.
  4. ContextManager: TracingContext 的控制类, 持有 TracingContext 的 ThreadLocal 引用. ContextManager 中的静态方法会

这是一个比较简单模式: 用户使用 ContextManager 来控制 TracingContext. TracingContext 来生成 SegmentSpan 对象, 并将他们组合到一起.

跨线程 Trace 的实现

跨线程Trace 分为两种情况:

  1. 单个 Span 跨线程. 也就是说, 同一个Span 的 startstop 操作在不同的线程中
  2. 整体 Trace 跨线程. 也就是说”父子Span”在不同的线程中.

我们分别讨论这两种情况.

单 Span 跨线程

单 Span 跨线程是指同一个Span 的 startstop 操作在不同的线程中. 直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
AbstractSpan mySpan = ContextManager.createEntrySpan("mySpan");
Runnable runnable = () -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 3. Propagate the span to any other thread.
// 4. Once the above steps are all set, call #asyncFinish in any thread.
mySpan.asyncFinish();


};
// 1. Call #prepareForAsync in the original context.
mySpan.prepareForAsync();
new Thread(runnable).start();
// 2. Run ContextManager#stopSpan in the original context when your job in the current thread is complete.
ContextManager.stopSpan();
}
}

上面这段代码, 在Main线程中创建了了一个 Span ,但在子线程中才结束这个 Span. 整个过程如下:

原理

整个单 Span 跨线程的过程分为四个阶段,我们分别分析:

在原始上下文中调用 prepareForAsync 方法

prepareForAsync 方法是 Span 的方法. 它会标记这个 Span 处于异步模式, 同时在方法内部调用 ContextManager#awaitFinishAsync 方法. 上下文中会通过asyncSpanCounter字段记录当前上下文有多少 Span 处于异步模式中.

在原始方法中调用 stopSpan 方法

由于上一步调用了 prepareForAsync, stopSpan 方法不会直接结束,而是判断当前上下文中asyncSpanCounter是否为0.为0的话结束,非0的话不结束.

将 Span 对象传递到其他线程中

可以使用闭包传递,这很好理解,不做说明了.

在子线程中嗲用 asyncFinish

调用 asyncFinish 会通知该 Span 的上下文, 减少asyncSpanCounter数量.并且再次尝试结束Span,如果此时asyncSpanCounter为0,就结束Span,否则不结束.

总结

在单个 Span 跨线程的场景下, 跨线程的 Span 还是原来的对象, 它持有原来线程 Context 的引用, traceId 等相关信息都没有发生改变.

整体 Trace 跨线程

整体 Trace 跨线程是说”父子Span”在不同的线程中.我们直接看代码:

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
public class Main2 {

public static void main(String[] args) {
AbstractSpan mySpan = ContextManager.createExitSpan("parent", "childThread");

Runnable runnable = () -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
new Thread(new RunnableWrapper(runnable)).start();
ContextManager.stopSpan(mySpan);
}

static class RunnableWrapper implements Runnable, EnhancedInstance {

private Object skyWalkingDynamicField;
private Runnable runnable;

public RunnableWrapper(Runnable runnable) {
if (ContextManager.isActive()) {
setSkyWalkingDynamicField(ContextManager.capture());
}
this.runnable = runnable;
}

@Override
public void run() {
AbstractSpan span = ContextManager.createLocalSpan("runnable.run");
span.setComponent(ComponentsDefine.JDK_THREADING);

final Object storedField = getSkyWalkingDynamicField();
if (storedField != null) {
final ContextSnapshot contextSnapshot = (ContextSnapshot) storedField;
ContextManager.continued(contextSnapshot);
}
try {
runnable.run();
} finally {
ContextManager.stopSpan();
}
}

@Override
public Object getSkyWalkingDynamicField() {
return skyWalkingDynamicField;
}

@Override
public void setSkyWalkingDynamicField(Object value) {
this.skyWalkingDynamicField = value;
}
}

}

这段代码是将RunnableInstrumentation 的代码增强逻辑展开后得来的(有简化).整体 Trace 跨线程主有两个主要步骤:

  1. 创建 RunnableWrapper 对象的时候, 在构造函数中,将当前线程的 ContextSnapshot 设置到 RunnableWrapper 的 skyWalkingDynamicField对象中.
  2. 在子线程执行 run 方法时, 创建一个新的 Span, 并将父线程的 contextSnapshot 当做参数传递给 continued 方法.

ContextSnapshot 的构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public ContextSnapshot capture() {
ContextSnapshot snapshot = new ContextSnapshot(
segment.getTraceSegmentId(), // segmentId
activeSpan().getSpanId(), // spanId
getPrimaryTraceId(), // traceId
primaryEndpoint.getName(), // parentEndpoint
this.correlationContext,
this.extensionContext
);
return snapshot;
}

continued 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Continue the context from the given snapshot of parent thread.
*
* @param snapshot from {@link #capture()} in the parent thread. Ref to {@link AbstractTracerContext#continued(ContextSnapshot)}
*/
@Override
public void continued(ContextSnapshot snapshot) {
if (snapshot.isValid()) {
TraceSegmentRef segmentRef = new TraceSegmentRef(snapshot);
this.segment.ref(segmentRef);
this.activeSpan().ref(segmentRef);
this.segment.relatedGlobalTrace(snapshot.getTraceId());
this.correlationContext.continued(snapshot);
this.extensionContext.continued(snapshot);
this.extensionContext.handle(this.activeSpan());
}
}

continued 方法通过 ContextSnapshot 构造 TraceSegmentRef,让当前线程的 Segment 和 Span 引用到 TraceSegmentRef, 从而建立起了两个线程的联系. 当这两个线程的 Segment 都完成之后, 就会被发送到 SkyWalking 服务端, 服务端可以根据这两个 Segment 之间的关系建立起联系.

我遇到的问题

我再做一个精细化耗时分析的程序,它可以实现跨线程的精细化耗时分析. 举个RPC的例子: 它会记录 IO 线程中, 请求到达的时间/IO 线程处理完成请求的时间; 然后将这些数据传递给业务线程, 业务线程继续记录 RPC 内部处理时间, 业务耗时等.

现在遇到的问题是: 在多层线程池嵌套的场景, 如何确定这个 Trace 的结束时间?

想象这么一个场景:
调用一个 RPC 接口, 这个 RPC 接口会再另外开启 5 条线程执行批量数据库操作,然后不等待数据库操作返会结果, RPC 提前返会.

在这种场景下,供涉及七条线程: IO 线程, RPC 业务线程, 批量操作5条线程.这七条线程只知道自己的 Span 状态, 无法得知其他 Span 的状态, 也就没办法知道整个事务什么时候结束(本线程事务结束的时候, 并不清楚其他线程的事务是否结束).

对于 SkyWalking, 它没有纠结整体事务有没有结束. 每个线程只关心自己的 Segment 是否结束, 结束就上报给 SkyWalking 服务端. 在上面这个例子中, 这七条线程的 Segment 是有关系的, skyWalking 可以根据这些关系, 再将他们组合起来, 统一展示.

参考文档

https://opentracing.io/specification/

Alibaba transmittable-thread-local 是什么

TransmittableThreadLocal(TTL):在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

相关文章

网上有很多 TTL 的原理分析/实践. TTL 作者在 这里 做了一个汇总. 我这篇文章就不深入细节展开了, 只写一点我自己对 TTL 笼统的理解.

分析

InheritableThreadLocal 原理

在创建 Thread 对象时 ,会复制复父线程的 InheritableThreadLocal 到新的 Thread 对象, 通过这种方式父线程的 ThreadLocal 传递给子线程. 这种方式有如下问题:

  • 线程池场景下,无法做到 ThreadLocal 的传递
  • 传递的参数是通用引用传递, 无法做到值传递.

TransmittableThreadLocal 原理

TransmittableThreadLocal 是在创建 Runnable 的时候做的 ThreadLocal 传递:

  1. 在创建 Runnable 的时候, 将 TransmittableThreadLocal 存储到一个对象属性中,此时所有逻辑在父线程中执行.
  2. 在执行 Runnable.run 时, 将上文对象中的 TransmittableThreadLocal 取出, 再放到 ThreadLocal 应该在的地方, 这个步骤的逻辑在子线程中执行.

另外TransmittableThreadLocal 还留有 TtlCopier 接口, 可以自定义实现值传递.

简单讲讲 TTL 就这么一回事,当然其中还有很多细节需要注意, 有需要的同学可以查看官方文档及官方文档汇总做进一步了解.

参考文档

transmittable-thread-local repo

背景

byte-buddy-agent 是 ByteBuddy 的一个组件, 用于快速构建出一个 JavaAgent. SkyWalking-java 是一个 SkyWalking 为 Java 准备的运行时代码生成器, 适配了众多中间件以提供定制的监控能力.

SkyWalking-java 本质是一个 java agent ,并在其中使用了 byte-buddy-agent.

本文会介绍 SkyWalking-agent 如何使用 byte-buddy-agent 来做到代码增强的.

ByteBuddy 基础用法

在深入 SkyWalking-agent 和 byte-buddy-agent 之前, 我们先来看下 ByteBuddy 是如何在调用一个方法前后插入日志的.

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
class MemoryDatabase {
public List<String> load(String info) {
return Arrays.asList(info + ": foo", info + ": bar");
}
}

class LoggerInterceptor {
public static List<String> log(@SuperCall Callable<List<String>> zuper) // SuperCall 注解用于表示 zuper 是原始方法的调用.
throws Exception {
System.out.println("Calling database");
try {
return zuper.call();
} finally {
System.out.println("Returned from database");
}
}
}

MemoryDatabase loggingDatabase = new ByteBuddy()
.subclass(MemoryDatabase.class) // 被增强的类继承于 MemoryDatabase 类
.method(named("load")) // 被增强的方法名为 load
.intercept(MethodDelegation.to(LoggerInterceptor.class)) // 增强方式是将方法调用代理到 LoggerInterceptor
.make() // 构造出上述配置产生的类的字节码
.load(getClass().getClassLoader()) // 将上述配置产生的类用过 getClass().getClassLoader() 加载到 jvm 中
.getLoaded() // 获取已经加载的 Class 类对象
.newInstance(); // 通过默认构造函数构造新的对象.

上面是一个简单的 ByteBuddy 例子. 它增强了 MemoryDatabase 类, 会在调用 MemoryDatabase.load 前后,打印输出一些信息.
ByteBuddy API 每行代码的含义都已经写在注释中了. 可以想象, 在SkyWalking agent 中, 也是构造了类似的代码增强, 在调用前后加入了 SkyWalking 埋点.

SkyWalking-java 简要分析

我们会先分析中间件如何接入 SkyWalking-java , 然后分析 SkyWalking-java 如何接入 byte-buddy-agent.

插件定义

SkyWalking-java 提供了一种比较简单的 API ,来供中间件接入. 我们以 SOFA RPC 为例子看下.

一个埋点的接入代码只有两个类,以及一个配置文件,直接上代码:

SofaRpcConsumerInstrumentation.java:

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
public class SofaRpcConsumerInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {

private static final String ENHANCE_CLASS = "com.alipay.sofa.rpc.filter.ConsumerInvoker";
private static final String INTERCEPT_CLASS = "org.apache.skywalking.apm.plugin.sofarpc.SofaRpcConsumerInterceptor";

@Override
protected ClassMatch enhanceClass() {
return NameMatch.byName(ENHANCE_CLASS);
}

@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return null;
}

@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[] {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return named("invoke");
}

@Override
public String getMethodsInterceptor() {
return INTERCEPT_CLASS;
}

@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
}

SofaRpcConsumerInstrumentation 继承了 ClassInstanceMethodsEnhancePluginDefine ,定义了 SOFA RPC 插件如何工作,它通过接口指定了三个属性:

  1. enhanceClass: 需要增强哪个类, 这里是增强 com.alipay.sofa.rpc.filter.ConsumerInvoker
  2. getConstructorsInterceptPoints: 指定构造函数如何增强: 这个插件不需要构造函数增强
  3. getInstanceMethodsInterceptPoints: 指定方法如何增强, 这里返回了一个 InstanceMethodsInterceptPoint 数组, 数组中每个元素代表一个增强点

接着我们看下 InstanceMethodsInterceptPoint 是怎么定义的:

  • getMethodsMatcher: 指定了如何找到需要增强的方法, 这里指定的是名为 invoke 的方法
  • getMethodsInterceptor: 指定如何增强刚刚指定的invoke方法, 这里指定的是 org.apache.skywalking.apm.plugin.sofarpc.SofaRpcConsumerInterceptor类, 这个类下文会分析.
  • isOverrideArgs: 这个属性指定了是否需要修改修改 invoke 的入参,这个例子中我们不需要修改,所以返回 false

SofaRpcConsumerInterceptor.java:

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 class SofaRpcConsumerInterceptor implements InstanceMethodsAroundInterceptor {

public static final String SKYWALKING_PREFIX = "skywalking.";

@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
// 省略一些代码
final ContextCarrier contextCarrier = new ContextCarrier();
final String operationName = generateOperationName(providerInfo, sofaRequest);
AbstractSpan span = ContextManager.createExitSpan(operationName, contextCarrier, host + ":" + port);
span.setComponent(ComponentsDefine.SOFARPC);
SpanLayer.asRPCFramework(span);
}

@Override
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) throws Throwable {
SofaResponse result = (SofaResponse) ret;
if (result != null && result.isError()) {
dealException((Throwable) result.getAppResponse());
}

ContextManager.stopSpan();
return ret;
}

@Override
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
Class<?>[] argumentsTypes, Throwable t) {
dealException(t);
}
}

SofaRpcConsumerInterceptor 会重载三个方法, 分别在 com.alipay.sofa.rpc.filter.ConsumerInvoker#invoke() 调用之前,之后,以及发生异常时被回调.

配置文件为:
skywalking-plugin.def

1
sofarpc=org.apache.skywalking.apm.plugin.sofarpc.SofaRpcConsumerInstrumentation

这里面配置了 SofaRpcConsumerInstrumentation 类.

适配 ByteBuddy

通过上文的配文件,SkyWalking-java 可以获取到所有的 PluginDefine 类. SkyWalking-java 会加载这些类, 并将他们统一交给 PluginFinder 管理.

PluginFinderbuildMatch() 方法生成一个 ElementMatcher 来匹配到所有需要被加载的类型. 以 SOFA RPC 为例就是 com.alipay.sofa.rpc.filter.ConsumerInvoker 类.

PluginFinder 会被传递给 org.apache.skywalking.apm.agent.SkyWalkingAgent.Transformer 来构造一个 AgentBuilder.Transformer. 可以被上面 ElementMatcher 匹配的类,都会被 Transformer 增强.

增强逻辑如下:

  • 处理每个匹配 ElementMatcher 的类
  • 通过 PluginFinder 找到这个类对应的 PluginDefine
  • 调用 PluginDefinedefine 方法对方法进行增强:
    • 获取 PluginDefine 定义的 InstanceMethodsInterceptPoints
    • 遍历 InstanceMethodsInterceptPoints, 通过 MethodsMatcher 确定需要增强的方法, 通过 getMethodsInterceptor 获取如何增强方法. 并将 Interceptor 注入到 InstMethodsInter 中.
    • MethodsMatcherInstMethodsInter 传递给 ByteBuddy 就完成了 JavaAgent 的配置.

下面我们看下 InstMethodsInter 是什么:

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
public class InstMethodsInter {
private static final ILog LOGGER = LogManager.getLogger(InstMethodsInter.class);

/**
* An {@link InstanceMethodsAroundInterceptor} This name should only stay in {@link String}, the real {@link Class}
* type will trigger classloader failure. If you want to know more, please check on books about Classloader or
* Classloader appointment mechanism.
*/
private InstanceMethodsAroundInterceptor interceptor;

/**
* @param instanceMethodsAroundInterceptorClassName class full name.
*/
public InstMethodsInter(String instanceMethodsAroundInterceptorClassName, ClassLoader classLoader) {
try {
interceptor = InterceptorInstanceLoader.load(instanceMethodsAroundInterceptorClassName, classLoader);
} catch (Throwable t) {
throw new PluginException("Can't create InstanceMethodsAroundInterceptor.", t);
}
}

/**
* Intercept the target instance method.
*
* @param obj target class instance.
* @param allArguments all method arguments
* @param method method description.
* @param zuper the origin call ref.
* @return the return value of target instance method.
* @throws Exception only throw exception because of zuper.call() or unexpected exception in sky-walking ( This is a
* bug, if anything triggers this condition ).
*/
@RuntimeType
public Object intercept(@This Object obj, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper,
@Origin Method method) throws Throwable {
EnhancedInstance targetObject = (EnhancedInstance) obj;

MethodInterceptResult result = new MethodInterceptResult();
try {
interceptor.beforeMethod(targetObject, method, allArguments, method.getParameterTypes(), result);
} catch (Throwable t) {
LOGGER.error(t, "class[{}] before method[{}] intercept failure", obj.getClass(), method.getName());
}

Object ret = null;
try {
if (!result.isContinue()) {
ret = result._ret();
} else {
ret = zuper.call();
}
} catch (Throwable t) {
try {
interceptor.handleMethodException(targetObject, method, allArguments, method.getParameterTypes(), t);
} catch (Throwable t2) {
LOGGER.error(t2, "class[{}] handle method[{}] exception failure", obj.getClass(), method.getName());
}
throw t;
} finally {
try {
ret = interceptor.afterMethod(targetObject, method, allArguments, method.getParameterTypes(), ret);
} catch (Throwable t) {
LOGGER.error(t, "class[{}] after method[{}] intercept failure", obj.getClass(), method.getName());
}
}
return ret;
}
}

InstMethodsInter 与上文 ByteBuddy 基础用法 中的 LoggerInterceptor 是同一类型的东西, ByteBuddy 会将需要增强的类代理给 intercept 方法. 可以看 intercept 方法的参数上都带上了 ByteBuddy 的注解:

  • This: 代表当前代理类的实例
  • AllArguments: 原始调用的参数列表
  • SuperCall: 代表原始调用
  • Origin: 代表原始方法

通过 InstMethodsInter , SkyWalking-java 在需要增强的方法前后及异常处理阶段增加了一些回调, 回调给 PluginDefine 中定义的 Interceptor.

ByteBuddy Agent 原理简介

在开始社介绍 ByteBuddy 原理之前, 可以先看下 Java Agent 原理简介 来回顾下原生 javaagent 是怎么工作的:

我们向 Instrumentation 注册一些 Transformer,在类加载期间 Transformertransform 方法就会被调用. transform 方法会修改类的二进制表示(字节码), 从而实现类增强.

那么 ByteBuddy 是如何适配原生 javaagent 这种模式的呢?

ByteBuddy 内部有一个适配器 net.bytebuddy.agent.builder.AgentBuilder.Default.ExecutingTransformer 将原生 transform 方法适配到 net.bytebuddy.agent.builder.AgentBuilder.Transformer 的方法上. 然后通过 net.bytebuddy.dynamic.DynamicType.Builder 构造新的类的字节码, 返回给 Instrumentation.

例子

目标

我们有一个类 EchoA, 它有一个方法 echo 返回值是一个字符串 "A":

1
2
3
4
5
public class EchoA {
public String echo() {
return "A";
}
}

我们想要通过 ByteBuddy 来动态生成一个类, 它的 echo 方法返回字符串 "B".

ByteBuddy 实现

我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
// 使用 ByteBuddy 开始构造一个类
DynamicType.Unloaded<EchoA> newClass = new ByteBuddy()
.subclass(EchoA.class) // 新的类继承于 EchoA
.method(ElementMatchers.named("echo")) // 对方法 echo 做增强
.intercept(FixedValue.value("B")) // 增强内容是返回一个固定值 "B"
.make(); // 构造这个 class

Class<?> dynamicType = newClass
.load(EchoA.class.getClassLoader()) // 使用 EchoA 的类加载器加载这个类
.getLoaded(); // 获取被加载的类
EchoA echoA = (EchoA) dynamicType.getDeclaredConstructor().newInstance(); // 使用反射调用构造函数, 构造新的类的实例
System.out.println(echoA.getClass().getName()); // 打印新类类名
System.out.println(echoA.echo()); // 调用 echo 方法并打印结果
}

上面这个 main 方法动态生成了一个类, 并调用这个类的 echo 方法,返回了字符串 "B".

原理

字节码视角: 改变了什么

ByteBuddy 可以生成字节码,但是不会生成 Java 源码,我们就从字节码层面看看, ByteBuddy 做了什么.

先看下原始的 EchoA 字节码(部分):
EchoA

echo 方法的字节码如下:

1
2
0 ldc #8 <B>
2 areturn

ldc 指令会将常量池中的常量入栈, 这里是将常量池第八个常量(也就是"A") 入栈. areturn 指令将栈中数据作为返回值返回.

生成的新类, 也是类似的逻辑:
EchoB

源码视角: 怎么实现的

ByteBuddy 是一个对 ASM 字节码框架的封装. ByteBuddy 对字节码操作做了多个层次的封装, 用户可以很方便的使用它, 但要理解它的运行原理, 有比较陡峭的学习曲线. 下面我们管中窥豹:

1
2
3
4
5
DynamicType.Unloaded<EchoA> newClass = new ByteBuddy()
.subclass(EchoA.class) // 新的类继承于 EchoA
.method(ElementMatchers.named("echo")) // 对方法名为 echo 做增强
.intercept(FixedValue.value("B")) // 增强内容是返回一个固定值 "B"
.make(); // 构造这个 class

我们着重分析这段代码:subclass,method,intercept 这三个方法都是对 ByteBuddy 对象进行设置,没有进行具体字节码操作.

由于 BtyeBuddy 框架中用了很多不可变类, 这三个方法的返回值都是一个新的 ByteBuddy 实例.

最后的 make 方法将会进行字节码增强,产生新的类的字节码.我们具体看下 make 方法内部.

net.bytebuddy.dynamic.DynamicType.Builder.AbstractBase.UsingTypeWriter#make(net.bytebuddy.dynamic.TypeResolutionStrategy):

1
2
3
public DynamicType.Unloaded<U> make(TypeResolutionStrategy typeResolutionStrategy, TypePool typePool) {
return toTypeWriter(typePool).make(typeResolutionStrategy.resolve());
}

make 方法内部使用会构造一个 TypeWriter 去做具体的 make 操作.
构造 TypeWriter 的过程参考下面代码的注释

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
protected TypeWriter<T> toTypeWriter(TypePool typePool) {
MethodRegistry.Compiled methodRegistry = constructorStrategy // constructorStrategy 规定了如何构造新类的构造函数, ByteBuddy 遵循约定优于配置的原则, constructorStrategy 有一个默认值,这里我们不展开讨论.
.inject(instrumentedType, this.methodRegistry) // inject 将构造函数如何构造的 handler 插入 methodRegistry,并返回这个 methodRegistry
.prepare(applyConstructorStrategy(instrumentedType), // prepare 方法会会根据配置 (也就是上文 method 和 intercept 方法), 将改造 echo 方法的 handler 加入 methodRegistry.
// 具体操作是: 在需要被怎强的类 EchoA 中, 通过 过滤器(ElementMatchers.named("echo")) 找到需要被增强的
方法 echo , 然后将这个方法和这个方法如何被增强 (返回固定值 value FixedValue.value("B")) 插入到 methodRegistry
methodGraphCompiler,
typeValidation,
visibilityBridgeStrategy,
new InstrumentableMatcher(ignoredMethods))
.compile(SubclassImplementationTarget.Factory.SUPER_CLASS, classFileVersion); // compile 方法会将对应的 handler 对应的 ByteCodeAppender 和 MethodAttributeAppender 和方法对应起来,并缓存起来.

// 最后将准备好的对象组织成 TypeWriter 返回
return TypeWriter.Default.<T>forCreation(methodRegistry,
auxiliaryTypes,
fieldRegistry.compile(methodRegistry.getInstrumentedType()),
recordComponentRegistry.compile(methodRegistry.getInstrumentedType()),
typeAttributeAppender,
asmVisitorWrapper,
classFileVersion,
annotationValueFilterFactory,
annotationRetention,
auxiliaryTypeNamingStrategy,
implementationContextFactory,
typeValidation,
classWriterStrategy,
typePool);
}

构造完成 TypeWriter 之后, 通过 make 方法最终构造出新的类的字节码, make 方法调用net.bytebuddy.dynamic.scaffold.TypeWriter.Default.ForCreation#create create 方法, create 方法调用 ASM 框架的方法, 将上述准备好的 ByteCodeAppender 应用到被增强的类上.

附录

ByteBuddy 官方教程: 如果想要快速上手 ByteBuddy, 看这个官方教程最直接.
The Java® Virtual Machine Specification Java SE 11 Edition 想了解字节码可以看这个文档

问题

使用 git push 代码到 github. 直接卡住了, 过了很长时间报错. 执行 telent github.com 22无法连通.

解决方案及原因

解决方案

将 DNS 解析服务器配置为 8.8.8.8.

原因

好在自己有另一台机器, 可以连上 github. 比对两台机器, 可以发现, 出问题的机器 github.com 域名解析出来是一个贵州的IP: 182.43.124.6. 而没出问题的机器,解析出来的是新加坡的IP: 20.205.243.166.
后者 telnet 结果:

1
2
3
4
5
# telnet 20.205.243.166 22
Trying 20.205.243.166...
Connected to github.com.
Escape character is '^]'.
SSH-2.0-babeld-cd305013

可以看到 SSH 的返回, 这是能正常连接的.

出问题的机器没有指定 DNS 服务器, 没出问题的机器指定了 8.8.8.8.

背景

现在我的主力开发机器是台台式机, 有时候出门在外, 就没办法操控这台机器了. 我解决这个问题的基本思路是: 租用一台云上带公网域名的机器,然后将本地 22 端口映射到云服务器上. 这样就可以将云服务器作为一台跳板机,登录我本地的机器了.

目标

  • 将本地 22 端口映射到云上机器
  • 让本地机器开机运行脚本,自动完成上述工作

实现步骤

准备工作:购买一台云服务器

我买的的是一台2C4G,腾讯云服务器.趁双11+新人优惠,只要100元/年.需要获取这台机器的IP,设置 ssh 公钥,让本地机器可以通过shell免密登录云服务器.

本地机器配置

构造以下两个文件:
remote_ssh.sh:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

PORT='8888'
n=1
while true
do
echo "try $n"
ssh -o ExitOnForwardFailure=yes -R 8888:localhost:22 -N root@cloud_ip
n=$((n+1))
echo "fail $n"
sleep 15
done

这个脚本会将云服务器上的 8888 收到的请求转发给本地22端口, 也就是本地 sshd 监听的端口. 同时如果操作失败,就会15s后重试.
变量说明:

  • root: 登录云服务器的用户名,根据你的需要修改
  • cloud_ip: 云服务器的公网ip
  • -o ExitOnForwardFailure=yes: 设置如果端口转发开启失败就退出的选项

startup.sh:

1
2
3
4
5
#!/bin/bash

curday=`date +%Y-%m-%d`

nohup bash ~/git/shell/remote_ssh.sh 2>&1 >~/git/shell/logs/remote_ssh$curday.log &

这个脚本会执行 remote_ssh.sh 这个脚本,并将日志输出到~/git/shell/logs/文件夹中.

startup.sh的默认打开方式修改为 Term 或者 iTerm.

在系统设置->用户与群组将 startup.sh 添加到登录项.

云上机器配置

修改 /etc/ssh/sshd_config 文件, 添加如下配置:

1
2
ClientAliveInterval 10
ClientAliveCountMax 1

这两个配置会开启服务端 shhd 对于客户端的探活,如果客户端连接断开了, 服务端也会关闭连接.

遇到的坑

端口转发失败之后, ssh 不退出

端口转发失败之后, ssh 不退出,就没办法做重试. 在添加 -o ExitOnForwardFailure=yes 选项之后,修复了这问题.

ssh 断开之后, sshd 依然监听端口

ssh 断开之后, sshd 依然监听端口. 当 ssh 继续重试建立端口转发,因为服务端 sshd 依然占用之前那个端口,导致重试始终无法成功.
解决方案是给 sshd 开启探活,添加 ClientAliveInterval 和 ClientAliveCountMax 配置.

参考文档:

SSH教程: 阮一峰的SSH教程, 看完不需要多长时间,能对 SSH 有初步了解
how-can-a-remote-ssh-tunnel-port-be-closed-on-the-ssh-server-when-the-ssh-comman
man:sshd_config

Java Agent 是什么

Java Agent 是一种可以动态修改 Java 类字节码的技术.

使用 Java Agent 通常是为了动态增加应用的功能,但这些功能和主体业务逻辑是不耦合的, 比如说各种监控\诊断工具,甚至有公司拿它来实现 Service Mesh.
举两个例子:

  1. SkyWalking-java SkyWalking 社区推出的 Java Agent, 用于Java 应用快速接入 SkyWalking
  2. Arthas 是 Alibaba 推出的 Java 诊断工具.
  3. 阿里云 Spring Cloud 应用 Proxyless Mesh 模式探索与实践

上面两者均是基于 Java Agent 实现的.

使用 Java Agent 可以实现如下技术目标:

  1. 对源代码无侵入的情况下, 完成功能的添加: SkyWalking-java 主要利用这一特性, 当我们有很多系统都需要接入监控埋点时, 就不需要升级每一个应用了, 添加 Java Agent 即可.
  2. 运行时动态修改功能,实现热部署. Arthas 主要利用这一特性, Arths 的诊断功能是对性能有影响的,只能单独选择一两台服务器开启. 并且故障诊断需要故障现场,不能重启应用, 因此热部署特性对于 Arthas 来说就至关重要了.

Java Agent 工作原理简介

Java Agent 的启动

Java Agent 的启动有两种方式:

  1. 在启动 Java 应用时,在 JVM 参数中添加 -javaagent:/path/to/your_agent_name.jar.下文简称 JVM 参数方式.
  2. 在运行时,通过 com.sun.tools.attach.VirtualMachine#loadAgent(java.lang.String) 动态加载 Java Agent. 下文简称运行时方式.

这里具体说明下运行时方式的启动: 假设我们有一个 biz.jar 是我们的业务应用, agent.jar 是我们的 Java Agent. 我们可以在启动一个 attach.jar ,来将 agent.jar attach 到 biz.jar 上, 整个过程是不需要修改 biz.jar 的.

下面我们会从几个方面来比较这两种启动方式:

JVM 参数方式 运行时方式
启动时机 main函数执行之前 应用运行时
执行线程 main 线程 AttachListener 线程
入口函数 premain() agentmain()
ClassLoader AppClassLoader AppClassLoader

Java Agent 启动代码分析

在 JVM 接收到相关的参数之后, 就会调用 InstrumentationImpl 对象执行 agent 逻辑. JVM 方式会调用 InstrumentationImpl 的 loadClassAndCallPremain 方法, 运行时方式会调用 loadClassAndCallAgentmain 方法.这两个方法底层都是调用 loadClassAndStartAgent 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// WARNING: the native code knows the name & signature of this method
private void
loadClassAndCallPremain( String classname,
String optionsString)
throws Throwable {

loadClassAndStartAgent( classname, "premain", optionsString );
}


// WARNING: the native code knows the name & signature of this method
private void
loadClassAndCallAgentmain( String classname,
String optionsString)
throws Throwable {

loadClassAndStartAgent( classname, "agentmain", optionsString );
}

在 loadClassAndStartAgent 方法中, 首先获取 AppClassLoader ,然后通过 AppClassLoader 加载 AgentClass 类.最后通过反射调用 premain 或者 agentmain 方法.

因此我们可以得出一个结论: 除了上文表格中的不同点, JVM 方式 和 运行时方式启动的 Java Agent 没什么区别.

agentmain 和 premain 有一个参数是 Instrumentation , Instrumentationimpl 会将自身传递给 agentmain/premian 方法.

实现字节码修改 – Instrumentation 简介

Instrumentation 是 agentmain/premain 的入参, 通过这个类, 可以实现字节码修改.

Instrumentation 对于字节码的修改,可以分为两种形式:

  1. transform: 修改字节码,会在字节码里面掺私货.给类加切面用这种方式.
  2. redefine: 重新定义字节码, 推倒重来. 直接修改类的业务逻辑用这种方式.

redeine 是 java1.5 引入的, 而 transform 是 java 1.6 引入的. transform 的使用更方便,因此我推荐使用 transform 而不用使用 redeine.

transform 又分为两类:

  1. transform: 在类第一次加载时执行
  2. retransform: 调用 Instrumentation 的 retransformClasses 方法手动执行.

transform 方法

我们可以向 Instrumentation 中注册 Transformer, 当一个类被加载或者被调用 retransform 时, Transformer 的 transform 方法会被调用. transform 接收一个用来表示 classFile 的二进制数组, 同时返回一个新的表示类的二进制数组.

可以用 ASM ,ByteBuddy 等框架来读取,修改这个二进制数组并返回. 这样, 一次代码增强就完成了

背景

最近在看 JVM 源码,没有 debug 环境很不方便, 都不知道方法是怎么跳转的. 因此想要自己搭一个 debug 环境. 网上有很多相关的文章, 但毕竟不是一模一样环境, 一点小小的差异, 就可能让编译失败,浪费很多时间.

因此我选择通过 brew 的编译方式来, 稍微修改一下参数即可获得我们需要的 debug 版本 JVM.

环境

  • Mac Studio M1 Max
  • macOS Monterey 12.5.1

编译过程

尝试直接通过 brew 编译 JVM

执行命令

1
brew install --build-from-source --verbose openjdk@11

直接安装 jdk 11, 这个命令会从源码安装 openjdk11. 可以看下能不能编译成功. 如果这个命令编译不成功,后面的步骤页没必要尝试了; 如果编译成功了, 那我们就会比较有信心了.

这个命令会输出很多细节信息, 这很重要.

上述命令实际上执行的是 openjdk@11.rb, 通目录下还有 openjdk8,17,19 可供选择.

brew 编译 JVM 的过程

  1. 下载并解压源码到 /private/tmp/openjdk11xxx
  2. 下载并解压 boot-jvm 到 /private/tmp/openjdk11-boot-jdkxxx
  3. 根据 openjdk@11.rb 中的配置执行 configure 脚本
  4. 执行 make 命令:make images CONF=release
  5. 删除所有临时文件

为了能编译出可以 debug 的 JVM ,我们还需要做两件事情:

  1. 在配置 configure 脚本时,添加 debug 参数
  2. brew 在安装完成后会将源码删除, 我们需要将源码保留

手动编译 JVM

接下俩我们仿照brew 的编译方式手动编译 JVM:

  1. 解压源码
    brew 安装完成后会删除源码,但是不会删除下载下来的源码压缩包,我们可以手动解压这个压缩包,然后将源码放到我们自己的目录中.

相关的命令在 brew 编译 JVM 的输出中有,可以直接参考.

注意: 解压目标文件夹需要已经存在.

  1. 解压 boot jdk
    同上, 我们需要吧boot jdk 解压出来

  2. 修改 configure 参数执行 configure 脚本
    执行 configure 脚本的参数在brew 输出中也有,可以抄下来.我们需要对其中几个参数进行修改:

  • --with-boot-jdk 如果修改了 boot-jdk 的位置,需要将这个参数指向真确的位置
  • --with-debug-level 修改为fastdebug
  • --with-native-debug-symbols 的值修改为 internal, 表示会把符号表嵌入到二进制中,这是 debug 的关键

configure 命令的例子

1
./configure --disable-hotspot-gtest --disable-warnings-as-errors --with-boot-jdk-jvmargs=-Duser.home=/Users/zhangchengxi/Library/Caches/Homebrew/java_cache --with-boot-jdk=/private/tmp/openjdkA11-20221122-35955-58bnn5/jdk11u-jdk-11.0.16.1-ga/boot-jdk --with-debug-level=fastdebug --with-conf-name=release --with-jvm-variants=server --with-jvm-features=shenandoahgc --with-native-debug-symbols=none --with-vendor-bug-url=https://github.com/Homebrew/homebrew-core/issues --with-vendor-name=Homebrew --with-vendor-url=https://github.com/Homebrew/homebrew-core/issues --with-vendor-version-string=Homebrew --with-vendor-vm-bug-url=https://github.com/Homebrew/homebrew-core/issues --without-version-opt --without-version-pre --enable-dtrace --with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX12.sdk --with-extra-ldflags=-Wl,-rpath,@loader_path/server -headerpad_max_install_names

这里有个问题, brew 执行这个命令是可以成功的,但是我执行的时候, 必须把最后一个参数 headerpad_max_install_names 去掉才能成功.

  1. 执行 make 命令: make images CONF=release
    这个命令会在 release/images/jdk/ 下创建 JVM 的二进制

上述步骤完成之后, 我们就有了可以用于 debug 的 JVM 了.

通过 Vscode 启动 java 并 debug

我们通过 vscode 打开 jvm 源码之后, 可以在左边打开一个 debug 界面.在这个界面中有一个
create a launch.json file 的按钮, 我们点击它,就能创建一个 .vscode/launch.json 的文件.
最终这个文件会长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"version": "0.2.0",
"configurations": [
{
"name": "(lldb) 启动",
"type": "cppdbg",
"request": "launch",
"program": "/your/path/to/jdk/source/build/release/images/jdk/bin/java",
"args": [
"-Dfile.encoding=UTF-8","-Djava.compiler=NONE",
"-classpath",
"/dir/demo/target/classes",
"org.example.Main"],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "lldb",
"targetArchitecture": "arm64"
}
]
}

这里面有两个参数需要修改:

  • “program”: 代表你编译生成的 java 命令的路径
  • “args”: 代表 java 命令的参数

小tips : args 参数可以在 JAVA IDE 的输出中找到, 但需要删除其中有关 IDE 的 aggent 参数.

参考:Customize debugging with launch.json

配置完成之后, 就能开始 debug 了.

参考文档

Building the JDK

背景

几年前就搭建了自己的 Github Page, 但当时搭建的时候, 博客文章没有上传到 Github, 只上传了生成的静态文件,而且没有自动部署的能力.准备重新提笔写博客,就重新搭一套.

目标

这次搭博客有如下技术目标:

  1. 搭建完成之后,只依赖 文本编辑+git 就能完成写作, 不需要其他依赖(尤其是node依赖)
  2. 文本内容上传 Github 之后自动部署.
  3. 任何其他人能够方便复刻这一套方案.

快速复刻本项目

创建站点

如果你也想搭建一套博客,而不想折腾环境,可以直接 Fork 本项目,然后改名为 username.github.io ,username 是你在 GitHub 上的使用者名称.
具体步骤为:

  1. Fork 本项目,fork 时可以将 orezzero.github.io 改为 username.github.io ,username 是你在 GitHub 上的使用者名称.
  2. 进入你的仓库, 进入 Action 页面, 确认 Action 功能已经被启用
  3. 删除 source/_posts (这里面是我的文章) 除了 template.md 的文件,删除 sources/images/中的图片, 修改 source/about/index.md 然后提交代码
  4. 等待 Action 完成 (第一次跑pages build and deployment会失败,别管他)
  5. 在你的仓库中进入 Settings > Pages > Source,并将 branch 改为 gh-pages。(在第一次提交代码之前,这个配置无法修改)
  6. 手动触发 Action 中的 Pages action 或者提交一个修改触发 Action,等待 Action 完成
  7. 访问 username.github.io 就能看到你的站点了

https://easyit-blog.github.io/ 就是这么创建出来的.

本项目有一些区别于原生 Next 主题的定制化配置, 可以参考 PR Modify theme settings #1 来自定义你的主题.

写作

source/_posts 文件夹中创建 markdown 文件. Hexo 对于 md 的格式有一定要求,需要用这种格式开头:

1
2
3
4
5
---
title: 创建一个自己的博客
date: 2022-11-23 06:55:58
tags:
---

这个头部后面就可以书写正文了.

技术细节

如何从零开始搭建 Hexo

我使用了 Docker 来初始化项目. 首先我通过 Dockerfile 创建了一个 Hexo 镜像, 启动这个镜像就会构建 Hexo 的基本环境到 /blog 目录下. 然后我将容器中的 /blog 目录拷贝出来. 基本环境就搭建好了.

1
2
# 构建 docker 镜像
docker build -t hexo .
1
2
# 启动 docker 容器
docker run --name hexo -d -P hexo
1
2
## 拷贝
docker cp hexo:/blog ./

本地启动 Hexo Server

按上节构造好镜像之后, 使用 docker-compose 配置文件启动 docker-compose 即可.

需要将配置文件放在 hexo 目录, 并在这个目录执行 docker compose 命令.

1
2
# 启动 docker compose
docker-compose up -d
1
2
# 关闭 docker compose
docker-compose down

自动部署

自动部署完全照搬了官网的 在 GitHub Pages 上部署 Hexo 十分简单.

自动部署在网上有很多教程, 但是这些教程都没有官网的简单可靠.

遇到的坑

  1. 主题文件夹themes/next没能成功上传到 github 上,导致编译不出静态页面.

参考文档

Next 主题文档站: 这个网站比较重要,很多配置可以参考这里的文档.
Hexo 官网
在 GitHub Pages 上部署 Hexo
建站