同事跟我说线上的一个dubbo provider服务启动不了了,然后发了一段报错信息,因为这个项目之前一直是我在跟,我就登上机器看了下
1.排查原因
// 这边省略了很多,loadClass递归了导致了StackOverflowError
Exception in thread "main" java.lang.StackOverflowError
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.getDeclaredMethods(Class.java:1975)
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:612)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:524)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:510)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:241)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1069)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1042)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)
at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:93)
at com.alibaba.dubbo.container.spring.SpringContainer.start(SpringContainer.java:50)
at com.alibaba.dubbo.container.Main.main(Main.java:80)
整个异常的调用栈就是这样。初步看起来是spring容器在创建bean的时候,加载这个bean相关的类时导致了栈溢出。
我们部署的dubbo provider使用的是 Dubbo 中的服务容器,dubbo的服务容器启动使用的是下图标红的start.sh
这个脚本中的设定是64位机器上线程的栈最大是256k,虽然256k的确有些小,但之前一直也都ok的,怎么现在不行了。
BITS=`java -version 2>&1 | grep -i 64-bit`
if [ -n "$BITS" ]; then
# JAVA_MEM_OPTS=" -server -Xmx2g -Xms2g -Xmn256m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 "
JAVA_MEM_OPTS=" -server -Xmx2g -Xms2g -Xmn256m -XX:MetaspaceSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 "
else
JAVA_MEM_OPTS=" -server -Xms1g -Xmx1g -XX:PermSize=128m -XX:SurvivorRatio=2 -XX:+UseParallelGC "
fi
本地启动vm参数加入-Xss256k 果然是相同的报错,加大到-Xss512k就能正常启动了。但我还是想看看到底是什么新的代码导致了这个问题。
分析调用栈是spring容器初始化bean的时候需要获取对象的构造方法,遍历了这个类的所有的方法,期间触发了类的加载,导致的stackOverflow。可是这个代码并没有日志打印beanName,无从知晓是哪个类引起的问题,也不可能在这边打断点一个个看,毕竟那么多需要加载的bean呢,那有什么办法呢。javaAgent加日志好像可行。
@Override
public Constructor>[] determineCandidateConstructors(Class> beanClass, final String beanName) throws BeansException {
if (!this.lookupMethodsChecked.contains(beanName)) {
ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Lookup lookup = method.getAnnotation(Lookup.class);
if (lookup != null) {
LookupOverride override = new LookupOverride(method, lookup.value());
try {
RootBeanDefinition mbd = (RootBeanDefinition) beanFactory.getMergedBeanDefinition(beanName);
mbd.getMethodOverrides().addOverride(override);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(beanName,
"Cannot apply @Lookup to beans without corresponding bean definition");
}
}
}
});
this.lookupMethodsChecked.add(beanName);
}
2.写一个javaagent
参考了如下博文
基于 Javassist 和 Javaagent 实现动态切面
JVM插码之三:javaagent介绍及javassist介绍
使用agent和javassist实现基于jvm的aop日志打印系统
完成了自己的实现,具体的实现可以查看传入类名和方法名打印指定方法的入参agent实现--GitHub这边就不具体讲了
jvm 参数加上
-javaagent:D:\javaAgentDemo\javaAgent\target\javaAgent-1.0.0-SNAPSHOT.jar=AutowiredAnnotationBeanPostProcessor,determineCandidateConstructors
日志打印显示是我们使用obs的bean,每次启动到这就会打印StackOverflowError
锁定了一个方法,为什么锁定这个方法呢
最近我们的对象存储服务从阿里云迁移到了华为云,其他方法都跟阿里云一样,就这个是我新增的一个方法。
public ObsClient getObsClient() {
String accessKeyId = obsConf.getAccessKeyId();
String accessKeySecret = obsConf.getAccessKeySecret();
String endpoint = obsConf.getEndpoint();
return new ObsClient(accessKeyId, accessKeySecret, endpoint);
}
可看起来这个方法也没什么特殊的啊,一看这个方法的返回吓一跳。ObsClient 的继承关系居然有20多层,spring获取ObsOperateServiceImpl的方法时需要加载ObsClient这个类,由于这个类的继承层级有20多层导致了StackOverflowError。把这个方法注释掉就能正常启动了。
(华为云的代码真心不怎么样啊,这个ObsClient集合了一堆东西,jar包就有5m大)
可惜的是google了很久也没有找到能准确统计栈内存增加的方法。