前言:
前面系列文章让我们对 JVM 及垃圾回收相关的知识有了一个基本的了解,JVM 的知识比较偏概念,当然也有一些底层的源码可以去研读,但多数来说我们了解了 JVM 的原理即可,本篇我们将基于前面分享的 JVM 相关的原理知识,提取一些 JVM 中场景的面试题,希望可以帮助到有需要的朋友。
JVM 系列文章传送门
Java 中的对象一定在堆上分配内存吗?
不一定,在 HotSpot 虚拟机中,存在 JIT 优化的机制,JIT 优化中可能会进行逃逸分析,经过逃逸分析后发现某一个局部对象没有逃逸到线程和方法外的话,那么这个对象可能不会在堆上分配内存,可能会在栈上分配内存。
什么是逃逸分析?
逃逸分析是 JVM 的一种内存优化策略,JVM 通过逃逸分析来确定该对象会不会被外部访问,如果不会被外部访问,也就是只会在线程内方法内,就可能将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随着栈帧出栈而销毁,可以减轻 GC 的压力,JDK 7 已经默认开启逃逸分析。
什么是标量替换?
要想搞清楚什么是标量替换就要先搞清楚什么是标量,标量是不可以进一步被拆解的量(数据),而 JAVA 基本数据类型就是标量,相对标量还有一个叫做聚合量的概念,可以进一步被分解的量(数据)就是聚合量,Java 中我们我们创建的 DO 对象就是聚合量 。
在 JIT 编译阶段,经过逃逸分析发现一个变量不会被外界访问的话,经过 JIT 优化,就会把这个对象拆解成若干个成员变量来代替,这个过程就是标量替换,标量替换为栈上分配提供了很好的基础。
什么是栈上分配?
我们知道对象和数组的内存分配是发生在堆上,那什么是栈上分配呢?栈上分配就是对经过逃逸分析,认为该对象不会被外部访问,对于这类对象就可以在栈上分配内存,而无需去堆上创建,这样可以降低 GC 的压力,因此栈内存在方法结束的时候就释放了。
Java 堆中对象是如何晋升的?
我们知道 Java 堆中分为年轻代和老年代,那么对象是如何从年轻代晋升到老年代呢?对象一定是在年轻代产生的吗?
正常情况下对象是在年轻代产生的,创建一个对象时,会先在 Eden 分配对象,当 Eden 内存不足的时候,会触发新生代的 GC,也叫轻 GC(Young GC、Minor GC),新生代 GC 后没有被回收的对象会被移动到 Survivor 区域,这个过程对象叫做晋升。
对象会从 Eden 区域晋升到 Survivor 区域,同样也会从 Survivor 区域晋升到老年代,对象晋升到老年代主要看对象的大小和对象的年纪,Java 堆中对象晋升到老年代只需要满足下面三个条件中的一个即可,如下:
- 经历过 15次 Minor GC,对象每经历过一次 Minor GC,对象的年龄就会加 1,对象经历过 15次 Minor GC 后,JVM 就会认为该对象是常用的对象,就会让其晋升到老年代,15次是 JDK8 默认的,可以自己设置对象晋升到老年代的年龄,设置 -XX:MaxTenuringThreshold = 8,表示对象经过 8次 Minor GC 后进入老年代。
- 对象动态年龄判断,当 Survivor 区域中小于某个年龄的对象占用的内存达到整个 Survivor 区域的一半的时候,就会把大于这个年龄的对象全部晋升到老年代,这是因为年轻代一般都是使用的标记-复制垃圾回收算法,我们知道 Survivor 区域其实还分为 Survivor1 和 Survivor2 区域。
- 大对象直接进入老年代,我们可以设置对象大于某个值的时候,直接让该对象进入老年代,可以通过参数 -XX:PretenureSizeThreshold 来设置,-XX:PretenureSizeThreshold 参数默认是 0,也就是对象默认在新生代产生。
分析了对象晋升到老年代的条件后,我们就可以明确的回答对象不一定是在年轻代产生的,也可能是直接在老年代创建的。
什么是 JVM 的空间分配担保机制?
JVM 的空间分配担保机制是对 JVM 堆中老年代的担保,我们知道 JVM 堆分为年轻代和老年代,年轻代的 Minor GC 可能会导致对象晋升到老年代,而 JVM 空间分配担保机制就是担保的这个环节,因为 Minor GC 会导致对象晋升到老年代,但是在发生 Minor GC 的时候并不确定此次晋升到老年代的对象有多少,此时 JVM 会检查老年代的连续可用内存空间是否大于年轻代的对象之和,如果大于则此次 MinorGC 是安全的,如果小于则 JVM 会查看 HandlePromotionFailure 设置值是否允许担保失败,如果 HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次 Minor GC,但这次 Minor GC 依然是有风险的,如果小于或者 HandlePromotionFailure=false,则改为进行一次 Full GC,这就是内存 JVM 的空间分配担保机制,简单来说就是在进行 Minor GC 的时候,担心老年代空间不足,是否考虑直接触发 Full GC。
为什么 JVM 堆中有一个 Eden 区域,却有两个 Survivor 区域?
我们知道 JVM 堆中有一个 Eden 区域但是有两个 Survivor 区域,它们的内存空间占用比默认是:8 :1 :1,堆中有一个 Eden 区域两个 Survivor 区域和年轻代使用的垃圾回收算法有关系,我们知道年轻代是用的是标记-复制算法,而标记-复制算法的前提就是要两块区域,每次只使用其中的一块,这个就和 Survivor 的两个区域吻合了,假设只有一个 Eden 区域和一个 Survivor 区域我们看看会出现什么问题。
我们知道对象是在 Eden 区域创建的,年轻代进行 Minor GC 的时候,使用标记-复制算法,我们模拟创建对象如下,第一次 Minor GC 情况如下:
可以看到第一次进行 Minor GC 的时候是没有问题的,因为此时 Survivor 区域是空的,可以把存活的对象移动到 Survivor 区域,我们接着创建对象看第二次 Minor GC。
继续模拟创建对象,此时 Eden 区域和 Survivor 区域空间占用如下:
第二次 Minor GC 情况如下:
我们发现如果使用标记-复制算法第二次 Minor GC 无法进行了,因为标记复制算法需要一款空着的内存空间,而此时 Eden 和 Survivor 都有存活的对象,无法满足标记-复制算法了。
有人可能会说我们第二次创建对象的时候再 Survivor 区域创建,这样就不至于 Eden 和 Survivor 区域都会有对象存在了,如果这样的话 Eden 区域和 Survivor 都要承担对象分配工作,都需要较大的内存空间,这样新生代的空间只能被利用一半,内存空间使用率很低。
JVM 中触发 Minor GC 和 Full GC 的条件?
Minor GC 的触发条件非常简单,Eden 区域内存空间不足,无法创建新的对象的时候就会出发 Minor GC。
Full GC 的触发条件比较多,如下:
- 老年代空间不足:Minor GC 后,有对象要晋升到老年代,此时老年代空间也不足,触发 Full GC。大对象直接在老年代创建的时候,发现老年代内存不足,直接触发 Full GC。
- 永久代空间不足:永久代默认空间是 21MB,如果永久代空间不足,也会触发 Full GC。
- 代码中显示调用 System.gc(),这种方式也会触发 Full GC,但是不会马上出发 Full GC。
JVM 新生代和老年代的垃圾回收算法选择?
新生代一般选择标记-复制算法,老年代选择标记-整理算法。
新生代选择垃圾回收算法的时候主要考虑了两个问题,一个是效率问题,一个是内存碎片问题,因此新生代发生 Minor GC 的频率很高,因此要选择一个效率较高的垃圾回收算法,对象基本都是在新生代创建的因此需要大量的连续内存空间,因此最好选择一个没有内存随便的回收算法,标记-复制算法的效率很高,但是会浪费一半的空间,但是 JVM 采用合理的设计,使用了 Survivor From 和 Survivor To 区域的设计降低了内存浪费的问题,因此新生代一般选择标记-复制算法。
老年代存活的都是年达比较久的对象或者是大对象,最好不要有空间浪费,因此通常来说会选择标记-整理算法,虽然效率低,但是避免了空间浪费和空间碎片的问题,通知老年代发生 Full GC 的频率也不高,也有老年代的垃圾收集器选择了标记-清除算法,例如 CMS 垃圾收集器,CMS 垃圾收集器选择标记清除算法是为了降低 STW(Stop The World)。
JVM 如何保证对象分配内存过程中的线程安全?
要想知道 JVM 分配对象过程的线程安全问题,我们先了解 JVM 对象分配的三种情况,如下:
- 对象在栈上分配,前面提到过逃逸分析,如果一个对象没有逃逸,会直接在栈上分配内存的,这种情况下不会有线程安全问题。
- 对象在新生代分配,对象在新生代分配的时候要区分 JVM 是否启动了 TLAB(Thread Local Allocation Buffer,TLAB 简单来说就是一块独立的内存空间),启动了 TLAB 拥有了独立的内存空间,对象在不同的内存空间中,自然不存在线程安全问题,如果没有启动 TLAB,这种情况多个对象可能分配到同一个内存空间,会存在线程安全问题,JVM 对于这种情况采用的是 CAS + 失败重试的方式来解决线程安全问题。
- 对象在老年代分配,同样是采用 CAS + 重试的方式保证线程安全,简单来说获取到锁的线程才可以分配对象内存,没有获取到锁的线程获取当前堆中的最新标识,然后等待重试。
什么是 TLAB?
上面在聊对象分配内存过程中的线程安全问题时候提到了 TLAB,TLAB 是 JVM 虚拟机在 Eden 区域为每个线程划分的一块专属区域,启动了 TLAB 了后,JVM 虚拟机会给每个线程分配一块独立的内存空间,只供当前内存使用,如此以来多个线程之间就不存在资源竞争了,可以提升内存分配效率。
需要注意的是 TLAB 只是在内存分配的时候是各个线程独享的,而在读取和垃圾回收的时候都是共享的,TLAB 机制并不会影响对象的读取和垃圾回收,简单来说 TLAB 区域的对象,其他线程一样可以读取,垃圾回收的时候一样可以进行移动。
TLAB 存在的问题:
任何一门技术引入都可能带来一些其他的影响,TLAB 也不例外,TLAB 机制存在内存浪费的问题,我们举例如下:
假设一个线程的 TLAB 空间是 100KB,我们使用了 80KB,此时我们想分配一个 25KB 的对象,明显内存空间不够用了,那怎么办呢?
- 直接在堆内存中分配对象。
- 重新为线程申请 TLAB。
这两个方案都有不足的地方,直接在堆内存中分配对象,存在明显的内存浪费,重新申请新的 TLAB 也会增加资源消耗,因为 TLAB 的申请过程是并发进行的,如果频繁进行 TLAB 申请,将会给 JVM 带来不小的负担,这样就违背了 TLAB 的初衷。
为了解决这个问题,JVM 又给出了一个新的方案,这个方案也很简单粗暴,就是允许浪费的空间,JVM 定义了一个 refill_waste 的值,这个值就是允许浪费的空间,比如这个值设置为 30KB,上面案例中申请的对象是 25KB,此时会废弃当前的 TLAB,重新申请新的 TLAB,如果申请的对象内存是 35KB,则直接在堆内存中分配,这中做法会降低频繁重新申请 TLAB 带来的开销。
JVM 堆内存一定是共享的吗?
我们常说堆内存是共享的,其实这个说法不够严谨,上面我们提到了 TLAB 机制,JVM 开始了 TLAB 机制后,JVM 会给每个线程分配一块独享的内存空间,只给当前线程使用,从这个角度来说内存是独占的,当然我们上面也说了 TLAB 机制也仅仅是在内存分配的时候独享,在对象读取和 JVM 垃圾回收来看还是堆内存共享的,只能说单纯的说 JVM 堆内存是共享的是不够严谨的。.
JVM 创建对象的过程了解吗?
JVM 创建对象的步骤大致如下:
- 检查 JVM 指令参数是否可以在常量池中定位到来这个类的符号引用,并检查这个符号引用的类是否被加载过,如果沒有加载则需要执行类加载过程。
- 为对象分配内存空间,HotSpot 虚拟机给对象分配内存有两种方式,分别是指针碰撞和空闲列表法,JVM 解决内存分配的并发问题有两种方式,我们上面提到过的 TLAB 机制和 CAS + 重试的机制。
- 给分配到的内存空间赋初始化值,例如 int 赋值为 0,引用类型初始化为 null,初始化赋值的目的是确保对象在创建的时候都有默认值。
- 设置对象头,包括该实例对应的类、元数据信息、对象的哈希码、GC 分代年龄、锁信息等。
- 调用该类的构造方法,初始化该对象,完成赋值。
- 返回对象引用,对象完成创建之后,返回对象的引用。
JVM 的指针碰撞和空闲列表算法了解吗?
我们在聊 JVM 创建对象的过程中提到了 JVM 给对象分配地址的时候一般会使用指针碰撞和空闲列表算法,那什么是指针碰撞和空闲列表算法呢?
- 指针碰撞算法:通过一个指针将 JVM 内存分为已经使用的内存空间和空闲的内存空间,给新的对象分配内存空间的时候,先计算出对象需要的内存空间,然后将指正移动到对象所需要的内存空间的位置,然后将指针移动的起始位置作为对象的起始地址,由此可见指针碰撞法适合堆内存完整的区域,没有浮动内存碎片的情况,一般对应这标记-整理、标记-复制垃圾回收算法,对应的垃圾收集器则是 Serial、ParNew 等垃圾收集器。
- 空闲列表算法:见名知意,就是把内存空间整理成列表,JVM 会维护这个列表,记录这个列表中哪里内存已经使用哪里内存还空闲在,新的对象在申请内存的时候,只需要找到一块可以容纳下这个对象的内存空间即可,空闲列表算法内存分配高效,可以灵活的使用内存碎片,适合于内存不连续的情况,对应了标记-清除算法的垃圾回收算法,对应了 CMS 垃圾收集器。
JVM 的字符创常量池了解吗?
字符串常量池是 JVM 中的一块共享的内存区域,用于存储字符串常量的(因为常量不可再变,因此字符串常量池可以共享),当我们编写的 Java 代码中出现常量时,JVM 会把这个常量存在在字符串常量池中,如果字符串常量池已经存在相同的字符串常量,JVM 会直接引用已经存在的字符串常量,而不会再次创建新的常量。
字符串常量池也是经过一些历史变更迭代的,在 JDK1.6 之前,字符串常量池存在于永久代(元数据区)中,我们知道永久代中存储了类信息、静态变量、方法接口信息等,从 JDK1.7 开始,字符串常量池从永久代(元数据区)中移动到堆内存中,这么做的主要目的是字符串常量大多是朝生夕死的,而永久代(元数据区)的 GC 频率很低,只有发生 Full GC 的时候才会被回收,将字符串常量池移动到堆内存中后,是为了及时的回收字符串常量,释放是内存空间,提高内存空间利用率。
JVM 为什么要分堆内存和栈内存?
JVM 区分堆内存和栈内存也是从 Java 语言自身的触发点来考虑的,也是从整体的内存管理效率来考虑的,我们首先搞清楚堆和栈的区别。
堆是用来存放对象的,而且堆内存是多个线程共享的,而栈是线程独享的一块区域,用于方法调用和局部变量的存储,栈内存可以随着方法调用结束而释放,不需要进行垃圾回收,试想一下如果把栈内存放到堆中是不是就增加了垃圾回收的压力,就会降低整个 JVM 系统的西能,反之就可以提升整体的性能,其次堆和栈的分离也有利于做到隔离效果,首相可以做到数据隔离,可以把线程独有的局部变量放到栈上,而共享的数据就可以放到堆上进行统一管理。
什么是堆外内存?
堆外内存就是堆外内存,没错就是堆外内存,是一个相对于 JVM 堆内存提出的一个概念,堆外内存就是 JVM 堆之外的一块内存空间,是操作系统内存的一部分,也由操作系统来管理,堆外内存对于大规模数据存储和访问来说,使用堆外内存可以有更好的性能,因为堆外内存无需进行复杂的垃圾回收,以及垃圾回收产生的内存碎片问题,同时也因为堆外内存没有垃圾回收机制,没有人给我们兜底,我们在使用堆外内存的时候要格外小心,使用完了要记得及时释放内存,否则可能造成内存泄漏等情况,堆外内存不受 JVM 限制,但是其一样受操作系统的限制,如果操作系统没有足够的内存供分配,同样也会报出 OutOfMemoryError. 的错误。
在 Java 中使用堆外内存的方式有两种,分别是借助 UnSafe 类和 NIO 技术。
- UnSafe 类:了解 Java CAS 的都知道 UnSafe 类是实现 CAS 的核心类,因为 Java 语言无法直接访问底层操作系统,一般都是通过 Native 方法来访问,不过尽管如此,JDK 中还是提供了一个 UnSafe 类,它提供了硬件级别的原子操作,在 UnSafe 类的众多能力中就有一项能力是直接操作堆外内存,Unsafe 类中提供了allocateMemory 方法来分配堆外内存,提供了 freeMemory 方法来释放堆外内存。
- Nio:NIO 中引入了 ByteBuffer 来处理堆外内存,使用 ByteBuffer 类的 allocateDirect() 方法可以创建一个 ByteBuffer 实例,使用其 put() 方法可以将数据写到堆外内存,使用其 get() 方法可以从堆外读取内存,因为堆外内存没有垃圾回收机制,因此使用的时候要注意,使用完了的时候要调用 clean 方法来释放堆外内存。
内存泄漏和内存溢出有什么区别?
内存泄漏是因,内存溢出是果。
- 内存泄漏:内存泄漏是指不再使用的对象没有得到回收,久而久之的大量占用空间,导致 JVM 堆可用空间日渐减少,最终导致的结果就是内存溢出(OutOfMemory),内存泄漏常见于对象或数据结构杯创建后,引用没有被及时的释放引用,导致垃圾回收器无法对其进行回收,从而产生内存泄漏。
- 内存溢出:内存溢出是指视图分配一个超过可用内存空间的空间的情况,这会直接导致程序奔溃,报出 OutOfMemory 错误。
多久进行一次 FullGC 正常?
这个问题没有标准答案,不要回答的太离谱即可,比如每小时都发生 Full GC,这肯定是不正常的,当然 Full GC 更多个因素有关系,比如业务的性质、堆内存的大小等,总而言之 Full GC 的频率应该相对较低才对,Full GC 的耗时也不应该太长了,一般一秒钟以内,Full GC 的频率最好也不要超过一天一次。
什么情况下 JVM 会退出?
- 程序正常执行完成,当 Java 应用程序中所有非守护线程都执行完成,JVM 会退出,这是最正常的退出方式。
- System.exit() 方法被调用,Java 程序中任何一个位置调用该方法,JVM 程序会立即开始终止过程,该方法接受一个状态码,0 表示正常退出,非 0 表示异常退出。
- Runtime.getRuntime().halt() 方法被调用,该方法是强制终止当前运行的 JVM 虚拟机,不会做其他任何处理。
- 遇到无法恢复的错误,比如操作系统的错误、JVM 自身的 Bug。
- 接受到终止的信号,比如 Linux 系统的 Kill 命令。
需要注意我们常见的 OOM 以及一些 Error 并不会让 JVM 退出运行。
JVM 垃圾回收中的并行回收和并发回收的区别?
首先普及一下并行和并发的概念,并行是指多个 CPU 同时执行不同的任务,并发是指一个 CPU 通过时间片的不停切换来执行不同的任务。
- 并行回收:并行回收关注的是吞吐量,吞吐量是指代码运行时间占整个时间的比重,因此并行回收为了尽可能的让吞吐量提高,在进行垃圾回收的时候,会考虑把用户线程全部停止,也就是 STW,让全部线程来进行垃圾回收,用最多的资源争取在最短的时间内完成垃圾回收,以提高吞吐量,像 Parallel Scavenge、Parallel Old、ParNew 都是并行并行回收的垃圾收集器。
- 并发回收:并发回收关注的是 STW,也就是用户线程暂停的时间,因此并发它运行用户线程和垃圾回收线程同时进行工作,以缩短用户线程停止的时间,代表的垃圾收集器有 CMS、G1 垃圾收集器。
什么是跨代引用,会有什么问题?
我们知道 JVM 堆内存中分为年轻代和老年代,跨代是指引用跨代,比如老年代引用了年轻代的对象。
跨代引用会增加 GC 的复杂度,比如一个对象在年轻代已经是不可用的,本来在进行 Minor GC 的时候是可以回收的,但再深究一下发现该对象被老年代的对应引用了,这个时候就不能进行回收了,这个发现的过程需要消耗资源, 那如何来实现这个发现的过程呢?
- 在 Minor GC 的时候,把老年代的所有对象也作为 GC ROOTS,进行可达性扫描。
- 在进行 Minor GC 的时候,如果发现年轻代的对象被老年代引用,就继续扫描,把所有涉及到的年轻代对象都进行标记。
- 增加一个数据结构来记录老年代对象到年轻代对象的引用,如果某个年轻代的对象被老年代对象引用,就记录下来,减少全部扫描的开销。
JVM 就是采用了记录老年代到年轻代对象引用的方案来解决跨代引用的问题的,具体到实现就是记忆集(RemerberdSet)和卡表(CardTable)。
什么是记忆集(RemerberdSet)和卡表(CardTable)?
记忆集(RemerberdSet):记忆集记录的是非垃圾收集区域指向垃圾收集区域的指针集合,如果不考虑效率和成本的话,可以用所有非垃圾收集区域的含跨代引用的对象数组来实现这个数据结构,很明显这样做的成本很高,我们需要有更高效更细粒度的实现方式,因此就有了卡表。
卡表:是一个字节数组,数组的每一个元素都对应着其标识的一块内存区域,这个内存区域被叫做卡页(Card Page),卡页的大小是 2的 N 次幂,卡页中记录的是内存区域的地址,一般来说一个卡页中不止一个对象,但卡表在实现的时候认为一个卡页里面只有有一个对象存在跨代引用,那就将对应的卡表元素的值标记为 1,称这个卡页为脏页(Dirty),不存在跨代引用的标识为 0,简单来说卡页只有两种状态 1 或者 0,在垃圾回收的时候,只需要把标记为 1 的脏页中的对象加入到 GC ROOTS 中一并扫描,就知道哪些是跨代引用了,不需要进行全部扫描了,节约了时间成本和空间成本。
我们把上面的描述说的更通俗一点,就是把老年代的区域划分为一个个小的内存区域块,每个内存区域块对应着卡表的元素,把存在跨代引用的卡页标记为脏页,JVM 进行垃圾回收的时候,将脏页的对象加入到 GC ROOTS 中一起扫描,这就相对高效的解决了跨代引用的问题。
卡表的卡页如何变脏?
卡表的脏页的对象是跨代引用的对象,那如何变脏呢?变脏的本质是出现了跨代引用,那变脏的时机就是引用,只需要在发生引用的时候标记为脏页即可,HotSpot 虚拟机是通过写屏障(Write Barrier)来实现的,写屏障可以看做 JVM 层面对引用类型字段赋值的 AOP 切面,在写之前叫做写前屏障,写之后叫做写后屏障,使用了写屏障之后,JVM 会为所有的引用赋值操作增加一个更新卡表的动作,每增加一个对象引用赋值就更新卡表肯定是有一定的开销的,但是这个开销相对还好。
卡表伪共享问题
卡表在高并发情况下会有伪共享(False Sharing)问题,伪共享是指并发情况下同一个缓存行更新的问题,并发情况下 A 的更新被 B 覆盖了,A 需要重新更新,面对这种情况,卡表采用了增加判断的方式来避免,不是采用无条件的写屏障,而是在使用写屏障之前判断该卡页是否标记过,如果已经标记过就不在标记,只有未标记过才将其标记为脏页,在JDK7 之后,HotSpot虚拟机增加了一个新的参数(-XX:+UseCondCardMark),该参数默认是关闭的,用来决定是否开启卡表更新的条件判断,因为开启这个判断也是有性能损耗的,可见 JVM 的开发者考虑的有多么的细致,我们可以根据自己的需求来选择是否打开这个判断。
打破双亲委派模式,可以重写 String 类吗?
答案是不可以,双亲委派模式相信大家不陌生,简单来说就是类加载的时候优先委托父类进行加载,一层一层往上寻找,只有所有父类的类加载器无法加载当前类的时候,才会由子类加载器来加载,双亲委派模式可以保护 Java 的核心类不会被篡改,而打破双亲委派模式则有利于增加第三方组件的能力,也却又一些场景需要这种模式,例如加载数据库连接,而重写 String 类显然不应该在打破双亲委派模式的范围中。
我们可以打破双亲委派模式,屏蔽 Bootstrap ClassLoader,但是无法重写 java. 包下面的类,这是为什么呢?
之所以无法重写 java. 包下面的类,原因我们就算是破坏双亲委派模型,依然需要调用父类中(java.lang.ClassLoader.java)的 defineClass() 方法来把字节流转换为一个JVM 识别的 class,而 defineClass() 方法中通过 preDefineClass() 方法限制了类全限定名不能以 java.开头,故不能重写 java. 包下面的类,如果非要重写 java. 包下面的类,除非你自己实现一个将字节流转换为 JVM 可识别的 class 的,那就可以绕过 defineClass() 中的校验全限定名的逻辑,也就可以重写 java.lang.String 类了。
持续更新ing,也欢迎大家评论区留言补充 JVM 常见问题。
如有不正确的地方欢迎各位指出纠正。
标签:面试题,对象,回收,GC,内存,JVM,线程,强烈推荐 From: https://blog.csdn.net/weixin_42118323/article/details/143694714