引言
JVM:Java 虚拟机(Java Virtual Machine)
虽然 JVM 说是一个虚拟机,但本质上,更像是一个解释器。
VMware、Virtual Box 算得上是真正的虚拟机,它们 100% 地模拟出了真实的硬件,而 JVM 只是对硬件设备进行了简单抽象的封装,从而能够达到跨平台效果。
而我们平时所说 Java 具有 " 可移植性 " / " 跨平台性 ",说的其实不是 Java 本身,而是 JVM 能够事先跨平台。我们平时写的 Java 程序 不是直接在电脑上运行的,而是在 JVM 上进行的,每个系统平台都是有自己的 JVM 虚拟机。所以,只要 JVM 能够正常运作,我们写的代码就能够在任何地方运行。
JDK、JRE、JVM
JDK:Java 开发工具包( Java Developer’s Kit )
JRE:Java 运行环境( Java Runtime Environment )
JVM:Java 虚拟机( Java Virtual Machine )
因为我们编写的 Java 代码最终是在 JVM 上运行的,所以才屏蔽了 " 操作系统的差异性 "。 然而,光有 JVM 自己,什么也干不了,你需要给它提供生产原料 " .class文件 "。仅仅是 JVM,是无法完成一次编译,处处运行的。它需要一个基本的类库,比如怎么操作文件、怎么连接网络等等。JVM 标准加上一大堆基础类库,就组成了 Java 的运行时环境,也就是我们常说的 JRE. 然而,JDK 就更为庞大了,它还提供了一些非常好用的小工具,比如 " javac、java、jar " 等等…
而它们三者,实际上构成了包含与被包含的关系。
声明
本篇博客主要基于下面三个要点,来展开说明 JVM 的面试题。
JVM 的内存布局
类加载机制 ( 面试题考的只是类加载中的一个小小的细节 )
垃圾回收机制
一、JVM 的内存布局
JVM 的内存被划分成了几个区域,如图所示:
看到这幅图,不要被吓到,这就是一份简单的布局图而已。
我们可以联想大学宿舍楼的分布,一部分区域是学计算机专业的,一部分区域是学英语专业的,还有一部分区域是学艺术专业的…
所以,上面的这幅图就是对于 Java 变量、Java 对象…按照一种规则进行排布。
关于这幅图我们需要注意四点,如下:
注意1
堆和方法区是在整个内存中,是唯一的一份;
而栈和程序计数器,每个独立的线程都有一份自己的。
注意2
了解四个主要的区域:( 栈、堆、方法区、程序计数器 )
(1) Stack:栈
栈里面放的是局部变量。
上图的 JVM Stack 表示 Java 虚拟机栈,Native Method Stack 表示本地方法栈。两者的思想差不多,不必过多区分。只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的。
(2) Heap:堆
堆里面放的是我们平时 new 出来的对象。
(3) Method Area,即方法区,里面放的是类对象。
我们需要知道,平时我们写的 Java 代码,实际上是一个 " .java " 这样的文件,最后都会被编译成 " .class " 这样的文件,里面放的是 二进制字节码,更详细地说,这些字节码就是一些指令。接着,JVM 就会将 " .class " 文件加载到内存中。
经过上面的这一系列的转换过程,最终才构成了类对象。而类对象,实际上就是与 static 关键字有关。
那么,类对象中一般有什么呢?
① 包含了这个类的各种属性的名字、类型、访问权限…
② 包含了这个类的各种方法的名字、参数类型、返回值类型、访问权限…
③ 包含了这个类的 static 成员与方法
(4) PC Register:Program Counter Register.
程序计数器,是内存区域中最小的一个部分,里面只是放了一个内存地址。这个地址的含义,就是 JVM 接下来要执行的指令地址。在上面,我们说 " .class " 文件中,实际上放的都是一些指令,那么,指令都有自己的地址。
注意3
① 局部变量放在栈区
② 成员变量放在堆区
③ 被 static 修饰的变量,即静态变量,放在方法区,有且只有一份
注意4
和内存区域相关的异常,主要有两个:
(1) HeapDumpOnOutOfMemoryError ( 堆溢出 )
典型的情况:堆空间耗尽,平时在代码中,不断地 new 对象,而后又没有及时释放,这就导致了此异常。
(2) StackOverFlow ( 栈溢出 )
典型地情况:进行递归操作时,没有控制好边界条件,从而导致了无限递归,也就引入了此异常。
理解代码 ( 常见面试题 )
class Person { public String name; public int age; } public class Student { Person person1 = new Person(); public int classID; public static int studentID; public static void main(String[] args) { Person person2 = new Person(); } }
(1) 栈区
// 【局部变量 person2 这个引用】 指向了 【new 出来的 Person 对象】 Person person2 = new Person();
" person2 " 在 main 方法内,属于局部变量,所以被放在 栈区。
" new Person() " 创建出来了一个 Person 对象,被放在 堆区。
(2) 堆区
// 【成员变量 person1 这个引用】 指向了 【new 出来的 Person 对象】 Person person1 = new Person(); // 成员变量 public int classID;
" person1 " 是类 Student 的成员变量," person1 " 若想被使用,只能通过外面的类 new 一个 Student 类。所以 " person1 " 被放在 堆区。
" new Person() " 创建出来了一个 Person 对象,所以也被放在 堆区。
而 " classID " 同样是类 Student 的成员变量,所以也被放在 堆区。
(3) 方法区
// 静态的成员变量 public static int studentID;
" studentID " 被 static 关键字修饰,所以是 一个静态成员变量,被放在 方法区,整个程序中," studentID " 只有独立一份,它不依赖对象,由 Student 类直接控制。
总结 ( 重要 )
一个变量存在于 JVM 中内存的哪个部分,不取决于它是整型、字符串类型、还是引用类型…
它存在哪个位置只取决于变量的形态。
① 局部变量放在栈区
② 成员变量放在堆区
③ 被 static 修饰的变量,即静态变量,放在方法区,有且只有一份
此外,我们应该理解 " person1 "与 " new Person() " 两者的关系,前者就表示一个引用类型的变量,而后者才是 Person 对象的本体。
而引用类型,我们只需要将其理解为一个 " 低配指针 " 即可。
二、类加载机制
问题1:类加载过程
上图描述了整个类的生命周期,我们当前只讨论整个 " 类加载的过程 ",也就是前五步,因为到第六步 " 使用 ",实际上 类 已经加载完了。
① 加载 => ② 验证 => ③ 准备 => ④ 解析 => ⑤ 初始化
1. 加载
首先,我们需要明确:" 加载阶段 " 是 " 整个类加载过程 " 中的一个阶段,两者不能混淆。
加载阶段做的事情:找到 " .class " 文件,并读取文件,此时就把这些数据已经读到内存里了。
2. 验证
验证阶段做的事情:验证刚才读到的内容是不是一个合法的 " .class 文件 ",假设,我们随便创建一个文件,再将后缀名设为 " .class ",显然,这是不能通过验证的。
那么怎么样才是一个 合法的 " .class 文件 " 呢 ?
首先,我们平时写的 Java 代码,是放在一个 " .java " 文件中的,若通过编译器进行编译后,才会生成一个 " .class 文件 ",这才是一个标准的流程,也即是 " .class " 文件真正的来源。
其次,要确保 " .class文件 " 的字节流中的信息,符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
3. 准备
准备阶段做的事情:正式为类中定义的变量 ( 即静态变量,被 static 修饰的变量 ) 分配内存并设置类变量初始值的阶段。
例如:下面的这一行代码:
public static int value = 123;
在准备阶段,它为 value 的值设置为 " 0 ",而不是 " 123 ".
4. 解析
解析阶段做的事情:JVM 针对字符串常量,进行一些处理。
5. 初始化
初始化阶段做的事情:真正地对静态变量进行初始化,同时也会执行 static 代码块。
例如:刚刚上面的代码:
public static int value = 123;
在初始化阶段,它真正地为 value 的值设置为 " 123 ".
注意
我们应该能够知道,static 修饰变量初始化以及 static 代码块的执行,是在对象的实例化之前的,它不依赖于外部 new 一个对象。
请看下面代码:
class A { public A() { System.out.println("A 的构造方法"); } static { System.out.println("A static"); } } class B extends A { public B() { System.out.println("B 的构造方法"); } static { System.out.println("B static"); } } public class Test { public static void main(String[] args) { B b = new B(); } }
打印结果:
上面的代码,让我们明白了 " 静态先行,先父后子 ".
问题2:双亲委派模型
双亲委派模型,出现在我们上面说的五个类加载阶段中的第一个阶段 " 加载 "。它的作用主要是利用类加载器之间优先级,来查目录;通过优先级的规则,来约定先查哪个目录,再查哪个目录。思想如下:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此,所有的加载请求,最终都应该传送到最顶层的启动类加载器中。只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去完成加载。
这里的 " 双亲 " 和 二叉树的 " 双亲节点 " 是同一个意思,实际上并不是双亲 parents,而是 parent,表示 " 父亲或母亲 "。那么,我们平时的默认叫法,一般就是 " 双亲 "。
三个类加载器
① 在类加载的过程中,先从 AppClassLoader 类加载器 开始,但 AppClassLoader 并不会立即去扫描自己负责的目录,而是先找他的父类加载器 ExtClassLoader,这时,ExtClassLoader 也不是立即去扫描自己负责的路径,而是先找他的父类加载器
BootStrapLoader. 由于 BootStrapLoader 已经是罪上的一级了,所以最终,BootStrapLoader 就会去扫描自己负责的目录。
② 如果最顶级的类加载器没找到目录,就逐级往下递减,让子类加载器也按照同样的规则进行查找,直至优先级最低的类加载器。如果最后一级还是未找到对应目录,就会抛出 ClassNotFoundException 整个异常。
如下图所示:
双亲委派模型的优点
避免重复加载类:例如, A 类和 B 类都有一个父类 C 类,那么当 A 类启动时,就会将 C 类加载起来,那么 B 类进行加载时,就不需要重复加载 C 类了。
安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的类,就可能会出现一些问题。
例如:若我们通过代码编写一个为 java.lang.Object 的类,之后在程序运行的时候,系统就会出现多个不同的 Object 类,因此安全性就得不到保证了。
三、垃圾回收机制
问:垃圾回收,回收的到底是什么?
答:回收的是内存。
针对 Java 来说,JVM 实际上是一个进程,那么,它就会有一个进程该有的特点。
我们知道,一个进程在运行的过程中,会持有很多的硬件资源,例如:CPU,内存,硬盘,带宽资源…然而,不论系统内存多大,内存总会有上限。那么,一个程序在使用内存的时候,必须得先申请才能使用,用完之后,还要记得释放。
所以,一般来说,先申请、接着使用、最后释放,是一个程序在使用内存前后的基本步骤。申请和使用都是常见现象,而释放却有不同的方式。
问:为什么要对内存进行释放?不释放可以吗?
答:不可以,当内存满了的时候,其他进程就无法运行了。
例如:每天我们出门可能穿不同的衣服,如果今天的日子结束了,回到家,将今天的衣服摆在沙发上,明天也这么做,长此以往,沙发就会摆满我们自己的衣服。这就显得凌乱不堪,最重要的是,如果我们不收拾沙发,有一天,沙发堆满了,我们连找个休息的地方都没有了。
1. 释放内存的方式
那么一般情况下,释放内存的方式分为两种。
1. 今天下班了,将衣服叠好,自己放回衣柜。
这就表示手动回收内存,谁申请的就谁释放,C 语言就是这么做的。程序员需要手动 " malloc " 申请内存,手动 " free " 释放内存。
2. 今天下班了,将衣服甩在沙发上,明天也是甩在沙发上。但有一天,你的爱人看不下去了,就将衣服统一收拾收拾,放回衣柜。
对于垃圾回收机制来说,谁申请都行,有一个统一的人来负责释放。Java 就是这么做的,程序员在代码中的任何地方,都可以申请内存,然后由 JVM 统一进行释放,具体来说,就是由 JVM 内部的一组专门负责垃圾回收的线程来进行这样的工作。
2. 垃圾回收机制的优缺点
垃圾回收机制的优点:
- 对于计算机来说,能够非常好地保证不出现内存泄露的情况,不是 100% 保证,但对于大部分场景来说,就能达到不错的效果。
- 对于程序员来说,能够节省很多时间和精力。因为我们知道,C 语言就是需要程序员自己手动释放内存的,如果在一个项目中,程序员忘记释放内存了,可能不仅仅是一个地方出错,这就很难调试,需要花费大量的时间和精力去找代码漏洞。
垃圾回收机制的缺点:
- 需要消耗额外的系统资源来实现此机制
- 内存的释放在一些场景下可能存在延时,很多地方,并不是说当前内存不用了,立即就被释放,而大多数情况下,都是在一定时间后,才会被释放。
3. Java 的垃圾回收到底回收的是哪些区域的内存?
在上面,我们提到,JVM 中的内存主要有( 堆、栈、方法区、程序计数器 ),那么垃圾回收是对这四个区域的内存都进行操作吗?
答案是否定的。
针对栈区和程序计数器,它们的内存都是和具体的线程绑定在一起的,这块的东西都是自动释放的,当线程结束后,也就意味着,内存被自动释放了。
针对方法区,我们知道里面主要装的是 " 类对象 " 和 " 静态变量 ",而这两者都是通过整个 " 类加载 " 的过程产生的,所以方法区较为特殊,它同样有 " 类卸载 " 的过程,那么,我们就暂不考虑此情况。
针对堆区,我们知道里面主要装的是 " 成员变量 ",这也是我们较为常用,也较为麻烦的内容区域,此外,这四个区域中,堆占据的内存空间就是最大的,那么综上所述,垃圾回收机制,针对的就是堆区的数据内存。
所以,我们日常所说的 " 垃圾回收 " 主要指的就是堆上的内存。
4. 回收堆上的内存,具体来说是什么?
参考平时我们写的 Java 代码,堆上的数据,基本都是我们 new 出来的对象,而后放的是一些对象的 " 成员属性 "。
所以,Java 中的垃圾回收,更加具体地来说,就是以 " 对象 " 为单位的。一个对象存在在内存当中,要么被回收,要么不被回收,不会出现一个对象被回收一半,而另一半还在占用着内存资源的情况。
5. 垃圾回收是以什么样的方式实现的?
垃圾回收的基本思想就是:先找出垃圾 + 再根据具体策略回收垃圾。
问:为什么要先找出垃圾,难道不能一锅端吗?
必须明确:相比于回收少了来说,回收多了或回收错了,是更严重的问题。
对于 GC 机制来说,( 宁可放过,也不能错杀 )。这很好理解,在网络交互的过程中,一个对象可能并不是连续被程序使用的,如果一个对象在后续的地方需要继续执行一些重要的操作,但中途被回收了,这就会直接导致结果数据出错!
所以,我们应该明白,垃圾回收机制首先就是要确定一个对象对象在后续的程序中,不再被需要,才能够对其进行回收操作。
6. 如何找到垃圾?
(1) 引用计数法 ( 了解 )
Person person1 = new Person(); Person person2 = person2;
对于 new Person() 这个对象,我们可以看到,上面的代码,有两个引用指向它,分别是 person1 和 person2. 那么此时计数的数值为 2.
person1 = null; person2 = null;
对于上面的代码,若我们先将 person1 置为 null,那么计数的数值变为 1,之后,我们又将 person2 置为 null,那么计数的数值就变为 0.
当计数的数值为 0 是,new Person() 这个对象就被认为是垃圾了,就即将被 JVM 回收了。类似于 Java 这样的语言,引用是访问对象的唯一途径,如果一个对象没有引用了,就可以认为这个对象在代码中,再也无法被使用了,因此就可以通过引用是否存在,来判定对象的生死。
现在,我们就能很明白 " 引用计数法 " 的思想了,当一个对象,多一个引用指向它,那么,对于它的引用计数就 +1,若少了一个引用,对于它的引用计数就 -1,直到为 0 的时刻,此方法就默认对象是垃圾了。
引用计数法的缺点:
① 空间利用率较低
如果我们对一个较大的对象进行引用计数,这是没问题的。比方说:一个对象有几个 M,而放一个类似于 int 类型的变量作为计数器,没什么负担。
而对于一个较小的对象来说,就会有较大的空间开销。比方说:一个对象本身的大小也就 4 个字节左右,难道还能采取这种方法吗?肯定是不能的了。
然而,在我们平时编写代码的时候,你不可能控制使用对象的使用次数,更不可能控制对象的大小。
② 存在循环引用的问题
上面举的代码例子,是一个较为简单的思想。然而,在一些特殊的代码下,例如一个对象被多个引用变量进行引用,引用计数就会出现错误。
因为像这样的计数方式,就是靠数值来判断是否要进行回收,只要数值出现一点错误,就会直接导致结果出错。比方说:本应该最后的值为 0,需要被回收的,但因为此方法的计算错误,导致最后的值为 1,这就无法回收。
但是,在 Java 中,JVM 并不是按照引用计数来寻找垃圾的,因为,Java 是完全面向对象的语言,所以,其中用到的引用特别多,所以很难避免循环引用的问题。
然而,例如 Python,PHP 这样的语言,采用的就是引用计数的这种方式,但是它们的 GC 机制是通过一定的手段,将引用计数的方式进行了改变。
(2) 可达性分析 ( 掌握 )
在 Java 世界,JVM 中的 GC 机制是按照可达性分析这样的方式进行判定垃圾的。
可达性分析的核心思想为:通过一系列称为 " GC Roots " 的对象作为起始点,从这些节点开始向下搜索,寻找过的路径称之为 " 引用链 ",如果从一个 " GC Roots " 到一个对象不可达时,证明此对象就是一个待回收的垃圾。
如下图所示:对象 Object5,Object6,Object7 之间虽然彼此有关联,但它们到并不存在于 " GC Roots " 这条引用链,也就是说,从根节点往下,顺藤摸瓜,找不到的对象,就被判定为可回收对象,即一个待回收的垃圾。
这其实和二叉树从根节点往下遍历的思想很相似。
GC 机制按照可达性的方式,实际上也是通过引用来进行判断一个对象究竟是否要被当作垃圾回收,但是,相比于引用计数法,它有效解决了循环引用的问题。毕竟,可达性分析,并不只是搜寻一次,它可以重复搜寻,如果第一轮遍历未搜寻到垃圾,可能就在第二轮、第三轮就找到了垃圾。
然而,事情有好就有坏,显然,它的整个遍历过程需要更大的开销。
7. Java 的引用类型 ( 了解 )
不管是引用计数,还是可达性分析,其判定原则都是依据对象是否被引用来指向了。所以,本质上,GC 机制 就是在通过引用,来决定对象的生死。然而,引用诞生的初衷,只是为了 " 访问对象 ",但随着时代的发展和开发人员的贡献,引用也被用来判定对象的生死了。
在 Java 的世界中,存在多种引用,有的引用可以判定对象的生死,有的则不能。下面就是我介绍的四种引用。事先说明,后三种引用都只是在 JVM 内部使用的,日常开发极少用到,所以,我们做到了解即可。
① 强引用,强引用就是我们日常编写代码使用的引用,它既能访问对象,也能决定对象的生死。
② 软引用,能够问对象,但只能在一定程度上决定对象的生死。
③ 弱引用,能访问对象,但不能决定对象的生死。
④ 虚引用,既不能访问对象,也不能决定对象的生死。
8. 垃圾回收算法
上面介绍到,在 JVM 中,已经通过 " 可达性分析 " 将待回收对象找出来了,现在通过什么样的策略来将垃圾进行回收呢?
① 标记-清除算法
" 标记-清除 " 算法 是最基础的收集算法,算法分为 " 标记 " 和 " 清除 " 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
如下图所示:
通过上图,我们发现 " 标记-清除 " 算法有两个不足的地方:
① 效率问题:标记和清除的两个过程的效率都不高。
② 空间问题:标记清除后,堆区就会产生大量不连续的内存碎片,如果空间碎片太多,可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
例如:为一个数组 new 一块内存空间,由于数组的内存空间是连续的,假设是一段很长的区域,这时,通过此算法,就会导致分配内存失败。而导致失败的主要原因,就是内存碎片太多,存活对象和未使用的内存区域冗杂在一起。
② 复制算法
" 复制 " 算法是为了解决 " 标记-清理 " 的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况。
如下图所示:
虽然复制算法能够有效解决内存碎片的问题,但通过上图,我们依旧能发现复制算法的缺点:
① 可用的内存空间只有一半。
② 如果要回收的死亡对象较少,剩下的存活对象较多,复制的开销就很大了。复制算法,更适用于对象被快速回收,且整体内存不大的场景下。
③ 标记-整理算法
" 标记-整理 " 与 " 标记-清除 " 运行过程一致,但后续步骤并不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。
如下图所示:
我们可以将 " 标记-整体 " 算法的思想理解为,顺序表删除中间位置的元素时的搬运操作,后一个位置,不断地往前覆盖。
但事情有好就有坏,我们都知道,若顺序表的申请内存的区域越大,那么删除中间位置元素,所进行覆盖的效率就越低。所以,这里 " 标记- 整理 " 算法的运行过程也是一样的,搬运过程并不是一个很高校的过程。
④ 分代算法
分代算法与上面的 三种算法 不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。这就好比中国的一国两制方针一样,对于不同的情况和地域设置更符合当地的规则,从而实现更好的管理,这就是分代算法的设计思想。
回收内存,更具体地说,是回收对象,那么分代算法就是按照对象的 " 年龄 " 来对整个内存进行分类的。把 " 年龄小的对象 " 放在一起," 年龄大的对象 " 放在一起,之后,对不同年龄段的对象所处的不同区域,就可以采取不同的垃圾回收算法来进行处理了。
所以,最终我们决定,把 " 年龄小的对象 " 放在新生代," 年龄大的对象 " 放在老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们应该采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,我们就应该采用 " 标记-清理 " 或 " 标记-整理 " 算法。
如下图所示:
理解分代回收的过程:
① 一个新的对象,诞生于伊甸区。
② 如果一个对象,经历了一轮 GC 依旧存活下来了,就拷贝到生存区。
③ 在生存区中,对象也要经历若干轮 GC,每一轮 GC 存活的对象,都要通过复制算法拷贝到另外一个生存区里。所以说,两个生存区的对象来回执行拷贝的过程,每一轮都会淘汰掉一波对象。
④ 在生存区中,当一个对象通过一定轮次的 GC 之后,仍然存活着,JVM 就认为此对象,在未来中,还会更持久地存在下去,于是就将此对象放置在老年代。
⑤ 进入老年代的对象,JVM 就认为它们是属于能持久存在的对象,而这些对象也需要使用GC 来扫描,但是扫描的频次就大大降低了。
我们可以将整个分代回收的过程想象成面试的过程,公司面试可能不止一轮,一般都是 2-3 轮,然而,每一次,都会刷下来一部分人,GC 机制就相当于面试官。在新生代的人,不管处于伊甸区还是生存区,总是需要进行大量的淘汰。而成功上岸成为正式员工的人,就进入了老年区,但可能需要按照以 " 年 " 为单位,来进行考核,不合格的员工,依然需要被淘汰,这就是一个 " 优化 " 的过程。
总结面试题 ( 重要 )
- 谈一谈 JVM 的内存布局。
- 谈一谈 JVM 中,类加载的过程。
- 谈一谈 JVM 中的双亲委派模型。
- 谈一谈 " 可达性分析 " 是用来干什么的?它的思想是什么?
- 谈一谈 分代回收 中 的" 新生代 " 和 " 老年代 " 。
- 谈一谈 JVM 的 GC 机制。
谈到 Java 中的 JVM 的 GC 机制时,首先就可以从 " 可达性分析 " 入手;不要提到 " 引用计数 " 这个方法,因为 Java 与它毫无关联。其次,就可以谈到三个算法的思想;最后,把分代算法的原理讲清楚即可。