ASM 字节码插桩 :线程治理

2,613 阅读10分钟

1.面临的挑战

对于开发者来说,线程治理一直是比较棘手的问题。主要有以下两个问题

  • 大量的匿名线程。new Thread 的方式虽然可以实现快速、优先级最高的异步化,然而过多的匿名线程对于问题排查难度、稳定性都是一种挑战
  • 空闲线程得不到释放。经过业务的快速迭代,项目中存在多处即使在空闲的时候,线程池中的线程一直在 waiting

2.优化思路

Android 中创建线程的方式主要有以下几种

  • New Thread,也是最常用的创建线程的方式
  • New Timer ,定时器
  • 创建线程池,包括工具类 Executors 提供的方法和使用 TheadPoolExecutor 创建自定义线程池

利用 AOP 思想,我们可以将创建线程及线程池的字节码指令在编译期替换成自定义的方法调用。

  • 线程重命名:为线程名加上调用者的类名前缀,当 APM 工具上报异常信息或对线程进行采样时,采集到的线程信息对于排查问题会十分有帮助
  • 线程池:对常用的 ThreadPoolExecutor,Executors 工具类提供的 newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool 等常见的5~6种线程池创建方式进行了 hook,增加线程池的核心线程可释放缩短 idle threads 超时时间

3.代码实现

3.1 线程重命名

首先来看最常用的创建匿名线程的方式及对应的字节码

3.1.1 直接创建线程

java
new Thread(runnable, "thread_season").start();

字节码
// 0-创建一个对象, 并将其引用引用值压入栈顶
methodVisitor.visitTypeInsn(NEW, "java/lang/Thread");
// 1-复制栈顶数值并将复制值压入栈顶
methodVisitor.visitInsn(DUP);
// 2-将指定的引用类型本地变量推送至栈顶 - 也就是 runnable
methodVisitor.visitVarInsn(ALOAD, 1);
// 3-将int,float或String型常量值从常量池中推送至栈顶 - 也就是 ‘thread_season’
methodVisitor.visitLdcInsn("thread_season");
// 4-调用实例初始化方法
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;Ljava/lang/String;)V", false);
// 5-调用实例方法
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);

如果想要实现拼接上线程所属的外部类名,可以在 4 处插入

methodVisitor.visitLdcInsn('className_prefix')

,同时需要为 4 处字节码指令的 descpritor 添加一个 String 类型的参数

methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;Ljava/lang/String;Ljava/lang/String;)V", false);

但是经过上述修改之后,并没有对应的线程构造函数,怎么办呢?可以看到无论通过何种构造函数创建线程,0 处的字节码指令都是相同的,所以我们可以将 java/lang/Thread 其替换成优化后的线程对应的类名

methodVisitor.visitTypeInsn(NEW, "com/zhangyue/ireader/optimizeThreadProxy/ShadowThread");

ShadowThread 内部会代理所有的线程构造函数并添加一个类名。这样当创建匿名线程时,实际上创建的是经过优化后的线程。

screenshot-20221113-123453.png 这里把通过 ASM 操作字节码的核心代码也贴出来

//遍历字节码,找到创建对象的 visitTypeInsn 指令后
/**
@param type - 优化线程类的类名全称
*/
static def transformNewInner(ClassNode cn, MethodNode methodNode, TypeInsnNode insnNode, String type) {
    def insnList = methodNode.instructions
    int index = insnList.indexOf(insnNode)
    def typeNodeDesc = insnNode.desc
    //向后遍历,寻找 <init> 方法
    for (int i = index + 1; i < insnList.size(); i++) {
        AbstractInsnNode node = insnList.get(i)
        if (
        node instanceof MethodInsnNode
                && node.opcode == Opcodes.INVOKESPECIAL
                && node.owner == typeNodeDesc
                && node.name == "<init>"
        ) {
            // java/lang/Thread -> com/zhangyue/ireader/optimizeThreadProxy/ShadowThread
            insnNode.desc = type
            node.owner = type
            //向 descriptor 中添加 String.class 入参
            node.desc = insertArgument(node.desc, String.class
            // <init> 方法前插入 ldc 指令
            insnList.insertBefore(node, new LdcInsnNode(cn.name))
            //找到一个就 break
            break
        }
    }
}

/**
 * 在描述符末尾添加文件描述符
 * @param descriptor
 * @param clazz
 * @return
 */
static String insertArgument(descriptor, Class<?> clazz) {
    def type = Type.getMethodType(descriptor)
    //返回值类型
    def returnType = type.getReturnType()
    //参数数组
    def argumentTypes = type.getArgumentTypes()
    //构造新的参数数组
    def newArgumentTypes = new Type[argumentTypes.length + 1]
    System.arraycopy(argumentTypes, 0, newArgumentTypes, 0, argumentTypes.length)
    newArgumentTypes[newArgumentTypes.length - 1] = Type.getType(clazz)
    return Type.getMethodDescriptor(returnType, newArgumentTypes)
}

3.1.2 通过线程的子类创建线程

我们来看一个继承 Thread 的普通类创建线程的方式

new MyThread_2("mythread_222").start();

字节码
// 0
methodVisitor.visitTypeInsn(NEW, "com/zhangyue/ireader/asm_hook/handleThread/MyThread_2");
// 1
methodVisitor.visitInsn(DUP);
// 2
methodVisitor.visitLdcInsn("mythread_222");
// 3
methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/zhangyue/ireader/asm_hook/handleThread/MyThread_2", "<init>", "(Ljava/lang/String;)V", false);
// 4
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/zhangyue/ireader/asm_hook/handleThread/MyThread_2", "start", "()V", false);

不同于直接创建线程的方式,0 处和 3 处的 className 是可变的,所以没有办法使用 3.1.1 优化线程代理的方式处理。 可不可以从继承类本身入手呢,答案是可以的。下面是一个普通的继承 Thread 的类

public class MyThread_2 extends Thread{
    public MyThread_2(@NonNull String name) {
        super(name);
    }
}

// 0 - 将 this 加载到栈顶
methodVisitor.visitVarInsn(ALOAD, 0);
// 1 - 将 name 加载到栈顶
methodVisitor.visitVarInsn(ALOAD, 1);
// 2 - 调用 super 方法
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/String;)V", false);

可以看到对于所以的直接继承 Thread 的类, 2 处的访问方法指令是固定的,所以可以从这里入手。

在编译程序代码的时候,方法栈帧需要多大的局部变量表,多深的操作数栈都已经确定了。操作数栈中的元素及出栈/入栈操作也都已经编译到了字节码中,所以我们可以通过 ASM 修改操作数栈相关的字节码指令,只需要保证修改之后的操作数栈中元素的数据类型和字节码指令的序列严格匹配。

有点抽象,以上述代码为例。我们需要做的就是,在 2 处前插入字节码指令,既能实现添加类名的目的,同时和 2 处访问方法指令匹配。如果直接在 2 处前通过插入 ldc 指令插入类名,那么就和 2 处的指令无法匹配,Thread 也没有和如下所示的描述符

(Ljava/lang/String;Ljava/lang/String;)V

匹配的构造方法。 所以可以通过插入一个访问静态方法的指令的方式插入一个新的方法栈帧,消费两条 ldc 指令的参数,返回一个 String 类型的参数。

methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitVarInsn(ALOAD, 1);
// 将代表 className 的常量推送至栈顶
methodVisitor.visitLdcInsn("\u200bcom.zhangyue.ireader.asm_hook.handleThread.MyThread_2");
// 访问静态方法,String,String --> String
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/zhangyue/ireader/optimizeThreadProxy/ShadowThread", "makeThreadName",
// 消费两条 ldc 指令,返回一个 String 类型的参数留在操作数栈中
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", false);
// 操作数栈中的结构仍然和访问方法指令是匹配的
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/String;)V", false);

这是其中一种场景。针对 Thread 的构造函数的参数不同,还有 2 种场景。

  • 插入 ldc 指令后,有匹配的构造函数,这种是比较容易处理的,只需要修改访问超类构造函数的字节码指令中的 descriptor,添加 String 类型的参数
super() ---> super(name)

methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "()V", false);
-->
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitLdcInsn("your className");
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/String;)V", false);
  • 还有一种是参数最多的 Thread 构造函数,笔者也是是参考了booster才知道如何处理,核心逻辑还是通过操作 操作数栈 中的元素来实现的。
case '(Ljava/lang/ThreadGroup;Ljava/lang/Runnable;Ljava/lang/String;J)V':   //Thread(ThreadGroup, Runnable, String, long)
    // in order to modify the thread name, the penultimate argument `name` have to be moved on the top
    // of operand stack, so that the `ShadowThread.makeThreadName(String, String)` could be invoked to
    // consume the `name` on the top of operand stack, and then a new name returned on the top of
    // operand stack.
    // due to JVM does not support swap long/double on the top of operand stack, so, we have to combine
    // DUP* and POP* to swap `name` and `stackSize`
    
    // JVM SWAP 指令不支持对 long 和 double 类型的操作数

    //  ..., name,stackSize => ...,stackSize, name,stackSize
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.DUP2_X1))

    //  ...,stackSize, name,stackSize => ...,stackSize, name
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.POP2))

    //  ...,stackSize, name => ...,stackSize, name,prefix
    methodNode.instructions.insertBefore(insnNode, new LdcInsnNode(makeThreadName(cn.name)))

    //  ...,stackSize, name,prefix => ...,stackSize, name
    methodNode.instructions.insertBefore(insnNode, new MethodInsnNode(Opcodes.INVOKESTATIC,
            SHADOW_THREAD, 'makeThreadName', '(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;', false))

    //  ...,stackSize, name => ...,stackSize, name,name
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.DUP))

    //  ...,stackSize, name,name => ...,name,name,stackSize, name,name
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.DUP2_X2))

    //  ...,name,name,stackSize, name,name => ...,name,name,stackSize
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.POP2))

    //  ...,name,name,stackSize => ...,name,stackSize,name,stackSize
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.DUP2_X1))

    //  ...,name,stackSize,name,stackSize => ...,name,stackSize,name
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.POP2))

    //  ...,name,stackSize,name => ...,name,stackSize
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.POP))

3.2 线程池优化

常用的使用线程池的方式有两种,一种是使用 Executors 工具类,一种是创建 ThreadPoolExecutor 实例

3.2.1 Executors

使用 executors 可以创建 5 种线程池,每一种方式其对应的字节码都是固定的,所以我们可以轻松的通过扫描代码找到匹配的字节码进行处理。 下面就以 newFixedThreadPool 举例。

ExecutorService services = Executors.newFixedThreadPool(1);

// 将设置的线程数量推送置栈顶
methodVisitor.visitInsn(ICONST_1);
// 方法调用
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/util/concurrent/Executors", "newFixedThreadPool", "(I)Ljava/util/concurrent/ExecutorService;", false);

通过替换 1 处指令的 ownerdescpritor,替换成代理方法

// 将设置的线程数量推送置栈顶
methodVisitor.visitInsn(ICONST_1);
// 将调用者类名推送置栈顶
methodVisitor.visitLdcInsn("your className");
// 调用代理方法,内部会做添加类名前缀和设置核心线程可超时的处理
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/zhangyue/ireader/optimizeThreadProxy/ShadowExecutors", "newOptimizedFixedThreadPool", "(ILjava/lang/String;)Ljava/util/concurrent/ExecutorService;", false);

代理方法的内部实现

public static ExecutorService newOptimizedFixedThreadPool(int nThreads, String name) {
    ThreadPoolExecutor t = new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(), new NamedThreadFactory(name));
    t.setKeepAliveTime(DEFAULT_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS);
    t.allowCoreThreadTimeOut(true);
    return t;
}

NamedThreadFactory 会将传入的 className 添加为线程名称的前缀,这里就不贴代码了

修改空闲线程存活时间及设置核心线程可超市固然可以带来优化线程数的收益,但是也可能产生意想不到的副作用。比如某个定时任务的周期和设置的存活时间相同,会导致线程池中的线程刚销毁就需要重新创新创建,带来的额外的系统开销。所以笔者这里通过插件配置的参数来控制是否启用线程池优化。

static def transformInvokeStatic(cn, methodNode, insnNode) {
    if (insnNode.owner == 'java/util/concurrent/Executors') {
        switch (insnNode.name) {
            case 'newCachedThreadPool':
            case 'newFixedThreadPool':
            case 'newSingleThreadExecutor':
                transformThreadPool(cn, methodNode, insnNode, Config.enableThreadPoolOptimized)
                break
            case 'newScheduledThreadPool':
            case 'newSingleThreadScheduledExecutor':
                transformThreadPool(cn, methodNode, insnNode, Config.enableScheduleThreadPoolOptimized)
                break
            default:
                break
        }
    }
}

static def transformThreadPool(ClassNode cn, MethodNode methodNode, MethodInsnNode insnNode, boolean enableThreadPoolOptimized) {
    // 替换成代理类
    insnNode.owner = 代理类的权限定名称
    // ldc className
    methodNode.instructions.insertBefore(insnNode, new LdcInsnNode(makeThreadName(cn.name)))
    //descpritor 添加 String 类型参数
    def index = insnNode.desc.lastIndexOf(')')
    insnNode.desc = insnNode.desc.substring(0, index) + 'Ljava/lang/String;' + insnNode.desc.substring(index)
    // 替换为代理类中的方法 newFixedThreadPool -> newOptimizedFixedThreadPool
    insnNode.name = enableThreadPoolOptimized ? insnNode.name.replace('new', 'newOptimized') : insnNode.name.replace('new': 'newNamed')
}

3.2.2 创建自定义线程池

对创建自定义线程池的处理方式和 3.1.1 所述处理线程的方式类似,在这里简单的说一下。

  • 如果是直接创建 ThreadPoolExecutor。通过扫描代码,找到匹配的字节码指令后,将创建自定义线程池的指令替换为创建代理线程池的指令。
new ThreadPoolExecutor(1, 1, 30, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
->
new ShadowThreadPoolExecutor(1, 1, 30L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), "className", true);

字节码

methodVisitor.visitTypeInsn(NEW, "java/util/concurrent/ThreadPoolExecutor");
methodVisitor.visitInsn(DUP);
methodVisitor.visitInsn(ICONST_1);
methodVisitor.visitInsn(ICONST_1);
methodVisitor.visitLdcInsn(new Long(30L));
methodVisitor.visitFieldInsn(GETSTATIC, "java/util/concurrent/TimeUnit", "MILLISECONDS", "Ljava/util/concurrent/TimeUnit;");
methodVisitor.visitTypeInsn(NEW, "java/util/concurrent/LinkedBlockingQueue");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/concurrent/LinkedBlockingQueue", "<init>", "()V", false);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/concurrent/ThreadPoolExecutor", "<init>", "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;)V", false);

--->

methodVisitor.visitTypeInsn(NEW, "com/zhangyue/ireader/optimizeThreadProxy/ShadowThreadPoolExecutor");
methodVisitor.visitInsn(DUP);
methodVisitor.visitInsn(ICONST_1);
methodVisitor.visitInsn(ICONST_1);
methodVisitor.visitLdcInsn(new Long(30L));
methodVisitor.visitFieldInsn(GETSTATIC, "java/util/concurrent/TimeUnit", "MILLISECONDS", "Ljava/util/concurrent/TimeUnit;");
methodVisitor.visitTypeInsn(NEW, "java/util/concurrent/LinkedBlockingQueue");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/concurrent/LinkedBlockingQueue", "<init>", "()V", false);
//插入 ldc 字节码指令
methodVisitor.visitLdcInsn("your class name");
methodVisitor.visitLdcInsn(new Integer(1));
methodVisitor.visitMethodInsn(INVOKESPECIAL, 
//修改指令 descpritor,和操作数栈匹配
"com/zhangyue/ireader/optimizeThreadProxy/ShadowThreadPoolExecutor", "<init>", "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/lang/String;Z)V", false);	


代理类方法
public ShadowThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, String name, boolean enableOptimized) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new NamedThreadFactory(name));
    if (enableOptimized && getKeepAliveTime(unit) > 0) {
        this.allowCoreThreadTimeOut(true);
    }
}

  • 通过 ThreadPoolExecutor 的子类创建线程池。处理方式也是通过 ASM 在扫描到匹配的方法指令之后,修改构造方法的操作数栈来达到重命名线程的目的。

public TestThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}

原始指令
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitVarInsn(ILOAD, 2);
methodVisitor.visitVarInsn(LLOAD, 3);
methodVisitor.visitVarInsn(ALOAD, 5);
methodVisitor.visitVarInsn(ALOAD, 6);
methodVisitor.visitVarInsn(ALOAD, 7);
methodVisitor.visitVarInsn(ALOAD, 8);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/concurrent/ThreadPoolExecutor", "<init>", "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/util/concurrent/RejectedExecutionHandler;)V", false);

处理方式
case '(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/util/concurrent/RejectedExecutionHandler;)V':
    // ..., threadFactory,handler -> ..., handler,threadFactory
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.SWAP))
    // ..., handler,threadFactory -> ...,handler,threadFactory,name
    methodNode.instructions.insertBefore(insnNode, new LdcInsnNode(makeThreadName(cn.name)))
    // ...,handler,threadFactory,name -> ..., handler,threadFactory
    methodNode.instructions.insertBefore(insnNode, new MethodInsnNode(Opcodes.INVOKESTATIC, NAMED_THREAD_FACTORY, 'newInstance',
            '(Ljava/util/concurrent/ThreadFactory;Ljava/lang/String;)Ljava/util/concurrent/ThreadFactory;', false))
    //交换回来,符合参数顺序
    // ..., handler,threadFactory -> ..., threadFactory,handler
    methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.SWAP))
    break
    
处理后指令
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitVarInsn(ILOAD, 2);
methodVisitor.visitVarInsn(LLOAD, 3);
methodVisitor.visitVarInsn(ALOAD, 5);
methodVisitor.visitVarInsn(ALOAD, 6);
methodVisitor.visitVarInsn(ALOAD, 7);
methodVisitor.visitVarInsn(ALOAD, 8);
methodVisitor.visitInsn(SWAP);
methodVisitor.visitLdcInsn("\u200bcom.zhangyue.ireader.asm_hook.handleThread.TestThreadPoolExecutor");
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/zhangyue/ireader/optimizeThreadProxy/NamedThreadFactory", "newInstance", "(Ljava/util/concurrent/ThreadFactory;Ljava/lang/String;)Ljava/util/concurrent/ThreadFactory;", false);
methodVisitor.visitInsn(SWAP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/concurrent/ThreadPoolExecutor", "<init>", "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/util/concurrent/RejectedExecutionHandler;)V", false);
    

总结

在进行线程重命名线程的在 APM 工具中显示的名称由 thread-index 优化为 className-thread-index,更加的清晰明了。在进行线程池优化后,APP 空闲时,常驻线程的数量有 150 多条减少到 120 多条,优化 20% 左右,效果比较明显。

源码

最后,该项目已经在 github 上开源,希望大家多多围观。项目地址

参考链接

AOP技术在APP开发中的多场景实践

多线程优化