JVM学习路径

背景知识

  • 我希望你必须具备下面的背景知识,再开始jvm的旅程
java相关
书单备注
java基础默认需要你有java背景相关知识,熟练掌握java常规语法
thinking in java没啥说的,必读
core java和thinking in java二选一吧不过这些java砖头书里面很多内容对互联网来说真没啥用
effective java没啥说的,必读
java concurrency in practice没啥说的,必读
  
周边知识
书单备注
计算机组成原理非特定,任意相关类型书籍
数据结构非特定,任意相关类型书籍
算法非特定,任意相关类型书籍
操作系统非特定,任意相关类型书籍对于java应用程序来说,jvm可以认为是一层操作系统 
设计模式非特定,任意相关类型书籍推荐gof,head first等比较经典的书籍
  

0.0 入门中的入门

书单备注
深入理解JVM,周志明入门中的入门,这本书里的东西都是最最基础的如果这本书能勾起你对JVM进一步的兴趣,继续
JVM规范必读,在学习阶段读个2-3遍都不过分JVM的原理和最终的解释都在这里
Java语言规范必读,在学习阶段读个2-3遍都不过分Java语言的原理和最终的解释都在这里注意:JVM和Java语言两个规范在细节中是不完全一致的,想想看为什么?
  

0.1 补充知识

  • 如果你还希望坚持,说明你对JVM抱有一定的兴趣。继续的学习需要更深入的背景知识
书单备注
虚拟机 – 系统与进程的通用平台https://book.douban.com/subject/3611865/了解虚拟机的定义和发展
编译原理https://book.douban.com/subject/3296317/对编译优化感兴趣的话,必读
C/C++语言知识大型JVM一般都使用C++编写有些小型JVM也能做到纯C编写,不容易(题外话:写惯了OO的代码,看到这种尽量模仿OO的过程式的代码,真心不习惯。另外在CSDN上看到有人开了一门课,叫做用C语言构建大规模OO系统,我也是服)
汇编语言知识所有JVM的底层都离不开汇编,需要了解
深入理解计算机系统https://book.douban.com/subject/26912767/对计算机系统有一个全面的认识
  

0.2 深入学习

书单备注
oracle/sun关于hotspot的在线文档很经典
Oracle JRockithttps://book.douban.com/subject/4873919/三大虚拟机中的另一个
IBM关于J9的在线文档三大虚拟机中的最后一个
R神领衔的国内JVM社区https://hllvm.group.iteye.com/这几年随着iteye的没落也没落了
R神自己关于JVM的帖子https://rednaxelafx.iteye.com/blog/362738比较杂,不建议新人阅读
R神自己推荐的书籍https://rednaxelafx.iteye.com/blog/1886170比我列的强太多太多了
Shenandoah设计原理https://wiki.openjdk.java.net/display/shenandoah/Main号称是下一代的GC,设计比较先进不过这两年的开发进度缓慢,和死了区别不大
Zing设计原理https://www.azul.com/products/zing/virtual-machine/设计先进的商用JVM,适用内存至少64G的大型堆内存空间
Gil Tene的JVM讲义https://greenteajug.cn/2015/10/18/understanding-java-garbage-collection/
dalvik/art设计原理作为客户端的jvm,dalvik/art和服务端的jvm在设计和理念上都有不小的区别在我这样的纯粹主义者看来dalvik/art甚至不应该被称为jvm(不符合jvm规范)不过在学习方面,dalvik/art为我们提供了一个很有意思的另样角度
垃圾回收算法手册https://book.douban.com/subject/26740958/对gc算法感兴趣的话,必读
google scholar的论文非常多,筛选是个头疼的问题
  

0.3 源码阅读

  • 我不太推荐在知识储备不丰富的时候直接阅读源码
书单备注
自己动手写Java虚拟机https://book.douban.com/subject/26802084/这本书真的挺好的,浅显易懂,省略了一些比较复杂的细节原书是使用Go语言写Java虚拟机。我自己写的时候使用的是Java,实现难度不大
jamvm非常精简的jamvm,全C语言编写,阅读难度不大(回忆杀:很怀念当年我用jamvm+gnu classpath在arm4/6上面跑java服务的岁月)
其它相关jvm网上开源的小型jvm还有很多,jikes,kvm都是不错的选择
hotspot源码我不太推荐直接读,除非有比较深刻的知识储备
  

0.9 入门结束

  • 到此为止,我可以认为你已经渡过了jvm的入门阶段了,这对于我来说已经是未知领域了,祝好运

Spring + Jackson有可能导致类数量爆炸

上周四线上的一个应用突然假死状态,通过gc log发现正在频繁的full gc,从gc log上可以看出是Metaspace导致频繁full gc(实际上这个时候Metaspace还没满,还有几十M的剩余),然后用jmap -histo <pid>发现有大量的GeneratedConstructorAccessorXXX和GeneratedMethodAccessorXXX类型的对象(总共有两万多个)。

熟悉java反射的同学应该知道,这两个类都是在反射过程中调用Method.invoke产生的,前者顾名思义就是调用构造函数产生的,后者就是调用普通的方法产生的,生成这个类的代码在:MethodAccessorGenerator

private static synchronized String generateName(boolean isConstructor,boolean forSerialization){if (isConstructor) {if (forSerialization) {int num = ++serializationConstructorSymnum;return "jdk/internal/reflect/GeneratedSerializationConstructorAccessor" + num;else {int num = ++constructorSymnum;return "jdk/internal/reflect/GeneratedConstructorAccessor" + num;}else {int num = ++methodSymnum;return "jdk/internal/reflect/GeneratedMethodAccessor" + num;}}

看到大量的这两个类,第一个念头是谁使用反射没有复用Method对象吧(意思是,我们如果使用反射的时候,如果这个反射会频繁的调用,那么不要每次都去拿Method,最好将method缓存着,不然成本会很高的)。有了这个念头后,我就想如果能把这个类代码dump下来,然后统计一下看看,是不是有些方法对应有很多的GeneratedMethodAccessor,那就基本上知道是什么方法的反射导致的了,然后翻翻代码基本上能确定原因。

正好github上有人写了个工具,可以将class的字节码dump出来:dumpclass

java -jar dumpclass.jar -p <pid> sun.reflect.GeneratedMethodAccessor*

这样就可以将所有的这样的类的字节码全部dump出来了,有类字节码然后我们使用javap对字节码进行反编译,然后就可以进行统计了。遗憾的是,最后发现几乎没有两个不同的GeneratedMethodAccessor类属于同一个方法反射生成的。线索到这里就断了,那说明不是因为没有缓存Method导致的,而是正常的反射使用。然后我随机的看了几个GeneratedMethodAccessor类的字节码,所有的方法基本上都是getter,那这说明什么呢?一般如果是业务代码里使用反射,大部分是调用一个业务方法,而什么会去调用getter呢?比如序列化,比如json。这个时候我正好看到这个应用里有很多地方打印日志,而打印日志的时候是直接使用Jackson将对象序列化之后打印,这里使用的Jackson序列化工具是公共包(common-api)里用Jackson封装的一个工具类。

然后我就怀疑是不是这个应用记录日志太多了,然后Jackson序列化的时候会反射调用对象的getter,然后产生大量的这个GeneratedMethodAccessor类呢?于是我就自己写了个简单的测试,我发现公共包的这个工具类并不会去调用Method.invoke,这就奇怪了,不是Jackson导致的?

不过正好这个时候,我在GeneratedMethodAccessor字节码里除了看到了getter,也看到了setter方法。有了setter方法,那说明应该不是打印日志导致的,打印日志只会序列化不会反序列化,线索再次中断。

然后我就从GeneratedMethodAccessor的字节码里找到一个类,然后查找引用,最后找到了Spring的Controller这一层。这个应用提供了非常非常多的HTTP API,然后这些Controller基本上都是返回对象的方式返回响应,并且Controller的参数接受的也是对象,类似下面这样的:

@RequestMapping(value = "/api/web/businessInfo/list/v2", method = RequestMethod.POST)@JsonResponseBody//返回BusinessInfoGridVO对象,接受BusinessInfoListVO对象public Pager<BusinessInfoGridVO> BusinessInfoListV2(@RequestBody BusinessInfoListVO infoListVO) {

我们都知道,返回对象的方式返回响应,在Spring框架里,会使用HttpMessageConverter进行序列化,然后将序列化结果吐给客户端,而接受对象也是在Spring框架里反序列化成对象。然后我找到了这个这个应用的HttpMessageConverter配置(实际上公司的web应用,如果引入了common-web, common-web-support,是不需要配置HttpMessageConverter的):

@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {super.configureMessageConverters(converters);converters.add(new MappingJackson2HttpMessageConverter(JsonUtil.getObjectMapperInstance()));}

通过这里我们可以看到使用的是MappingJackson2HttpMessageConverter,而传入MappingJackson2HttpMessageConverter构造函数的是一个ObjectMapper:JsonUtil.getObjectMapperInstance()。这个JsonUtil是这个应用自己封装的一个json工具类(非来自common包),然后我就用这个工具类写个序列化测试,惊奇的发现产生了GeneratedMethodAccessor。看来是有什么配置导致公共包里的和这里的两个json工具类的差异。但是这个ObjectMapper的配置太多了,很难看出来是什么差异,而且我也对Jackson本身的机制不太熟悉,只好祭起了debug大法。

我先debug了一下公共包里的json工具类,发现这个类虽然不产生GeneratedMethodAccessor,但是会产生另外一个类:Xxx$Access4JacksonSerializer00000000,最前面的Xxx是你原来的类名。但是Jackson生成的机制和反射生成机制不同的是一个类只会产生一个这个类,而使用Java原生的反射是每个方法产生一个类。更恐怖的是,Java的反射给每个类都实例化一个单独Classloader(DelegatingClassLoader,而这也是导致Metaspace频繁full gc的其中一个原因,后文再说):

static Class<?> defineClass(String name, byte[] bytes, int off, int len,final ClassLoader parentClassLoader){ClassLoader newLoader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {public ClassLoader run() {return new DelegatingClassLoader(parentClassLoader);}});return unsafe.defineClass(name, bytes, off, len, newLoader, null);}

那么我们可以想象:假设我们有一万个getter+setter(对于大量的POJO来讲也不算多,也就总共5000个field)。

那为什么公共包里的JsonUtil不调用Method.invoke呢,经过debug找到了生成Xxx$Access4JacksonSerializer00000000的代码位置:PropertyAccessorCollector。然后设置断点,记录stacktrace,在每个stacktrack包含的方法都设置断点,然后再debug这个应用自己封装的JsonUtil类,发现这个JsonUtil没进入到这个路径,然后就打开两台电脑同时debug,看看是在哪里开始走不同的路径了,最终发现在这里com.fasterxml.jackson.databind.ser.BeanSerializerFactory:

// Need to allow reordering of properties to serializeif (_factoryConfig.hasSerializerModifiers()) {for (BeanSerializerModifier mod : _factoryConfig.serializerModifiers()) {props = mod.orderProperties(config, beanDesc, props);}}

这就很明显了,_factoryConfig.hasSerializerModifiers()看起来是配置的东西导致的。曙光来了,后来发现是这个是下面这个配置导致hasSerializerModifiers为true,在公共包的json工具类里是有这个配置的,而这个JsonUtil是没有配置的。

objectMapper.registerModules(new AfterburnerModule())

现在问题基本上确定了,也就是Spring使用的Jackson序列化导致的,只需要改一下配置基本上就没有问题了(AfterburnerModule github)。

但是在查这个问题的时候,还有另外一个问题:其实频繁发生full gc的时候,Metaspace并没有满。那么没有满为啥就频繁full gc呢,其实Metaspace的内存分配是按照classloader分配的,也就是会给每个classloader分配一块内存,所以如果存在大量的classloader的话,会有很严重的碎片问题,碎片多了自然没有满就会full gc了。

后记

1. 经常看到各种项目里有JsonUtil, HttpUtil,DateUtil, StringUtil等等各种Util,强烈不建议这样做,一来是没有什么必要,二来也让后来的人产生困惑,三来如果有bug什么的公共包里可以集中修复,另外公共包里的类也经过了更充分的测试。

2. 很多应用里打印了大量的日志,主要是担心查问题的时候没有线索,但是直接将对象序列化的方式打印日志并不可取,首先是这样打印的日志太多太多了,完全没有必要,消耗资源不说,还有可能泄露一些敏感信息。那么即使要将对象完全打印出来,最好也是通过覆盖toString的方法来输出日志,使用lombok等工具覆盖toString方法也几乎没有什么成本。

3. HttpMessageConverter这个配置公共包里已经配置好了,不需要自己再定义

注:转载自公司内wiki

Code Review 最佳实践

文章地址

原文地址:https://mp.weixin.qq.com/s/zIiDk7rcIKgvqh0YAvQPaA

英文地址:https://github.com/google/eng-practices/

CR的标准

  • Reviewer 有责任保证CL(change list,指这次改动)的质量,作为Review的代码的Owner
  • Reviewer不应追求完美,而应追求持续改进
  • CR要具有指导意义
  • 代码风格应该与现有的一致。如果项目没有统一风格,那就接受作者的风格
  • 解决冲突难以达成共识时,需要面对面或者拉起更大的团队讨论,带上Leader

在 CR 中要看些什么

  • 代码经过完善的设计
  • 功能性对于使用者们是好的
  • 对于任何UI改动要合理且好看
  • 任何并行编程的实现是安全
  • 代码不应该复杂超过原本所必须的
  • 开发者不该实现一个现在不用而未来可能需要的功能
  • 代码有适当的单元测试
  • 测试经过完善的设计
  • 开发者对于每样东西有使用清晰、明了的命名
  • 注释要清楚且有用,并只用来解释why而非what
  • 代码有适当的写下文件(一般在g3doc)
  • 代码风格符合style guide
  • 确保你查看被要求review的每一行代码、确认上下文、确保你正在改善代码质量,并赞扬开发人员所做的好事与优点吧!

如何浏览这次改动(CL)

步骤1: 用宏观的角度来看待改动,查看CL描述以及它做什么步骤2: 检查CL主要的部分步骤3: 用合理的顺序看CL 其余的改动

Review 的速度

为什么 Review 速度要快?

在Google我们优化开发团队 共同生产产品的速度,而不是优化个人开发的速度。个人的开发速度很重要,但它不如整个团队的开发速度重要。

CR 如果很慢,则:

①、团队整体的速度下降。

②、开发人员开始抗议 cr。

③、代码质量会收到影响。review 慢时,开发者提交的压力大。

CR 要多快?

如果你并没有处于需要专注工作的时候,那么应该在 review 完后尽快修改。回复最长的极限是一个工作日。

速度 vs 中断

我们可以在投入到处理他人给的review评论之前,找个适当的时机点来进行cr。这有可能是当你的当前开发任务完成后、午餐、刚从会议脱身或从微型厨房回来等等。

快速回应

个人回应评论的速度,比起让整个 cr 过程快速结束更重要。

 若在整个过程中能快速获得来自 reviewer 的回应,大大减轻开发者对缓慢 cr 过程的挫败感。

 reviewer员要花足够的时间来进行review,确保他们给出的LGTM,意味着“此代码符合我们的标准”。

 理想的个人的回应速度还是越快越好。