ASM 字节码插桩 :线程治理

ASM 字节码插桩 :线程治理

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 内部会署理一切的线程结构函数并增加一个类名。这样当创立匿名线程时,实际上创立的是经过优化后的线程。

ASM 字节码插桩 :线程治理
这儿把经过 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开发中的多场景实践

多线程优化