面试官:你说你熟悉JVM,那来聊聊Classloader吧

6 阅读5分钟

面试官老周推了推眼镜,镜片反光让人看不清眼神。

“简历上写熟悉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。”

老汪眼神突然认真起来。

“面试官问底层,不是在为难你。他是在筛选谁能解决这种线上问题,谁只会重启。”

小强沉默了。

茶水间的饮水机咕咚响了一声,像一颗石头落进深井里。