V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
4ra1n
V2EX  ›  Java

有没有什么思路对 Fat Jar 进行瘦身

  •  
  •   4ra1n · 6 天前 · 1341 次点击
    考虑到反射或者懒加载等内容,我思路是这样的:

    1. 启动时挂一个 java agent 上去,内容是 hook 加载本地 jar 文件的方法
    2. 用户尝试自己需要的功能,例如注册/登录/管理后台等等,完全做一遍
    3. 然后看整个流程中 java agent 这边记录的,都加载了哪些 jar 文件
    4. 对比存在的 jar 得出一个可以删掉的 jar 列表

    请教下这个思路是否可行,以及我需要 hook 的类应该是哪些,或者有没有更好的思路
    15 条回复    2025-01-02 11:08:11 +08:00
    bunnyblueair
        1
    bunnyblueair  
       6 天前
    ProGuard
    4ra1n
        2
    4ra1n  
    OP
       6 天前
    ProGuard 主要是两个问题,第一个配置非常麻烦,不是容易上手的方式

    另外一个问题是,它应该是静态分析的引用关系,而不是动态决定的,比如反射问题,某些功能如果是运行时候决定是否调用某个类的,是无法处理的
    foolishcrab
        3
    foolishcrab  
       6 天前
    可行,不需要考虑对应用性能影响的时候这个是很简单的东西。
    叫 reachability analysis, native image 就有一个专门的 agent 来收集 runtime reflection metadata 之类的东西。

    其实这个方案最大的问题在于你帖子里的第二步,对于一个大型软件而言这是很难的,所以一般要在生产上挂着用真实流量收集,这样的话就需要考虑 agent 的性能影响。这里就不展开了
    sagaxu
        4
    sagaxu  
       6 天前
    java --verbose:class 找出用到的所有 class 和 jar 包,删掉没用的 jar 包,甚至 jar 包内部删掉无用的 class
    chendy
        5
    chendy  
       5 天前
    fat jat 解压开,依赖的 jar 包放某个共享目录
    所有项目挂在这个共享目录,然后配好 classpath
    比较丑陋,但是确实能用
    4ra1n
        6
    4ra1n  
    OP
       5 天前
    已经实现了,大概是:

    capabilities.can_generate_all_class_hook_events = 1;
    (*jvmti)->AddCapabilities(jvmti, &capabilities);
    callbacks.ClassFileLoadHook = &ClassLoadHook;

    在 ClassLoadHook 函数中做收集

    java 启动参数 -agentpath:/path/to/agent.dll

    可以收集所有加载的 class 信息,动态地
    4ra1n
        7
    4ra1n  
    OP
       4 天前
    我发现不能对 ClassFile 做操作,有时候取到的 name 是 null

    callbacks.ClassFileLoadHook = &ClassLoadHook;

    使用 ClassLoad 会更靠谱

    callbacks.ClassLoad = &ClassLoadHook;

    void JNICALL ClassLoadHook(
    jvmtiEnv *jvmti_env,
    JNIEnv *jni_env,
    jthread thread,
    const jclass klass) {
    char *classSignature;
    char *genericPtr;
    const jvmtiError err = (*jvmti_env)->GetClassSignature(
    jvmti_env, klass, &classSignature, &genericPtr);
    if (err != JVMTI_ERROR_NONE) {
    LOG_JVM(jni_env,"ERROR GET CLASS SIGNATURE");
    return;
    }
    DoAnalyze(jni_env,classSignature);
    (*jvmti_env)->Deallocate(jvmti_env, (unsigned char *) classSignature);
    if (genericPtr != NULL) {
    (*jvmti_env)->Deallocate(jvmti_env, (unsigned char *) genericPtr);
    }
    }
    635925926
        8
    635925926  
       3 天前
    代码里搜索依赖包的基础包名如何。依赖使用不多的就改成自己实现。
    lixiaolin123
        9
    lixiaolin123  
       3 天前
    @4ra1n java agent 不是可以通过字节码/字节注入来实现运行时的吗?
    Aresxue
        10
    Aresxue  
       3 天前
    这个诉求没这么复杂,java agent 把 instrumentation 的引用保存起来就行了,然后放线上去接受真实流量,跑个一天或者手动触发业务操作,再把某台容器从注册中心剔除掉,到这台容器上调用 http 接口,接口里面用 java.lang.instrument.Instrumentation#getAllLoadedClasses 获取所有的 class ,然后对 class 所在 jar 包去重就行了
    lianhuayu420
        11
    lianhuayu420  
       2 天前
    记得 maven 好像有个插件可以将所有只用到的 class 抽出来然后打包
    4ra1n
        12
    4ra1n  
    OP
       2 天前
    @lianhuayu420 嗯,这个主要问题是,运行时到底加载了哪些 CLASS

    maven 插件的静态分析难免漏了

    例如一个 controller 接收 param clazz=xxx

    代码是 Class.forname(xxx); 然后 newInstance 后续做什么事情

    如果不动态地根据功能测出具体业务需要哪个 class 就会导致不可用

    我的出发点是这个
    4ra1n
        13
    4ra1n  
    OP
       2 天前
    @Aresxue 感谢

    确实 Java 层做信息收集,比 C 层 callbacks.ClassLoad 会更通用,我之后实践一下

    两者的区别是:

    C 层的 Hook 是实时的,Java Agent 是做完一些列操作之后,调用一下 getAllLoadedClasses 得到这个过程中的结果

    就这个需求来看,不太需要实时地,最终拿到结果即可
    int0x03
        14
    int0x03  
       1 天前
    如果是在程序稳定一段时间后查看所有被加载的类, 可以尝试下面的方法:

    ```bash
    # 找到对应的 Java 进程
    $ xxx/jdk/bin/jps
    77675 MyApp

    # 查看 jcmd 子命令, 根据 JDK 版本, 可能看到不同的子命令
    $ xxx/jdk/bin/jcmd 77675 help
    GC.class_histogram
    VM.class_hierarchy
    VM.classes

    # 查看加载的类
    $ xxx/jdk/bin/jcmd 77675 GC.class_histogram

    77675:
    num #instances #bytes class name (module)
    -------------------------------------------------------
    1: 2680074 131859776 [B ([email protected])
    2: 2566521 61596504 java.lang.String ([email protected])
    3: 1194171 47766840 java.util.TreeMap$Entry ([email protected])
    4: 348145 29392240 [Ljava.lang.Object; ([email protected])
    5: 139184 19783688 [I ([email protected])
    6: 59540 18054840 [J ([email protected])
    7: 322735 10327520 java.util.HashMap$Node ([email protected])

    # 或者上面的结果保存到 csv, 然后做数据处理
    ```
    Aresxue
        15
    Aresxue  
       11 小时 42 分钟前
    @4ra1n 这里其实还有个更安全的方式,把 arthas 源码搞下来,然后自己开发一个新的 command ,可以直接复用 arthas 的 sc 指令( sc *),然后自己做 class->jar 文件的计数处理即可,这样可以使用 attach 模式而不是 premain ,对业务更安全影响也更小。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2910 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 14:50 · PVG 22:50 · LAX 06:50 · JFK 09:50
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.