java8的正式版本已经发布了2年多了,咱们都知道java8更加高效,好比更加高效的G一、更加高效的jit、默认开启TieredCompilation更加高效的工做模式以及和“java.lang.OutOfMemoryError: PermGen space” 撒油啦啦 等等高大上的性能优化;固然了,这都不是最主要的:
主要缘由是由于前一段时间线上tomcat很诡异的就忽然挂掉了,经查失败log,是命中了一个jvm crash的bug,bug详见:http://bugs.java.com/view_bug.do?bug_id=8021898,jdk的1.7.0_60后修复了这个问题,正好看到java8那么多高大上的优化后,就准备一步到位直接升级到java8;
可是理想是美好的,现实是骨感的;咱们很荣幸的掉进了坑里了,下面主要从spring4.0的坑和新metaspace问题 这2个方面来描述。java
上面说了,遇到了jvm的crash的bug,必需要升级jdk的版本;找了套测试环境作了回归,发现没啥问题,很是高兴,准备在线上开搞。web
最开始的找了2组流量相对较小的机器,升级后重启服务没有发现异常现象,觉得没啥问题了,就准备开始搞一组qps比较高的机器,替换jdk重启以后没啥问题,而后观察了会,发现load在缓慢上升,紧接着服务的接口开始大量的超时,见下图:spring
首先,是抓了几个tomcat栈log下来,研究栈发现有大量的lock,见下图:tomcat
往下继续找正在运行的那个线程,以下图:性能优化
经过上面的现象看到,不论是lock的,仍是runnable的,都是卡在的spring的代码栈下,这里就有点奇怪了,spring的版本没有升级啊,先否决了,由于java8针对gc和jit有过相关的,首先来开始排查这2者,继续往下;服务器
首先排查了gc的问题,发现除了比以前更频繁一点以外,其它没啥区别,也没有触发full gc,基本能够排除。app
java8针对jit有一些默认的优化,因此发现gc没有问题的状况下,首先想到的就是jit的设置问题。jvm
首先怀疑的是jit的cache的内存大小问题,调大后重启,发现只能延缓一点load升高的时间,没有根本解决问题
而后是调整jit的默认线程数,以前咱们是设置了2个线程,使用默认的3个线程,有必定的效果,抗住了20分钟;而后继续调大四、5,发现反而比以前挂了还要快,重启以后基本上就挂了,也基本能够排除。ide
最容易出现问题的方面排查过了,没有发现问题,那就回归栈日志来排查,找不到明显问题,问下Google吧。
还真有发现:spring官网有人汇报过一个bug,SerializableTypeWrapper从jdk的1.7.0_51这个版本开始出现了性能瓶颈,bug地址详见:性能
https://jira.spring.io/browse/SPR-11335
咱们以前的是用的jdk1.7.0_45这个版本,没有发现这个问题;具体缘由在后面来描述。
升级spring的版本就能够解决这个问题。
从4.0.3开始,spring针对jdk8有较大的改善,具体可参见官网说明:https://spring.io/blog/2014/03/27/spring-framework-4-0-3-released-with-java-8-support-now-production-ready
首先咱们来看下ResolvableType这个类的相关代码,以下图:
能够看到1018-1022行之间,先是从cache中取,若是没有取到,就会往cache中put一个,经过上面的栈能够知道,equals比较耗时(会经过SerializableTypeWrapper生成相关代理),这里get和put之间没有任何拦截措施,get不到直接put,put内部是有lock的,就是咱们看到的上面的lock的图,这样就有可能致使大量的线程等待。
按理说即便会慢,也应该只是在刚开始启动的时候,等都初始化彻底后,cache中就有了,后面就行了啊,我们继续往下看。
ConcurrentReferenceHashMap是一个soft cache, 过时的时候会走到put里,加锁等待,致使更多的线程block, 线程block致使tomcat更多的线程同时运行致使gc压力,而soft cache是一个软cache,压力过大时会被gc回收掉,这样会致使soft cache 接连失效;这也就验证了上面开始的gc比之间频繁的问题,这种状况在qps高的服务下会比较容易出现。
能够看到在get以前加了一个check操做;大致意思就是checker一下cache的有效性,若是cache已过时,把过时的信息强制剔除以提升效率。
能够看到针对equals有优化操做;大致意思是在执行equals方法的时候,直接执行原始的equals,不用再通过多层代理的过滤;尤为是针对ResolvableType这种高执行频率的操做效果较好。
通过上面的折腾,终于算是把java8用上了,运行了几天以后很正常(实际上是我想多了)。
在大概一个星期以后的凌晨,忽然收到报警,线上服务swap有报警或者服务挂掉了……
查了下缘由,问题出如今java8新搞得MetaSpace身上。
首先来介绍2个概念:
PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,说说为何会内存益出:这一部分用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和和存放Instance的Heap区域不一样,因此若是你的APP会LOAD不少CLASS的话,就极可能出现PermGen space错误。这种错误常见在web服务器对JSP进行pre compile的时候。
Java8将移除永久区,使用本地内存来存储类元数据信息并称之为:元空间(Metaspace)。
Java8的启动参数:PermSize 和 MaxPermSize 会被忽略并给出警告(若是在启用时设置了这两个参数)。
这意味着不会再有java.lang.OutOfMemoryError: PermGen问题,也再也不须要你进行调优及监控内存空间的使用……但请等等,这么说还为时过早。在默认状况下,这些改变是透明的,接下来咱们的展现将使你知道仍然要关注类元数据内存的占用。请必定要牢记,这个新特性也不能神奇地消除类和类加载器致使的内存泄漏。
默认状况下,类元数据只受可用的本地内存限制(容量取决因而32位或是64位操做系统的可用虚拟内存大小)。
新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。若是没有指定这个参数,元空间会在运行时根据须要动态调整。
在java8里,因为PermSize 和 MaxPermSize已经失效,而你又正好没有设置MetaspaceSize和MaxMetaspaceSize这2个参数,那么就有可能会致使 metaspace的空间在不停的扩展,会致使机器的内存不足;进而可能出现swap内存被吃;严重可能致使进程直接被系统直接kill掉。
升级java8应该注意如下2点:
需配合Spring 4.0.3以上版本使用
须要配置Metaspace大小
暂时为止,升级java8的问题暂时告一段落。
若是你近期有升级java8的计划,正好你也看到了这篇文章,那么恭喜你,你能够少踩2个坑;java8很美好,升级需谨慎。
还遗漏一个问题,为何ResolvableType的equals在java8下性能会明显降低,你们能够本身找找源码,后面有机会能够再单独出一篇文章来解释下。