码字不易,欢迎大家转载,烦请注明出处;谢谢配合
场景描述
JDK版本信息
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
JVM启动参数
#未明确指定,启动命令类似于以下形式
nohup java -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/dump/
-Xloggc:gc.log -XX:+PrintGCDetails
-jar xxx.jar --spring.profiles.active=prod
问题描述
项目启动时正常,没有频繁Full GC 情况发生,
项目运行一段时间后(大约半个月左右),出现频繁的Full GC(3-5秒一次),
严重影响服务的吞吐量以及稳定性。
案发取证
获取java应用进程id:jps
# 示例
jps
17802 Application.jar
观察GC情况:jstat -gcutil <PID> milliseconds
# 示例:每3000毫秒(3秒)输出一次GC情况
jstat -gcutil 17802 3000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 69.30 51.27 86.99 95.50 91.70 1057 8.450 5 1.374 9.824
0.00 69.30 60.54 86.99 95.50 91.70 1057 8.450 5 1.374 9.824
0.00 69.30 67.57 86.99 95.50 91.70 1057 8.450 5 1.374 9.824
堆使用情况:jmap -heap <PID>
服务器有备份的情况下,可以从集群中摘除,对当前节点生成堆dump文件:jmap -dump:format=b,file=xxx.hprof <PID>
# 示例
jmap -dump:format=b,file=plugin.hprof 17802
栈快照保存:jatack <PID> >jstack.log
# 示例:如果伴随CPU较高可以生产栈快照
jstack 17802 > jstack.log
所以一般建议打开HeapDumpOnOutOfMemoryError,HeapDumpPath等设置。
案情梳理
-
首先使用JDK1.8,未明确指定JVM启动参数,未指定垃圾收集器,默认会采用:
Parallel Scanvage + Parallel Old
来进行垃圾收集 -
使用
JProfiler
进行dump分析,也可以使用MAT等工具,如下图:
也可以利用jmap命令获取前10个数量的对象统计
jmap -histo pid| head -n 23
发现char数组、ConcurrentHashMap$Node实例数量比较多有200W+,同时char数组size达到605MB。
结合上图1-1.堆使用情况分析最大堆 948MB
,老年代 capacity 632MB
;怀疑问题可能出在char数组
产生了内存泄露,导致老年代过大,进而导致频繁Full GC
- 选中当前
char[]
进行引用分析
这里有很多引用分析的方法,例如:Incoming references
、Outgoning references
、Merged incoming references
、Merged outgoning references
、Merged dominating references
;以当前char[]
为例,其中Incoming references
、Outgoning references
更倾向于当前数组中的每个个体的信息,Merged incoming references
、Merged outgoning references
、Merged dominating references
更倾向于数组整体的信息,incoming
表示指向当前数组的引用关系,outgoning
表示当前数组的对其他对象的引用关系
1-3.引用分析图中含义表示 99%是String
实例,其中91%是AnnotationAwareAspectJAutoProxyCreator
。
- 我们再选取当前引用进行分析,如下图
1-4.具体引用
1-5.引用内容
我们发现91%
的引用竟然都是 redirect https://xxxxx?variable=变量值
,占用了堆中将近567MB的内存空间
- 那么redirect 为何会
占着茅坑不拉屎
呢,别着急,我们以其中一个为例,来看看它的GC Roots
我们选取其中一个GC Root 进行分析,我们发现其存在GC Root引用链,所以无法被回收,而这部分是应该被回收的,所以验证了我们的猜测,确实发生了 内存泄露
。
原因分析
我们找到了问题,如何梳理整个流程呢?RequestMappingHandlerAdapter
->AnnotationAwareAspectJAutoProxyCreator
->redirect:https://XXX
,我们模拟问题代码,探寻流程,示例如下:
@Controller
public class TestController {
/**用UUID模拟变量**/
@RequestMapping("/test1")
public String test() {
return "redirect:index.html?openId=" + UUID.randomUUID();
}
}
熟悉Spring MVC的同学应该知道,RequestMappingHandlerAdapter
是HandlerAdapter
的实现类,这里我们不做过多的描述,不熟悉的同学,可以查看笔者MVC的专题。如图由下到上是到RequestMappingHandlerAdapter的调用关系
而在 DispatcherServle
t执行完handle
之后,会进行视图的渲染,我们一起来看render
方法。
调用ViewResolver
来resolveViewName
ViewResolver
的抽象实现类AbstractCachingViewResolver
,具体过程可以参考示例代码来debug,这里调用了创建视图,并进行了缓存
注意: viewAccessCache
,viewCreationCache
都是有大小限制的这里不会造成内存泄露,限制大小为1024
调用子类UrlBasedViewResolver
执行createView
方法,创建视图的过程
调用子类UrlBasedViewResolver
执行applyLifecycleMethods
方法,初始化Bean
此处的initializeBean
最终会调用到AbstractAutowireCapableBeanFactory
,对应initializeBean的三个阶段,初始化前,初始化,初始化后
,这里的beanName
就是之前的字符串redirect:https://xxx
看到这里,你可能会有疑问,redirect
跟哪个BeanPostProcessor有关系呢?还记得GC Root 引用链中的AnnotationAwareAspectJAutoProxyCreator
么?如下是它的继承实现关系
AnnotationAwareAspectJAutoProxyCreator
的抽象父类AbstractAutoProxyCreator
实现了BeanPostProcessor
的子接口SmartInstantiationAwareBeanPostProcessor
,它的postProcessAfterInitialization
实现如下:
最终放入到adviseBeans
中,其实类型则是ConcurrentHashMap
,而其大小则是无限制的。
问题解决
经过以上悉心的分析,我们找到了问题的原因,那么该如何解决呢?常见的有以下几种解决方法。
方法一:使用RedirectView
@RequestMapping("/test4")
public RedirectView test4() {
RedirectView redirectView = new RedirectView("index.html");
Map<String, String> map = new HashMap<>();
map.put("openId", UUID.randomUUID().toString());
redirectView.setAttributesMap(map);
return redirectView;
}
为什么使用Redirect可以避免cache呢?原因在于,渲染render
方法中利用mv.isReference()
是否是引用。
所以直接使用RedirectView可以解决
方法二:直接利用response.sendRedirect
方法来重定向
@RequestMapping("/test3")
public void test3(HttpServletResponse response) {
try {
response.sendRedirect("index.html?openId=" + UUID.randomUUID());
} catch (IOException e) {
e.printStackTrace();
}
}
以上代码经过HandlerAdapter.handle
返回的ModelAndView
为null
,所以不会出现内存泄露
方法三:重定向的参数信息通过RedirectAttributes
传递
@RequestMapping("/test2")
public String test2(RedirectAttributes attributes) {
attributes.addAttribute("openId", UUID.randomUUID());
return "redirect:index.html";
}
此方法经过AbstractCachingViewResolver
缓存时 viewName
为redirecr:index.html
不带变量参数,即使通过BeanPostProcessor
也只会缓存一次。
第二次调用时,AbstractCachingViewResolver
可以从cache
中取出
总结
以上便是整个问题定位,以及解决的全部流程,希望大家也可以从本文中有所收获。
- 本文链接: https://www.sunce.wang/archives/jvm-xi-lie--shi-zhan-an-li-zhi-wo-de-redirect-zen-me-le-
- 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!