Java虚拟机(JVM)面试题和知识点整理(2021最新版)更新中ing

新生代

  • Eden区

  • Servivor From

  • Servivor To

新生代GC(MinorGc)

复制、清空、互换

  • eden、servivor from复制到servivor to区,对象年龄+1

  • 清空eden、servivor from区

  • servivor from区、servivor to区互换

老年代

永久代

Java6、7、8优化历程

Java8内存模型—永久代(PermGen)和元空间(Metaspace)

https://www.cnblogs.com/paddix/p/5309550.html

堆内存划分为: Eden、Survivor 和 Tenured/Old 空间

堆中垃圾回收示意图:

在这里插入图片描述

在这里插入图片描述

二、JVM线程模型(JMM)

=======================================================================

用于区分JVM内存结构

三、类加载机制

================================================================

类加载器


判断类是否“相等”

任意一个类,都由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。

因此,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

加载器种类

系统提供了 3 种类加载器:

  • 启动类加载器(Bootstrap ClassLoader): 负责将存放在 <JAVA_HOME>\lib 目录中的,并且能被虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。

  • 扩展类加载器(Extension ClassLoader): 负责加载 <JAVA_HOME>\lib\ext 目录中的所有类库,开发者可以直接使用扩展类加载器。

  • 应用程序类加载器(Application ClassLoader): 由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称它为“系统类加载器”。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

在这里插入图片描述

当然,如果有必要,还可以加入自己定义的类加载器。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

双亲委派模型


双亲委派模式的工作原理

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

即每个儿子都不愿意干活,每次有活就丢给父亲去干(父亲给他的父亲,即爷爷),直到父亲说这件事我也干不了时,儿子自己想办法去完成。

优点

双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行

保证了程序的安全性。例子:比如我们重新写了一个String类,加载的时候并不会去加载到我们自己写的String类,因为当请求上到最高层的时候,启动类加载器发现自己能够加载String类,因此就不会加载到我们自己写的String类了。

双亲委派模式

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。这也就可以解释为什么无法加载一个自定义的java.lang.String类!!!

双亲委派机制:

1、当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3、如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。

ClassLoader源码分析:

protected synchronized Class<?> loadClass(String name, boolean resolve)

throws ClassNotFoundException{

// 首先判断该类型是否已经被加载

Class c = findLoadedClass(name);

if (c == null) {

//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载

try {

if (parent != null) {

//如果存在父类加载器,就委派给父类加载器加载,false表示加载的类不会初始化

c = parent.loadClass(name, false);

} else {

//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native // Class findBootstrapClass(String name)

c = findBootstrapClass0(name);

}

} catch (ClassNotFoundException e) {

// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能

c = findClass(name);

}

}

if (resolve) {

resolveClass©;

}

return c;

}

双亲委派模型意义:

  • 系统类防止内存中出现多份同样的字节码

  • 保证Java程序安全稳定运行

Class.forName()和ClassLoader.loadClass()区别

类加载有三种方式:

  • java命令行启动应用时候由JVM初始化加载

  • 通过Class.forName()方法动态加载

  • 通过ClassLoader.loadClass()方法动态加载

Class<?> loadClass(String name)

Class<?> loadClass(String name, boolean resolve)

我们看到上面两个方法声明,第二个方法的第二个参数用于设置加载类的时候是否链接该类(上文中装载类文件的第二步),true链接,否则就不链接。Class类的forName方法则相反,使用forName加载时会将Class进行解析和初始化。

例如:JDBC DRIVER的加载,我们在加载JDBC驱动的时候都是使用的forName而非是ClassLoader的loadClass方法呢?我们知道,JDBC驱动是通过DriverManager,必须在DriverManager中注册,如果驱动类没有被初始化,则不能注册到DriverManager中,因此必须使用forName而不是用LoadClass。

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承 ClassLoader类,并重写 findClass 方法

四、JIT即时编译

==================================================================

JIT即时编译是什么及其作用?

Java程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。

于是后来在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。

五、四种引用类型

=================================================================

1、强引用


在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

2、软引用


软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

3、弱引用


弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

4、虚引用


虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

3 Java四种引用类型分别是什么?及存活时间


  • 强引用:程序代码普遍存在的,类似"Object obj=new Object()";只要强引用还存在,GC永远不会回收被引用的对象;

  • 软引用:描述还有用但并非必需的对象;直到内存空间不够时(抛出OutOfMemoryError之前),才会被垃圾回收;最常用于实现对内存敏感的缓存;SoftReference类实现;

  • 弱引用:用来描述非必需对象;只能生存到下一次垃圾回收之前,无论内存是否足够;WeakReference类实现;

  • 虚引用:完全不会对其生存时间构成影响;唯一目的就是能在这个对象被回收时收到一个系统通知;PhantomRenference类实现;

4 Java四种引用使用场景


  • 强引用-FinalReference

地球人都知道,但是我讲不出来;

  • 软引用-SoftReference

创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。PS:图片编辑器,视频编辑器之类的软件可以使用这种思路。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用-WeakReference

Java源码中的java.util.WeakHashMap中的key就是使用弱引用,一旦不需要某个引用,JVM会自动处理它,这样就不需要做其它操作。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用-PhantomReference

主要用来跟踪对象被垃圾回收器回收的活动。虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效。

六、垃圾回收gc

=================================================================

程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而灭;栈帧随着方法的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

而对于 Java 堆和方法区,我们只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。

判定对象是否存活


若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。

引用计数法

在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。

引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为它很难解决对象之间循环引用的问题。

举个栗子 👉 对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它们互相引用着对方,导致它们的引用计数都不为 0,于是引用计数算法无法通知 GC 收集器回收它们。

可达性分析法

所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。

GC Roots 是指:

  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 本地方法栈中引用的对象

  • 方法区中常量引用的对象

  • 方法区中类静态属性引用的对象

GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。

回收堆中无效对象


对于可达性分析中不可达的对象,也并不是没有存活的可能。

判定 finalize() 是否有必要执行

JVM 会判断此对象是否有必要执行 finalize() 方法,如果对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么视为“没有必要执行”。那么对象基本上就真的被回收了。

如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize()方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。

对象重生或死亡

如果在执行 finalize() 方法时,将 this 赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。

任何一个对象的 finalize() 方法只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会被再次执行,想继续在 finalize() 中自救就失效了。

回收方法区内存


方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:

  • 废弃常量

  • 无用的类

判定废弃常量

只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。比如,一个字符串 “bingo” 进入了常量池,但是当前系统没有任何一个 String 对象引用常量池中的 “bingo” 常量,也没有其它地方引用这个字面量,必要的话,"bingo"常量会被清理出常量池。

判定无用的类

判定一个类是否是“无用的类”,条件较为苛刻。

  • 该类的所有对象都已经被清除

  • 加载该类的 ClassLoader 已经被回收

  • 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。

垃圾收集算法


学会了如何判定无效对象、无用类、废弃常量之后,剩余工作就是回收这些垃圾。常见的垃圾收集算法有以下几个:

标记-清除算法

标记的过程是:遍历所有的 GC Roots,然后将所有 GC Roots 可达的对象标记为存活的对象

清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。

这种方法有两个不足

  • 效率问题:标记和清除两个过程的效率都不高。

  • 空间问题:标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法(新生代)

为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。这种算法有优有劣:

  • 优点:不会有内存碎片的问题。

  • 缺点:内存缩小为原来的一半,浪费空间。

为了解决空间利用率问题,可以将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。

但是我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其他内存(指老年代)进行分配担保。

分配担保

为对象分配内存空间时,如果 Eden+Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入 Eden 区。

标记-整理算法(老年代)

标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots,然后将存活的对象标记。

整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。

这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。

分代收集算法

根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。

  • 新生代:复制算法

  • 老年代:标记-清除算法、标记-整理算法

4大算法


1、标记清除

优点:基于最基础的可达性分析算法,它是最基础的收集算法;而后续的收集算法都是基于这种思路并对其不足进行改进得到的;

缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量不连续的内存碎片;这会导致分配大内存对象时,无法找到足够的连续内存;从而需要提前触发另一次垃圾收集动作;

缺点:

2次扫描 一次标记 一次清除

有内存碎片

2、复制算法

优点

使得每次都是只对整个半区进行内存回收;内存分配时也不用考虑内存碎片等问题;实现简单,运行高效;

缺点

空间浪费;效率随对象存活率升高而变低;

HotSpot虚拟机复制算法过程

  • 将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;

  • 每次使用Eden和其中一块Survivor;

  • 当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;

  • 而后清理掉Eden和使用过的Survivor空间;

  • 后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;

默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;

分配担保机制

如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制(Handle Promotion)进入老年代;

3、标记压缩

优点:不会产生内存碎片;

缺点:增加了对存活对象需要整理的过程,效率更低;

4、分代收集算法

“分代收集”(Generational Collection)算法结合不同的收集算法处理不同区域。

新生代:每次垃圾收集都有大批对象死去,只有少量存活;所以可采用复制算法;

老年代:对象存活率高,没有额外的空间可以分配担保;使用"标记-清理"或"标记-整理"算法;

优点:根据各个年代的特点采用最适当的收集算法;

缺点:仍然不能控制每次垃圾收集的时间;

7大垃圾收集器


https://github.com/doocs/jvm/blob/main/docs/04-hotspot-gc.md

在这里插入图片描述

在这里插入图片描述

新生代收集器:Serial、ParNew、Parallel Scavenge;

老年代收集器:Serial Old、Parallel Old、CMS;

整堆收集器:G1;

CMS收集器

老年代、"标记-清除"算法(不进行压缩操作,产生内存碎片)、并发收集、低停顿

整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;

CMS收集器3个明显的缺点:

对CPU资源非常敏感;

无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败;

产生大量内存碎片;

https://blog.csdn.net/zqz_zqz/article/details/70568819

哪些情况下对象内存分配会直接进入老年代?

  • 当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);Minor GC时,如果发现存活的对象无法全部放入Survivor空间,只好通过分配担保机制提前转移到老年代。

  • 需要大量连续内存空间的Java大对象会直接进入老年代,容易提前触发老年代GC;

  • 经过多次Minor GC,如果年龄达到一定程度,就晋升到老年代;

  • 动态对象年龄判定:如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代;

方法区中可回收哪些对象?

  • 废弃常量:与回收Java堆中对象非常类似;

  • 无用的类:

  • (1)该类所有实例都已经被回收(即Java椎中不存在该类的任何实例);

  • (2)加载该类的ClassLoader已经被回收,也即通过引导程序加载器加载的类不能被回收;

  • (3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;

JDK HotSpot虚拟机方法区调整

在JDK7中,使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;

在JDK8中,永久代已被删除,类元数据(Class Metadata)存储空间直接在本地内存中分配;

七、HotSpot 虚拟机对象探秘

==========================================================================

对象的内存布局


在 HotSpot 虚拟机中,对象的内存布局分为以下 3 块区域:

  • 对象头(Header)

  • 实例数据(Instance Data)

  • 对齐填充(Padding)

对象头

对象头记录了对象在运行过程中所需要使用的一些数据:

  • 哈希码

  • GC 分代年龄

  • 锁状态标志

  • 线程持有的锁

  • 偏向线程 ID

  • 偏向时间戳

对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。

实例数据

实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。

对齐填充

用于确保对象的总长度为 8 字节的整数倍。

HotSpot VM 的自动内存管理系统要求对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对齐填充并不是必然存在,也没有特别的含义,它仅仅起着占位符的作用。

对象的创建过程


类加载检查

虚拟机在解析.class文件时,若遇到一条 new 指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。

为新生对象分配内存

对象所需内存的大小在类加载完成后便可完全确定,接下来从堆中划分一块对应大小的内存空间给新的对象。分配堆中内存有两种方式:

  • 指针碰撞

如果 Java 堆中内存绝对规整(说明采用的是“复制算法”或“标记整理法”),空闲内存和已使用内存中间放着一个指针作为分界点指示器,那么分配内存时只需要把指针向空闲内存挪动一段与对象大小一样的距离,这种分配方式称为“指针碰撞”。

  • 空闲列表

如果 Java 堆中内存并不规整,已使用的内存和空闲内存交错(说明采用的是标记-清除法,有碎片),此时没法简单进行指针碰撞, VM 必须维护一个列表,记录其中哪些内存块空闲可用。分配之时从空闲列表中找到一块足够大的内存空间划分给对象实例。这种方式称为“空闲列表”。

初始化

分配完内存后,为对象中的成员变量赋上初始值,设置对象头信息,调用对象的构造函数方法进行初始化。

至此,整个对象的创建过程就完成了。

对象的访问方式


所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配的。也就是说在建立一个对象时两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。 那么根据引用存放的地址类型的不同,对象有不同的访问方式。

句柄访问方式

堆中需要有一块叫做“句柄池”的内存空间,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

引用类型的变量存放的是该对象的句柄地址(reference)。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址找到对象。

在这里插入图片描述

直接指针访问方式

引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。

在这里插入图片描述

需要说明的是,HotSpot 采用第二种方式,即直接指针方式来访问对象,只需要一次寻址操作,所以在性能上比句柄访问方式快一倍。但像上面所说,它需要额外的策略来存储对象在方法区中类信息的地址。

内存分配与回收策略

==================================================================

对象的内存分配,就是在堆上分配(也可能经过 JIT 编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代的 Eden 区上,少数情况下可能直接分配在老年代,分配规则不固定,取决于当前使用的垃圾收集器组合以及相关的参数配置。

以下列举几条最普遍的内存分配规则,供大家学习。

对象优先在 Eden 分配


大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

👇Minor GC vs Major GC/Full GC

  • Minor GC:回收新生代(包括 Eden 和 Survivor 区域),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

  • Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。

在 JVM 规范中,Major GC 和 Full GC 都没有一个正式的定义,所以有人也简单地认为 Major GC 清理老年代,而 Full GC 清理整个内存堆。

大对象直接进入老年代


大对象是指需要大量连续内存空间的 Java 对象,如很长的字符串或数据。

一个大对象能够存入 Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下。

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。(还记得吗,新生代采用复制算法回收垃圾)

长期存活的对象将进入老年代


JVM 给每个对象定义了一个对象年龄计数器。当新生代发生一次 Minor GC 后,存活下来的对象年龄 +1,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去。

使用 -XXMaxTenuringThreshold 设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。

动态对象年龄判定


如果当前新生代的 Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

空间分配担保


JDK 6 Update 24 之前的规则是这样的:

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间, 如果这个条件成立,Minor GC 可以确保是安全的; 如果不成立,则虚拟机会查看 HandlePromotionFailure 值是否设置为允许担保失败, 如果是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的; 如果小于,或者 HandlePromotionFailure 设置不允许冒险,那此时也要改为进行一次 Full GC。

JDK 6 Update 24 之后的规则变为:

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。

通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保。

这个过程就是分配担保。


👇 总结一下有哪些情况可能会触发 JVM 进行 Full GC。

  1. System.gc() 方法的调用

此方法的调用是建议 JVM 进行 Full GC,注意这只是建议而非一定,但在很多情况下它会触发 Full GC,从而增加 Full GC 的频率。通常情况下我们只需要让虚拟机自己去管理内存即可,我们可以通过 -XX:+ DisableExplicitGC 来禁止调用 System.gc()。

  1. 老年代空间不足

老年代空间不足会触发 Full GC 操作,若进行该操作后空间依然不足,则会抛出如下错误:

java.lang.OutOfMemoryError: Java heap space

  1. 永久代空间不足

JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中也称为永久代(Permanet Generation),存放一些类信息、常量、静态变量等数据,当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,会触发 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:

java.lang.OutOfMemoryError: PermGen space

  1. CMS GC 时出现 promotion failed 和 concurrent mode failure

promotion failed,就是上文所说的担保失败,而 concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。

  1. 统计得到的 Minor GC 晋升到旧生代的平均大小大于老年代的剩余空间

类文件结构

==============================================================

JVM 的“无关性”


谈论 JVM 的无关性,主要有以下两个:

  • 平台无关性:任何操作系统都能运行 Java 代码

  • 语言无关性: JVM 能运行除 Java 以外的其他代码

Java 源代码首先需要使用 Javac 编译器编译成 .class 文件,然后由 JVM 执行 .class 文件,从而程序开始运行。

JVM 只认识 .class 文件,它不关心是何种语言生成了 .class 文件,只要 .class 文件符合 JVM 的规范就能运行。 目前已经有 JRuby、Jython、Scala 等语言能够在 JVM 上运行。它们有各自的语法规则,不过它们的编译器 都能将各自的源码编译成符合 JVM 规范的 .class 文件,从而能够借助 JVM 运行它们。

Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的, 因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更加强大。 因此,有一些 Java 语言本身无法有效支持的语言特性,不代表字节码本身无法有效支持。

Class 文件结构


Class 文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全都是连续的 0/1。Class 文件 中的所有内容被分为两种类型:无符号数、表。

  • 无符号数 无符号数表示 Class 文件中的值,这些值没有任何类型,但有不同的长度。u1、u2、u4、u8 分别代表 1/2/4/8 字节的无符号数。

  • 表 由多个无符号数或者其他表作为数据项构成的复合数据类型。

Class 文件具体由以下几个构成:

  • 魔数

  • 版本信息

  • 常量池

  • 访问标志

  • 类索引、父类索引、接口索引集合

  • 字段表集合

  • 方法表集合

  • 属性表集合

魔数

Class 文件的头 4 个字节称为魔数,用来表示这个 Class 文件的类型。

Class 文件的魔数是用 16 进制表示的“CAFE BABE”,是不是很具有浪漫色彩?

魔数相当于文件后缀名,只不过后缀名容易被修改,不安全,因此在 Class 文件中标识文件类型比较合适。

版本信息

紧接着魔数的 4 个字节是版本信息,5-6 字节表示次版本号,7-8 字节表示主版本号,它们表示当前 Class 文件中使用的是哪个版本的 JDK。

高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必需拒绝执行超过其版本号的 Class 文件。

常量池

版本信息之后就是常量池,常量池中存放两种类型的常量:

  • 字面值常量

字面值常量就是我们在程序中定义的字符串、被 final 修饰的值。

  • 符号引用

符号引用就是我们定义的各种名字:类和接口的全限定名、字段的名字和描述符、方法的名字和描述符。

常量池的特点
  • 常量池中常量数量不固定,因此常量池开头放置一个 u2 类型的无符号数,用来存储当前常量池的容量。

  • 常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量属于哪种常量类型。

常量池中常量类型

| 类型 | tag | 描述 |

| — | — | — |

| CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 |

| CONSTANT_Integer_info | 3 | 整型字面量 |

| CONSTANT_Float_info | 4 | 浮点型字面量 |

| CONSTANT_Long_info | 5 | 长整型字面量 |

| CONSTANT_Double_info | 6 | 双精度浮点型字面量 |

| CONSTANT_Class_info | 7 | 类或接口的符号引用 |

| CONSTANT_String_info | 8 | 字符串类型字面量 |

| CONSTANT_Fieldref_info | 9 | 字段的符号引用 |

| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |

| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |

| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |

| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |

| CONSTANT_MethodType_info | 16 | 标识方法类型 |

| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |

对于 CONSTANT_Class_info(此类型的常量代表一个类或者接口的符号引用),它的二维表结构如下:

| 类型 | 名称 | 数量 |

| — | — | — |

| u1 | tag | 1 |

| u2 | name_index | 1 |

tag 是标志位,用于区分常量类型;name_index 是一个索引值,它指向常量池中一个 CONSTANT_Utf8_info 类型常量,此常量代表这个类(或接口)的全限定名,这里 name_index 值若为 0x0002,也即是指向了常量池中的第二项常量。

CONSTANT_Utf8_info 型常量的结构如下:

| 类型 | 名称 | 数量 |

| — | — | — |

| u1 | tag | 1 |

| u2 | length | 1 |

| u1 | bytes | length |

tag 是当前常量的类型;length 表示这个字符串的长度;bytes 是这个字符串的内容(采用缩略的 UTF8 编码)

访问标志

在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否被 abstract/final 修饰。

类索引、父类索引、接口索引集合

类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。

由于 Java 不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。一个类可能实现了多个接口,因此用接口索引集合来描述。这个集合第一项为 u2 类型的数据,表示索引表的容量,接下来就是接口的名字索引。

类索引和父类索引用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过该常量总的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。

字段表集合

字段表集合存储本类涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。

每一个字段表只表示一个成员变量,本类中的所有成员变量构成了字段表集合。字段表结构如下:

| 类型 | 名称 | 数量 | 说明 |

| — | — | — | — |

| u2 | access_flags | 1 | 字段的访问标志,与类稍有不同 |

| u2 | name_index | 1 | 字段名字的索引 |

| u2 | descriptor_index | 1 | 描述符,用于描述字段的数据类型。 基本数据类型用大写字母表示; 对象类型用“L 对象类型的全限定名”表示。 |

| u2 | attributes_count | 1 | 属性表集合的长度 |

| u2 | attributes | attributes_count | 属性表集合,用于存放属性的额外信息,如属性的值。 |

字段表集合中不会出现从父类(或接口)中继承而来的字段,但有可能出现原本 Java 代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

方法表集合

方法表结构与属性表类似。

volatile 关键字 和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT 标志。

方法表的属性表集合中有一张 Code 属性表,用于存储当前方法经编译器编译后的字节码指令。

属性表集合

每个属性对应一张属性表,属性表的结构如下:

| 类型 | 名称 | 数量 |

| — | — | — |

| u2 | attribute_name_index | 1 |

| u4 | attribute_length | 1 |

| u1 | info | attribute_length |

JVM 性能调优

=================================================================

在高性能硬件上部署程序,目前主要有两种方式:

  • 通过 64 位 JDK 来使用大内存;

  • 使用若干个 32 位虚拟机建立逻辑集群来利用硬件资源。

使用 64 位 JDK 管理大内存


堆内存变大后,虽然垃圾收集的频率减少了,但每次垃圾回收的时间变长。 如果堆内存为 14 G,那么每次 Full GC 将长达数十秒。如果 Full GC 频繁发生,那么对于一个网站来说是无法忍受的。

对于用户交互性强、对停顿时间敏感的系统,可以给 Java 虚拟机分配超大堆的前提是有把握把应用程序的 Full GC 频率控制得足够低,至少要低到不会影响用户使用。

可能面临的问题:

  • 内存回收导致的长时间停顿;

  • 现阶段,64 位 JDK 的性能普遍比 32 位 JDK 低;

  • 需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎就无法产生堆转储快照(因为要产生超过 10GB 的 Dump 文件),哪怕产生了快照也几乎无法进行分析;

  • 相同程序在 64 位 JDK 消耗的内存一般比 32 位 JDK 大,这是由于指针膨胀,以及数据类型对齐补白等因素导致的。

使用 32 位 JVM 建立逻辑集群


在一台物理机器上启动多个应用服务器进程,每个服务器进程分配不同端口, 然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。

考虑到在一台物理机器上建立逻辑集群的目的仅仅是为了尽可能利用硬件资源,并不需要关心状态保留、热转移之类的高可用性能需求, 也不需要保证每个虚拟机进程有绝对的均衡负载,因此使用无 Session 复制的亲合式集群是一个不错的选择。 我们仅仅需要保障集群具备亲合性,也就是均衡器按一定的规则算法(一般根据 SessionID 分配) 将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可。

可能遇到的问题:

  • 尽量避免节点竞争全局资源,如磁盘竞争,各个节点如果同时访问某个磁盘文件的话,很可能导致 IO 异常;

  • 很难高效利用资源池,如连接池,一般都是在节点建立自己独立的连接池,这样有可能导致一些节点池满了而另外一些节点仍有较多空余;

  • 各个节点受到 32 位的内存限制;

  • 大量使用本地缓存的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点都有一份缓存,这时候可以考虑把本地缓存改成集中式缓存。

调优案例分析与实战


场景描述

一个小型系统,使用 32 位 JDK,4G 内存,测试期间发现服务端不定时抛出内存溢出异常。 加入 -XX:+HeapDumpOnOutOfMemoryError(添加这个参数后,堆内存溢出时就会输出异常日志), 但再次发生内存溢出时,没有生成相关异常日志。

分析

在 32 位 JDK 上,1.6G 分配给堆,还有一部分分配给 JVM 的其他内存,直接内存最大也只能在剩余的 0.4G 空间中分出一部分, 如果使用了 NIO,JVM 会在 JVM 内存之外分配内存空间,那么就要小心“直接内存”不足时发生内存溢出异常了。

直接内存的回收过程

直接内存虽然不是 JVM 内存空间,但它的垃圾回收也由 JVM 负责。

垃圾收集进行时,虚拟机虽然会对直接内存进行回收, 但是直接内存却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收, 它只能等老年代满了后 Full GC,然后“顺便”帮它清理掉内存的废弃对象。 否则只能一直等到抛出内存溢出异常时,先 catch 掉,再在 catch 块里大喊 “System.gc()”。 要是虚拟机还是不听,那就只能眼睁睁看着堆中还有许多空闲内存,自己却不得不抛出内存溢出异常了。

常见面试题

==============================================================

请描述下GC流程?或者一个对象从创建到GC的过程?

在这里插入图片描述

1 判断对象是否可回收有几种方式?


  • 引用计数算法

优点:实现简单,判定高效;

缺点:很难解决对象之间相互循环引用的问题;

  • 可达性分析算法

通过一系列"GC Roots"对象作为起始点,开始向下搜索,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到这个对象不可达),则证明该对象是不可用的;

优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;

缺点:实现比较复杂;需要分析大量数据,消耗大量时间;分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题);

2 "GC Roots"对象都包含哪些


  • 虚拟机栈 (栈帧中本地变量表)中引用的对象;

  • 方法区中 类静态属性引用的对象 ;

  • 方法区中 常量引用的对象;

  • 本地方法栈 JNI(Native方法)中引用的对象;

5 JVM如何进行对象标记


  • 第一次标记:在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记;并且进行一次筛选:此对象是否必要执行finalize()方法;没有必要执行的情况,则标记对象已死;有必要执行的情况,则对象被放入F-Queue队列中;

  • 第二次标记:GC将对F-Queue队列中的对象进行第二次小规模标记;finalize()方法是对象逃脱死亡的最后一次机会;一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

6 为何不建议使用finalize()方法


因为其执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用);如果需要"释放资源",可以定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用;

如果有关键资源,必须显式的终止方法;一般情况下,应尽量避免使用它,甚至可以忘掉它;

7 什么是安全点,为什么需要


运行中,非常多的指令都会导致引用关系变化;如果为这些指令都生成对应的OopMap,需要的空间成本太高;

只在特定的位置记录OopMap引用关系,这些位置称为安全点(Safepoint);

8 如何选定安全点


不能太少,否则GC等待时间太长;也不能太多,否则GC过于频繁,增大运行时负荷;

所以,基本上是以程序"是否具有让程序长时间执行的特征"为标准选定,如:方法调用、循环跳转、循环的末尾、异常跳转等;

只有具有这些功能的指令才会产生Safepoint;

9 如何使Java线程在安全点上停顿


抢先式中断(Preemptive Suspension):在GC发生时,首先中断所有线程;如果发现不在Safepoint上的线程,就恢复让其运行到Safepoint上;

主动式中断(Voluntary Suspension):在GC发生时,不直接操作线程中断,而是仅简单设置一个标志;让各线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起;

而轮询标志的地方和Safepoint是重合的;

10 什么是安全区域,为什么需要安全区域


线程不执行时没有CPU时间(Sleep或Blocked状态),无法运行到Safepoint上再中断挂起;

安全区域:指一段代码片段中,引用关系不会发生变化;在这个区域中的任意地方开始GC都是安全的;

11 如何使用安全区域解决问题


线程执行进入Safe Region,首先标识自己已经进入Safe Region;

线程被唤醒离开Safe Region时,其需要检查系统是否已经完成根节点枚举(或整个GC);

如果已经完成,就继续执行;否则必须等待,直到收到可以安全离开Safe Region的信号通知,这样就不会影响标记结果;

12 GC算法:标记-清楚优缺点


优点:基于最基础的可达性分析算法,它是最基础的收集算法;而后续的收集算法都是基于这种思路并对其不足进行改进得到的;

缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量不连续的内存碎片;这会导致分配大内存对象时,无法找到足够的连续内存;从而需要提前触发另一次垃圾收集动作;

13 GC算法:复制算法优缺点


优点:使得每次都是只对整个半区进行内存回收;内存分配时也不用考虑内存碎片等问题;实现简单,运行高效;

缺点:空间浪费;效率随对象存活率升高而变低;

14 GC算法:HotSpot虚拟机复制算法


将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;

每次使用Eden和其中一块Survivor;

当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;

而后清理掉Eden和使用过的Survivor空间;

后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;

默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;

15 什么是分配担保


如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制(Handle Promotion)进入老年代;

16 GC算法:标记-整理优缺点


优点:不会产生内存碎片;

缺点:增加了对存活对象需要整理的过程,效率更低;

17 分代收集算法


“分代收集”(Generational Collection)算法结合不同的收集算法处理不同区域。

新生代:每次垃圾收集都有大批对象死去,只有少量存活;所以可采用复制算法;

老年代:对象存活率高,没有额外的空间可以分配担保;使用"标记-清理"或"标记-整理"算法;

优点:根据各个年代的特点采用最适当的收集算法;

缺点:仍然不能控制每次垃圾收集的时间;

18 G1垃圾收集算法


19 JVM有哪些收集器?分别用于哪些代?


JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:

JVM收集器

新生代收集器:Serial、ParNew、Parallel Scavenge;

老年代收集器:Serial Old、Parallel Old、CMS;

整堆收集器:G1;

20 Serial收集器


新生代、复制算法、单线程收集;

缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";

Serial/Serial Old组合收集器运行示意图如下:

Serial/Serial Old组合

21 ParNew收集器


新生代、复制算法、多线程收集;

缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";

ParNew/Serial Old组合收集器运行示意图如下:

ParNew/Serial Old组合

22 Parallel Scavenge收集器


新生代、复制算法、多线程收集;

CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);

23 Serial Old收集器


老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、单线程收集;

24 Parallel Old收集器


老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、多线程收集;

25 CMS收集器


老年代、"标记-清除"算法(不进行压缩操作,产生内存碎片)、并发收集、低停顿

CMS收集器运行示意图如下:

一线互联网大厂Java核心面试题库

image

正逢面试跳槽季,给大家整理了大厂问到的一些面试真题,由于文章长度限制,只给大家展示了部分题目,更多Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等已整理上传,感兴趣的朋友可以看看支持一波!
量存活;所以可采用复制算法;

老年代:对象存活率高,没有额外的空间可以分配担保;使用"标记-清理"或"标记-整理"算法;

优点:根据各个年代的特点采用最适当的收集算法;

缺点:仍然不能控制每次垃圾收集的时间;

18 G1垃圾收集算法


19 JVM有哪些收集器?分别用于哪些代?


JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:

JVM收集器

新生代收集器:Serial、ParNew、Parallel Scavenge;

老年代收集器:Serial Old、Parallel Old、CMS;

整堆收集器:G1;

20 Serial收集器


新生代、复制算法、单线程收集;

缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";

Serial/Serial Old组合收集器运行示意图如下:

Serial/Serial Old组合

21 ParNew收集器


新生代、复制算法、多线程收集;

缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";

ParNew/Serial Old组合收集器运行示意图如下:

ParNew/Serial Old组合

22 Parallel Scavenge收集器


新生代、复制算法、多线程收集;

CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);

23 Serial Old收集器


老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、单线程收集;

24 Parallel Old收集器


老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、多线程收集;

25 CMS收集器


老年代、"标记-清除"算法(不进行压缩操作,产生内存碎片)、并发收集、低停顿

CMS收集器运行示意图如下:

一线互联网大厂Java核心面试题库

[外链图片转存中…(img-3wbUbgmT-1723538466286)]

正逢面试跳槽季,给大家整理了大厂问到的一些面试真题,由于文章长度限制,只给大家展示了部分题目,更多Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等已整理上传,感兴趣的朋友可以看看支持一波!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值