JRASP内存泄漏检测与清除实践
2024-3-23 18:51:23 Author: mp.weixin.qq.com(查看原文) 阅读量:14 收藏

热加载与卸载已经成为RASP的标配,而涉及到插件或者脚本的卸载问题,却少有技术文档提及, 主要原因是RASP开发人员更多的偏向安全,即使是经验丰富的Java工程师,遇到内存泄露问题,也会感到棘手。

类卸载的条件十分苛刻,要同时满足下面的三个条件:

  • 类所有的实例对象已经被回收;

  • 加载该类的Classloder已经被回收;

  • 该类对应的java.lang.Class对象没有任何对方被引用;

一般的,无需关心class对象的引用关系,但是有些场景(如反射场景)会缓存类的Class对象,使得class对象的引用存在,导致JRASP无法卸载。

2. 类加载器置空

以JDK8为例子,Java虚拟机自带的类加载器有BootstrapClassLoader、ExtensionClassLoader和SystemClassLoader, 这些类加载器在JVM整个生命周期中都不会被置为空,因此它们加载的类也不会被卸载。

而用户自定义的类加载器,可以在使用完成之后将加载器的对象置空,从而满足类卸载的三个条件之一。

以JRASP代码为例子,执行卸载关闭操作之后,将自定义类加载器置空。

如果自定义类加载器没有正确的置空,JRASP将不会被完全的清理,从而引发内存泄漏。 现在我们做一个测试,将上面代码截图的第90行的 raspClassLoader=null;注释掉(即类加载器不置空)

打包编译后加载JRASP后再执行卸载操作,主动Full GC jmap-histo:live50730|grep com.jrasp.agent.core,结果如下所示:

 206:            50           3200  com.jrasp.agent.core.log.LogRecord 307:            12           1344  com.jrasp.agent.core.classloader.ModuleJarClassLoader 460:            12            480  com.jrasp.agent.core.module.CoreModule 542:            12            288  [Lcom.jrasp.agent.core.classloader.RoutingURLClassLoader$Routing; 548:            12            288  com.jrasp.agent.core.classloader.RoutingURLClassLoader$Routing // 其他类省略...

在Full GC之后,JRASP实例个数不为空,存在内存泄漏。使用性能诊断(jprofile,eclipse的MAT工具也是可以)工具,查看JRASP的对象:

查看其中一个对象的引用关系,如下所示:

从上图的引用关系可以明显看出,存在一条引用链路,链路从 RaspClassloader开始指向 LogRecord (上图中两个红色圈的之间的灰色线) (黄色为class对象,红色为GC Roots)

内存泄漏的原因:classloader没有置为空,导致内存泄漏

3. 对象置空和资源关闭

以JRASP为例说明,这里包括线程池关闭、自定义线程、定时器停止、shutdownHook移除、ClassFileTransformer移除和threadlocal线程变量清除等.

3.1 定时器停止

完全停止 java.lang.timer,需要将定时器线程停止,并将任务执行队列清空。

3.2 shutdownHook移除

在Java进程关闭之前,能够即时的清理rasp占用的磁盘等资源,shutdownHook可以执行指定的操作。

如果主动关闭rasp,没有清理shutdownHook,将会导致内存泄漏。

3.3 线程池关闭

如果RASP使用到了线程池,在卸载时需要关闭。即使关闭了线程池,由于jvm线程池重写了 finalize方法,一次FullGC依然无法清除残留的对象。 JRASP 1.2.x(商业版本) 已经把线程池替换为多个 java.lang.timer,卸载时非常清爽干净。

3.4 线程变量的清除

在JRASP中,使用线程变量threadlocal关联请求上下文与具体的hook类,来辅助检测功能。

在RASP卸载时,需要将线程thread中缓存的threadlocal对象。

在介绍JRASP实现方案之前,先来看下tomcat是如何实现热卸载的和内存泄漏检测的。

3.4.1 tomcat资源清除与内存泄漏检测

tomcat在卸载war包时,调用war的类加载器 WebappClassLoaderBase对象的stop方法完成资源的关闭与清理操作。

具体的引用清除实现来在 clearReferences中,主要有:注销JDBC驱动、关闭应用创建的线程、检查线程变量的内存泄漏等,关闭连接和线程的操作容易实现,本节主要针对线程变量的内存泄漏清理与检测。

  • 线程变量泄漏检测

在线程Thread对象中使用两个字段保存该线程使用的 threadlocal对象:

  1. public class Thread implements Runnable {

  2. /* ThreadLocal values pertaining to this thread. This map is maintained

  3. * by the ThreadLocal class. */

  4. ThreadLocal.ThreadLocalMap threadLocals = null;

  5. /*

  6. * InheritableThreadLocal values pertaining to this thread. This map is

  7. * maintained by the InheritableThreadLocal class.

  8. */

  9. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

  10. // 其他代码省略...

  11. }

threadLocals的类型是 ThreadLocalMap,ThreadLocalMap中用数组table保存threadlocal变量的key、value。 因此最终我们需要清理的是这个table里面的Entry。

  1. static class ThreadLocalMap {

  2. static class Entry extends WeakReference<ThreadLocal<?>> {

  3. Object value;

  4. Entry(ThreadLocal<?> k, Object v) {

  5. super(k);

  6. value = v;

  7. }

  8. }

  9. // 保存threadlocal变量的key、value

  10. private Entry[] table;

  11. }

tomcat中线程变量的内存泄漏检测代码在 checkThreadLocalsForLeaks中。

主要是反射threadLocals、inheritableThreadLocals

3.4.2 JRASP线程清除方案

一般的在使用完线程变量之后,要及时的调用 threadlocal.remove() 将线程变量移除。 但是对于RASP来说,业务线程池线程复用机制,并且无法确定什么时候任务执行完成,也就无法在任务执行完成之后清除。

JRASP采用类似于tomcat线程变量内存泄漏的检测方式,即反射调用 threadlocal.remove()方法。

cleanThreadLocals的实现:

上面的方案存在一些限制,JDK17以上禁止了跨模块的反射,上面的反射调用执行会报错,需要业务在JVM参数中增加 --add-opens=java.base/java.lang=ALL-UNNAMED 解除限制。(增加参数成本较低,业务使用了三方包也会开启该参数)

4. 总结

  本文介绍了JRASP卸载时的一些坑,并给出了解决方案,特别是线程池的threadlocal内存泄漏, 给出了检测和卸载代码,该方案在JRASP上使用较为成功。


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg5MjQ1OTkwMg==&mid=2247484693&idx=1&sn=d20162cc9237788b8b087bbf1563c449&chksm=c03c8b04f74b021212da83e058ca8e8f0c00ca37264db46d4fd2853cd3f60ec60487a81d3920&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh