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

43 天前
 4ra1n
考虑到反射或者懒加载等内容,我思路是这样的:

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

请教下这个思路是否可行,以及我需要 hook 的类应该是哪些,或者有没有更好的思路
1895 次点击
所在节点    Java
16 条回复
bunnyblueair
43 天前
ProGuard
4ra1n
43 天前
ProGuard 主要是两个问题,第一个配置非常麻烦,不是容易上手的方式

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

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

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

在 ClassLoadHook 函数中做收集

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

可以收集所有加载的 class 信息,动态地
4ra1n
42 天前
我发现不能对 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
40 天前
代码里搜索依赖包的基础包名如何。依赖使用不多的就改成自己实现。
lixiaolin123
40 天前
@4ra1n java agent 不是可以通过字节码/字节注入来实现运行时的吗?
Aresxue
40 天前
这个诉求没这么复杂,java agent 把 instrumentation 的引用保存起来就行了,然后放线上去接受真实流量,跑个一天或者手动触发业务操作,再把某台容器从注册中心剔除掉,到这台容器上调用 http 接口,接口里面用 java.lang.instrument.Instrumentation#getAllLoadedClasses 获取所有的 class ,然后对 class 所在 jar 包去重就行了
lianhuayu420
39 天前
记得 maven 好像有个插件可以将所有只用到的 class 抽出来然后打包
4ra1n
39 天前
@lianhuayu420 嗯,这个主要问题是,运行时到底加载了哪些 CLASS

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

例如一个 controller 接收 param clazz=xxx

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

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

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

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

两者的区别是:

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

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

```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 (java.base@21.0.4)
2: 2566521 61596504 java.lang.String (java.base@21.0.4)
3: 1194171 47766840 java.util.TreeMap$Entry (java.base@21.0.4)
4: 348145 29392240 [Ljava.lang.Object; (java.base@21.0.4)
5: 139184 19783688 [I (java.base@21.0.4)
6: 59540 18054840 [J (java.base@21.0.4)
7: 322735 10327520 java.util.HashMap$Node (java.base@21.0.4)

# 或者上面的结果保存到 csv, 然后做数据处理
```
Aresxue
37 天前
@4ra1n 这里其实还有个更安全的方式,把 arthas 源码搞下来,然后自己开发一个新的 command ,可以直接复用 arthas 的 sc 指令( sc *),然后自己做 class->jar 文件的计数处理即可,这样可以使用 attach 模式而不是 premain ,对业务更安全影响也更小。
4ra1n
36 天前
@Aresxue 学习了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://tanronggui.xyz/t/1100793

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX