面试官老周推了推眼镜,镜片反光让人看不清眼神。
“简历上写熟悉JVM?”
小强挺了挺胸:“是的,三年一直在用。类加载、内存模型、GC,都熟。”
“好。”老周低头看了眼简历,“那说说类加载的双亲委派机制吧。”
小强心里一乐。这题他背过。
“就是当一个类需要加载时,先交给父类加载器。父类还能往上交,直到顶层的Bootstrap ClassLoader。父类能加载就返回,不能才往下走。这样避免核心类被篡改。”
老周没抬头:“还有吗?”
“呃……”小强脑子转了转,“保证了Java核心库的安全。比如String类,一定是启动类加载器加载的那个,不可能被自定义的String替换。”
老周靠在椅背上:“那Tomcat为什么要破坏双亲委派?”
小强笑容凝固了。
“Tomcat……它要隔离不同应用的类……”他舌头开始打结,“好像是每个应用有自己的类加载器,所以……所以不能完全按双亲委派来……”
“具体怎么实现的?”
“WebappClassLoader……”小强声音越来越小,“重写了……loadClass方法?”
老周面无表情:“你刚才说String不能被替换。那假如我自己写一个类叫java.lang.MyString,放rt.jar里,启动类加载器会加载吗?”
小强彻底蒙了。
大脑CPU此时占用率100%。内存区域一片空白。
“这个……不太清楚。”
老周合上笔记本:“回去等通知吧。”
小强走出会议室,走廊里空调吹得后脊发凉。
回到公司,小强掏出手机,给老汪发消息。
“汪哥,救命。茶水间。”
老汪端着枸杞保温杯,慢悠悠走进来。
“咋了,脸都绿了。”
小强把刚才的问题噼里啪啦复述一遍,最后补了一句:“我背了那么多八股文,他问的一个都没考。”
老汪啜了口枸杞水,笑了。
“你这不叫背八股文,你那是背了目录。”
“先说你答上来的那部分。双亲委派,你讲的没错,但那是课本第一页。面试官问Tomcat,是在考你有没有真正理解类加载器的加载机制,而不是死记硬背那三层结构。”
小强挠头:“那Tomcat到底为啥要破坏?”
老汪把保温杯往桌上一搁。
“这玩意儿说白了就是——你小区门口那个快递架。”
“双亲委派,就是小区规定,所有快递必须先送到物业。物业能处理的物业处理,处理不了再通知你自己拿。目的是什么呢?防止你偷偷买违禁品。”
“但Tomcat是个合租公寓。一栋楼里住了十户,每户都买自己的东西。如果所有快递还是先送物业,物业一看,好家伙,十户都买了同款拖鞋,但尺码不一样。物业只留一双,另外九双直接扔了。那九户人不得疯?”
“所以Tomcat的做法是,每个租户门口放个自己的快递架。快递来了,先看你这个租户的架子上有没有。没有再往物业送。”
“这就是WebappClassLoader重写loadClass的逻辑——先自己找,找不到再交给父类。加载顺序反过来了。”
小强眼睛亮了:“所以他能隔离不同应用的jar包版本。”
“对。”老汪点头,“应用A用Spring 5,应用B用Spring 6,各找各的,互不影响。双亲委派做不到这个。”
“那面试官最后那个问题呢?”小强追问,“自己写个java.lang.MyString放rt.jar里?”
老汪又喝了一口水。
“这个问题更狠。他是在问你对类加载底层机制的理解深度。”
“你回忆一下,loadClass方法的源码。双亲委派的代码写在哪儿?”
小强皱眉想了半天:“在ClassLoader的loadClass方法里,有个findLoadedClass先查缓存,然后parent.loadClass往上交……”
“对。那你想想,Bootstrap ClassLoader是C++实现的,它loadClass的时候,会先去rt.jar里找。但找的不是所有类,而是它内部有一份预定义的合法类名列表。”
“这份列表在哪儿呢?”
小强摇头。
“在jvm.cpp里。启动类加载器只认这个列表里的类名。你写的java.lang.MyString根本不在列表里。所以它压根不会加载,直接抛ClassNotFoundException。不是双亲委派机制挡住的,是启动类加载器自己的一层白名单校验。”
“这道题考的不是双亲委派,是你有没有看过源码。”
小强张了张嘴,这一刻他想起了所有背过的面试题。每一道都像浮在水面的油花,看着亮晶晶的,一戳就散。
“老汪,那我到底该看啥?”
老汪从兜里掏出一张皱巴巴的便签纸,写了几个字推过来。
“三件事。第一,看ClassLoader.loadClass的源码,重点看findLoadedClass和findClass两个方法被调用的时机。第二,看Tomcat的WebappClassLoader,它重写loadClass的那段逻辑,不到一百行,读三遍。第三——”
他顿了顿。
“下载个OpenJDK 8的源码,搜classLoader.cpp,看bootstrap class loader的初始化列表。看看java.lang.String是怎么被注册进去的。”
“这三样看完,下次再有人问你类加载,你嘴里说出来的每一句话,都带着源码的味道。”
小强把便签揣兜里,使劲点头。
老汪站起来,拍了拍他肩膀。
“另外我跟你说个血泪教训。我们去年线上有个故障,排查了三天三夜。表象是CPU飙到800%,所有线程都在跑GC。”
“跟类加载有关?”
“根因是一个第三方jar包里用ServiceLoader加载SPI实现。每来一个请求就new一个URLClassLoader去加载驱动类。类加载器没关,Metaspace里类定义越堆越多。最后Full GC疯狂回收但回收不掉,因为每个类加载器还被引用着。”
“JVM参数配了-XX:MaxMetaspaceSize吗?”
“配了512M。爆了。日志里全是OutOfMemoryError: Metaspace。”
老汪眼神突然认真起来。
“面试官问底层,不是在为难你。他是在筛选谁能解决这种线上问题,谁只会重启。”
小强沉默了。
茶水间的饮水机咕咚响了一声,像一颗石头落进深井里。