没准备好就不要冲动

以下只针对Java岗。来源主要是牛客的Java实习面经。下面的回答直接背就可以,需要一定的Java和Jvm基础,适合春招实习的同学,但是我会在每个问题下把有助于理解的博客贴出来。如果发现有问题欢迎私聊我或留言我会在下面更新

关于Java虚拟机,需要知道Jvm的内存结构,垃圾回收机制,内存分配策略,类的加载机制以及内存模型 (并发) 。以下是面试常考的问题,有时候还会问到一些Jvm的分析工具

内存结构

虚拟机栈引用的对象,方法区类静态属性引用的对象,方法区常量引用的对象

三七互娱19年春招

1. 简单谈谈JVM内存结构

华为19年社招,京东19年秋招本科

对于JDK1.6来说,Jvm的内存结构主要分为五部分,其中的堆和方法区*(对于HotSpot来说由永久代实现,包括常量池)*是线程共享的,虚拟机栈,本地方法栈和程序计数器是线程独有的;永久代在堆中

对于JDK1.7来说,Jvm的内存结构主要分为五部分,其中的堆*(包括常量池)和方法区(对于HotSpot来说由永久代实现)*是线程共享的,虚拟机栈,本地方法栈和程序计数器是线程独有的;五部分都在运行时数据区

对于JDK1.8来说,Jvm的内存结构主要分为五部分,其中的堆*(包括常量池)和方法区(对于HotSpot来说由元空间MetaSpace实现)*是线程共享的,虚拟机栈,本地方法栈和程序计数器是线程独有的;MetaSpace在直接内存中,不受Jvm堆的控制,其他四部分还在运行时数据区

有一点需要特殊注意,TLAB在堆中,分配时是线程独有的,使用时是线程共享的

  • 废除永久代的原因主要是:1. 其他虚拟机不存在永久代概念*(官方准备融合 JRockit VM )*;2. 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。同时可以加载的类会更多

  • 程序计数器:如果是native方法,则计数器值为空;如果正在执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址;Jvm规范中唯一没有规定任何OutOfMemoryError异常情况的区域;线程上下文交换时记录保存在程序计数器中;选取要执行的指令,跳转,循环等的执行

  • 虚拟机栈:描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。每个方法执行(不是创建)时都会创建一个栈帧(栈帧和栈不是一回事,栈帧只是栈的一段区域,有栈顶和栈底)。栈帧中存有局部变量表,操作数栈,动态链接,方法出口等信息,通过return和异常可以退出栈帧;该区域可能出现两种异常情况:StackOverflowError(线程请求的栈的深度大于虚拟机允许的)和OutOfMemoryError(虚拟机栈扩展时无法申请到足够的内存)*(-Xss:)*

  • 本地方法栈:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一

  • 堆:用来存放实例,是Jvm最大的一块区域,也是GC管理的主要区域。在JDK7及以前,分为新生代*( Eden 8、From Survivor 1、To Survivor 1 ),老年代和永久代(即方法区)。新生代与老年代是1:2;Java堆既可以是固定大小,也可以是可扩展的(通过-Xmx:256G和-Xms:256G实现。前者最大值,后者最小值。还有一个命令参数是-XX:+HeapDumpOnOutOfMemoryError,当发生该异常是dump堆快照)*,无法扩展时,抛出OutOfMemoryError

  • 方法区,7及之前是永久代,之后是元空间。 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;抛出OutOfMemoryError

    • 常用参数:

      1
      2
      -XX:PermSize=N //方法区 (永久代) 初始大小
      -XX:MaxPermSize=N //方法区 (永久代) 最大大小 java.lang.OutOfMemoryError: PermGen
      1
      2
      -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
      -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小 java.lang.OutOfMemoryError: MetaSpace
  • 运行时常量池,6及以前在方法区中,之后再堆中。用于存放的编译期间形成的各种字面量和符号引用,在编译期形成,如声明为final的常量值等符号引用,类和接口的完全限定名等;当常量池无法申请内存时会抛出OutOfMemoryError

  • 直接内存,这个不在Jvm的堆管理中,MetaSpace,Java中的nio,buffer都在直接内存中。通过-XX:MaxDirectMemorySize

2. 什么东西分配在堆上和栈上

猪场19年实习

JDK1.7及以前,堆中有方法区和运行时常量池。包括实例,常量,类信息

JDK1.8及以后,堆中有运行时常量池。有实例和常量,没有类信息

3. 说一下内存结构,和内存模型有什么联系呢,为什么要分代

阿里19年秋招本科

内存结构是对于Jvm来说的,内存模型是Java Memory Model,针对于Java并发编程的

对于第二个问题来说,HotSpot将Jvm堆分为新生代和老年代,主要是用了分代回收算法。根据研究表明,大量对象都是”朝生夕死”的,根据这个特性,我们在新生代存储可能“朝生夕死”的对象,在老年代中存储存活时间比较长的对象,这样可以针对不同的存活时间进而选择更高效率的回收算法

在新生代中,因为有98%的对象朝生夕死,我们只需要很少的空间就可以进行复制算法,非常划算。在老年代中, 对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集

为什么Eden区不用标记-清除算法

三七互娱19年春招

因为新生代频繁创建对象

4. 说一下OutOfMemoryError

阿里19年秋招本科

OutOfMemoryError一般出现在堆和方法区,也会出现在运行时常量池中。一般情况下出现在堆中,可能是堆内存不够,需要调用-Xmx?G/M来增加堆的大小。如果频繁出现这种情况*jstat -gcutil pid 1000*,说明极有可能出现了内存泄露情况,此时需要使用参数-XX:HeapDumpOnOutOfMemoryErrordump堆内存的快照,然后借助工具进行排查。下面部分会详细说

类加载

5. 类加载详细说一下

阿里19年秋招本科

Class 文件需要加载到虚拟机中之后才能运行和使用,类加载过程主要分为三步:加载,连接*(验证,准备,解析)*,初始化

  • 加载:查找并加载类的二进制数据*(网络,jar包等)*。将类的.class文件中的二进制数据读入内存中,将其放在方法区中,然后在内存中创建一个java.lang.Class对象(Hotspot将其放入方法区中)用来封装类在方法区的数据结构
  • 连接:将类与类的关系(符号引用转为直接引用)确定好,校验字节码
    • 验证:校验类的正确性(文件格式,语义,字节码,二进制兼容性)
    • 准备:为类的静态变量分配内存,将其初始化为默认值。但是在到达初始化之前,类变量都没有初始化为真正的初始值
    • 解析:把类的符号引用转为直接引用*(类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用 )*
  • 初始化:初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()方法的过程。所有Java虚拟机实现必须在每个类或接口被Java程序首次主动使用才初始化,但类加载不一定,静态代码块在类初始化时执行
    • 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时
    • 使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化
    • 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
    • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类
    • 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化

6. 类加载机制

滴滴19年秋招本科,京东19年秋招本科

这里不是指类加载几个过程,类加载主要采用了双亲委派模型。

每一个类都有自己的类加载器, 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。
$$
AppClassLoader \rightarrow ExtensionClassLoader \rightarrow BootstrapClassLoader
$$
双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改

如果多个类加载器加载同一个类,会出现什么情况

首先要先看这几个类加载器是否遵循双亲委派原则,如果遵循,那么对于Object类来说,只会有BootstrapClassLoader来加载。

如果这个类不能被三个内置类加载器加载,多个类加载器加载同一个类时,加载出来的类是不一样的

7. 如何验证版本冲突

滴滴19年秋招本科

class文件中,紧接着魔数的四个字节存储的是 Class 文件的版本号:第五和第六是次版本号,第七和第八是主版本号。高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件

8. 类加载器自己写过没

猪场19年实习

这里可以自己写一个类加载器,同时也可以说Tomcat中的类加载器

如果是自己实现类加载器,那么只需要覆盖ClassLoader这个抽象类就行。自定义类加载器,可以自定义类的查找来源,自定义加密,热部署等等。

对于Tomcat的类加载器

  1. 使用bootstrap引导类加载器加载
  2. 使用system系统类加载器加载
  3. 使用应用类加载器在WEB-INF/classes中加载
  4. 使用应用类加载器在WEB-INF/lib中加载
  5. 使用common类加载器在CATALINA_HOME/lib中加载

垃圾回收

9. 讲一下GC

字节跳动19秋招,阿里19年秋招本科,华为19年社招,京东19年秋招本科

这个题也可以理解为垃圾回收策略。

当Jvm判断对象不可达并且经过两次标记之后,就会通过GC算法进行回收。在新生代主要是复制算法,在老年代主要是标记复制和标记清楚算法。常见的垃圾收集器包括Serial,ParNew,parallel-scavenge,Serial Old,ParNew Old,CMS,G1

垃圾回收

GC参数

打印GC日志是-XX:+PrintGCDetails

高吞吐量的话使用CMS算法

10. 内存分配策略

华为19年社招

  1. 如果JIT的逃逸移出分析后该对象没有逃逸,那么可能优化到栈上分配。否则对象主要分配到Eden区上,如果启动了TLAB,则分配到TLAB中。
  2. 之后如果对象在Eden出生,并且经过第一次Minor GC仍然存活并能被Survivor容纳的话,将对象年邻设为1*(每经过一次Minor GC,age++)*,当年邻到15时,直接进入老年代。可以设置-XX:MaxTenuringThreshold来设置对象年龄阈值
  3. 除了2之外,如果Survivor中相同年龄所有对象的大小总和大于Survivor的一半,那么年龄大于该年龄的对象就直接到老年代
  4. 大对象直接进入老年代,譬如很长的字符串和数组,避免为大对象分配内存时由于分配担保机制带来的复制而降低效率 。可以设置-XX:PretenureSizeThreshold,令大于该尺寸的对象直接进入老年代

11. Full GC什么时候触发

华为19年社招

Full GC是针对整个新生代、老生代、元空间的全局范围的GC。

触发full GC最主要的原因就是空间不足,有:创建大对象进入老年代时,如果老年代内存不足,则触发Full GC;常量池和元空间内存不足时,也会触发full GC;从新生代进入老年代时内存不足,触发Full GC;显式调用System.gc()

12. 标记清除 & 复制算法的原理

三七互娱19年春招

  • 标记清除分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:效率问题空间问题(标记清除后会产生大量不连续的碎片)
  • 复制算法为了解决效率问题,它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。一般在新生代中使用

13. 标记清除&标记整理算法的区别

bigo19年秋招本科

标记清除是先标记再清除,容易产生大量碎片。标记整理算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

14. 垃圾回收机制的优劣

三七互娱19年春招

不用手动管理垃圾回收,但是也造成了无法对垃圾进行管控

15. 可达性分析算法原理

三七互娱19年春招

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

什么可以作为GC-roots的引用链

GC Roots的对象包括:static,final,native和局部变量所引用的对象

16. 只对堆进行gc吗,调用System.gc()马上就gc吗

猪场19年实习

GC不止是对堆,还是直接内存。

调用System.gc()只是提醒Jvm执行GC,而不是一定就能GC

17. 说一下一个对象在内存里面的生存周期

阿里19年秋招本科

  • 对于对象创建来说,一共分为5步。
    1. 首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
    2. 分配内存。有两种方式,指针碰撞*(适合堆内存规整的,对应Serial,ParNew GC,标记整理算法,复制算法)和空闲列表(适合堆内存不规整,对应CMS GC,标记清除算法)*;分配内存解决并发有两种手段,一个是CAS+失败重试,一个是Thread Local Allocation Buffer(私有线程堆上分配)
    3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用
    4. 设置对象头。 该实例所对应的类、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄,轻量级锁等等信息
    5. 调用init方法,初始化对象。如按照程序员意愿进行赋值
  • 对于对象分配来说
    1. 如果JIT的逃逸移出分析后该对象没有逃逸,那么可能优化到栈上分配。否则对象主要分配到Eden区上,如果启动了TLAB,则分配到TLAB中。
    2. 之后如果对象在Eden出生,并且经过第一次Minor GC仍然存活并能被Survivor容纳的话,将对象年邻设为1*(每经过一次Minor GC,age++)*,当年邻到15时,直接进入老年代。可以设置-XX:MaxTenuringThreshold来设置对象年龄阈值
    3. 除了2之外,如果Survivor中相同年龄所有对象的大小总和大于Survivor的一般,那么年龄大于该年龄的对象就直接到老年代
    4. 大对象直接进入老年代,譬如很长的字符串和数组,避免为大对象分配内存时由于分配担保机制带来的复制而降低效率 。可以设置-XX:PretenureSizeThreshold,令大于该尺寸的对象直接进入老年代
  • 对于垃圾回收来说,如果判断不可达并经过两次标记之后,则通过特定算法进行回收

18. 说一下四种引用状态

阿里19年秋招本科

JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

  • 强引用:必需对象。new Object(),GC宁愿抛出OutOfMemory也不会回收
  • 软引用:有用但非必需。 如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。可以使用SoftReference
  • 弱引用:非必需,只能生存到下次垃圾回收之前。通过WeakReference
  • 虚引用:幽灵引用/幻影引用:无法通过虚引用获得一个对象实例。唯一目的是在GC回收前可以收到系统通知。通过RhantomReference虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收

19. 怎么判断对象是否存活

pdd19年秋招本科

有两种算法,分别是引用计数法和可达性分析算法

  • 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
  • 可达性分析算法: 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。GC Roots的对象包括:static,final,native和局部变量所引用的对象

对象是否存活,需要两次标记,第一次标记通过可达性分析算法。如果没有GC Roots相连接的引用链,那么将第一次标记。如果对象的finalize()方法被覆盖并且没有执行过,则放在F-Queue队列中等待执行*(不一定一定执行)*,如果一段时间后该队列的finalize()方法被执行且和GC Roots关联,则移出“即将回收”集合。如果仍然没有关联,则进行第二次标记,进行回收

类和常量的废弃

  • 假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池
  • 类需要同时满足下面 3 个条件才能算是 “无用的类”
    1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    2. 加载该类的 ClassLoader 已经被回收
    3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

20. 分代收集算法

bigo19年秋招本科

  • 是套组合拳,新生代分为Eden区和Survivor From和To区,用复制算法,老年代用标记-整理、标记-清除*(碎片化)*
  • 标记-清除:标记,然后清除,缺点:效率低,产生空间碎片
  • 复制算法:开辟两个空间,一块用,一块不用,清除的时候把一块用的,存活的放入另外一个空的里面,缺点是当对象存活率高的时候复制效率低。对于新生代来说,每次使用Eden和Survivor From
  • 标记-整理:解决标记清除的碎片化问题,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

什么参数能够调整新生代的比例

  • -XX:SurvivorRatio:Eden和Survivor的比值,默认8,代表8:1
  • -Xmn: 设置新生代的大小
  • -XX:NewRatio:老年代和新生代内存大小的比例,默认2,代表2:1 (阿里云的天机设置的是1)

工具与调优

21. JVM性能调优的监控工具了解那些

华为19年社招

  • jps (JVM Process Status): 类似 UNIX 的 ps 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
  • **jstat**( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
  • jinfo (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息;
  • jmap (Memory Map for Java) :生成堆转储快照;
  • jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
  • jstack (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
  • JConsole: 监视本地及远程服务器的 java 进程的内存使用情况。堆,栈和检查死锁
  • Visual VM

常见的Jvm工具

22. 如何解决内存泄露

三七互娱19年春招,pdd19年秋招本科,bigo19年秋招本科

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:

  • 这些对象是可达的,即在有向图中,存在通路可以与其相连
  • 其次,这些对象是无用的,即程序以后不会再使用这些对象。

如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。久而久之,就会导致VM不断Full GC,但是却没有什么用

  1. 首先通过jps / ps -ef | grep java来找到要查看的java pid
  2. 然后通过jstat -gcutil pid 1000来打印Full GC的频率来确定是否发生Full GC
  3. 通过jmap先分析下存活的对象jmap -histo:live pid,如果明显某个实例数比较多,则基本锁定是该实例内存泄露。如果找不出来,可以打印堆文件jmap -dump:live,format=b,file=heap.bin pid
  4. 通过jhat,Jconsole,Visual VM,MAT,btrace 进行堆分析,查看泄露代码

23. jstat的参数

滴滴19年秋招本科

  • jstat -class vmid :显示 ClassLoader 的相关信息;
  • jstat -compiler vmid :显示 JIT 编译的相关信息;
  • jstat -gc vmid :显示与 GC 相关的堆信息;
  • jstat -gccapacity vmid :显示各个代的容量及使用情况;
  • jstat -gcnew vmid :显示新生代信息;
  • jstat -gcnewcapcacity vmid :显示新生代大小与使用情况;
  • jstat -gcold vmid :显示老年代和永久代的信息;
  • jstat -gcoldcapacity vmid :显示老年代的大小;
  • jstat -gcpermcapacity vmid :显示永久代大小;
  • jstat -gcutil vmid :显示垃圾收集信息;

另外,加上 -t参数可以在输出信息上加一个 Timestamp 列,显示程序的运行时间

比如看一个线程的回收情况,怎么看、其他的命令呢

24. 发现频繁Full GC怎么去排查和调整

字节跳动19年本科,bigo19年秋招本科

  • 用一个指令去参考Full GC的次数,新生代老年代的比例,调整各比例
  • 产生Full GC的原因可能是:新生代到老年代的对象,老年代的空间不足,才产生Full GC

还会其他JVM命令吗

此处可以说下其他的新能调优工具如jstat,jamp,jps等等

也可以说Jvm启动时的参数,如-XX:newRatio -XX:SurvivorRatio :XX:+SerialGC -Xms: -Xmn 等等

JDK8:Parallel Scavenger + Parallel Old

高吞吐量的话用哪种gc算法:Parallel Scavenger

滴滴19年秋招本科

如何去验证版本的冲突的,比如jdk1.6到jdk1.7

初始化去怎么做的

实际运用中,ClassLoader都有了解什么

如何实现一个动态加载、实现哪些方法

实现ClassLoader这个类

首先加载这个字节码的字符流,然后有个loader方法吧,记不太清楚了

如何获取当前的ClassLoader

getClassLoader方法

那可以保持建立多个ClassLoader吗?

给他扯到了不同ClassLoader加载一个clas出来的类不一样

为什么加载器加载出来的类不一样

不同加载器加载出来的类不一样,因为加载器不同啊

面试官补充:因为每个加载器都有自己的隔离机制

京东19年秋招本科

ClassLoader的分类