码字不易,欢迎大家转载,烦请注明出处;谢谢配合
场景描述
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 -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>
1-1.堆使用情况
生成堆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
案情梳理
-
首先使用JDK1.8,未明确指定JVM启动参数,未指定垃圾收集器,默认会采用:
Parallel Scanvage + Parallel Old来进行垃圾收集 -
使用
JProfiler进行dump分析,也可以使用MAT等工具,如下图:
1-2.dump分析
发现char数组、ConcurrentHashMap$Node实例数量比较多有200W+,同时char数组size达到605MB。
结合上图1-1.堆使用情况分析最大堆 948MB,老年代 capacity 632MB;怀疑问题可能出在char数组产生了内存泄露,导致老年代过大,进而导致频繁Full GC
- 选中当前
char[]进行引用分析
1-3.引用分析
这里有很多引用分析的方法,例如: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
1-6.Single GC Root
我们选取其中一个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的调用关系
调用关系
而在 DispatcherServlet执行完handle之后,会进行视图的渲染,我们一起来看render方法。
render
调用ViewResolver来resolveViewName
resolveViewName
ViewResolver的抽象实现类AbstractCachingViewResolver,具体过程可以参考示例代码来debug,这里调用了创建视图,并进行了缓存
resolveViewName实现
注意: viewAccessCache,viewCreationCache 都是有大小限制的这里不会造成内存泄露,限制大小为1024
Cache
调用子类UrlBasedViewResolver 执行createView方法,创建视图的过程
createView
调用子类UrlBasedViewResolver 执行applyLifecycleMethods方法,初始化Bean
applyLifecycleMethods
此处的initializeBean 最终会调用到AbstractAutowireCapableBeanFactory,对应initializeBean的三个阶段,初始化前,初始化,初始化后,这里的beanName就是之前的字符串redirect:https://xxx
initializeBean
applyBeanPostProcessorsAfterInitialization
看到这里,你可能会有疑问,redirect跟哪个BeanPostProcessor有关系呢?还记得GC Root 引用链中的AnnotationAwareAspectJAutoProxyCreator么?如下是它的继承实现关系
AnnotationAwareAspectJAutoProxyCreator
AnnotationAwareAspectJAutoProxyCreator的抽象父类AbstractAutoProxyCreator实现了BeanPostProcessor的子接口SmartInstantiationAwareBeanPostProcessor,它的postProcessAfterInitialization实现如下:
postProcessAfterInitialization
wrapIfNecessary
最终放入到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中取出
第二次
总结
以上便是整个问题定位,以及解决的全部流程,希望大家也可以从本文中有所收获。









网友评论