之前以为面经只是死记硬背的东西,后来发现记住了它们,对自己对知识的理解确实有帮助,难怪语文的文章老是要求背背背。
前言
这次的面经整理分为以下几个部分,希望对大家的工作有帮助。
内容 | 链接地址 |
---|---|
Java 基础 | 传送门 |
Java 集合 | 传送门 |
Java 多线程 | 传送门 |
Java虚拟机 | 传送门 |
计算机网络 | 传送门 |
数据结构和算法 | 传送门 |
数据库 | 传送门 |
JavaWeb | 传送门 |
设计模式 | 传送门 |
Spring、MyBatis | 传送门 |
1 JVM 的体系机构
1.1 JVM 的位置
计算机底层到 Java 代码,分别是:硬件体系 → 操作系统 → JVM → Java 代码。
1.2 JVM 的体系结构
JDK1.8 之前的 JVM 体系结构如下图所示,JDK1.8 及之后的体系结构中,将方法区(放在堆中的)使用元空间(放在非运行时数据区)代替,里面存放的内容还是一样的。
线程私有的是:虚拟机栈、本地方法栈、程序计数器;线程共享的是:堆、方法区、直接内存(放元空间的区域)。
1.3 虚拟机栈
虚拟机栈的生命周期与线程同步,一旦线程结束,栈内存也就被释放了,所以对于栈来说,不存在垃圾回收的问题。栈内存中存放的内容为:八大基本类型、对象引用和实例的方法。
对于一个对象,虚拟机栈中存放的是该对象的引用,对象的实体是存放在堆中的,这一点应该是很清楚的。拓展一下,对象的常量、静态变量、常量池、类信息是放在方法区的。
虚拟机栈描述的是 Java 方法执行的内存模型。它实际上是由一个个栈帧组成的,栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
方法在 JVM 中是如何调用的:
Java 栈中保存的主要内容是栈帧,每⼀次方法 调⽤都会有⼀个对应的栈帧被压⼊ Java 栈,每⼀个方法调⽤结束后,都会有⼀个栈帧被弹出。
Java ⽅法有两种返回⽅式:
- return 语句;
- 抛出异常。
不管哪种返回⽅式都会导致栈帧被弹出。
1.4 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈作用相似,区别在于虚拟机栈为虚拟机执行 Java 方法(字节码)服务,而本地方法栈是为虚拟机使用到的 native 方法服务。
在程序中,凡是带了 native 关键字的,就说明 Java 的作用范围达不到了,应该是调用其它语言的库,并且进入到本地方法栈中,然后调用本地方法接口 JNI。比如线程中的 start()。
1.5 程序计数器
程序计数器是⼀块较⼩的内存空间,可以看作是当前线程所执⾏的字节码的⾏号指示器。
程序计数器主要有两个作用:
- 字节码解释器通过程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理;
- 为了线程切换后能恢复到正确的执行位置,每条线程都需要有⼀个独⽴的程序计数器,各线程之间计数器互不影响,独⽴存储。在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置。
注意:程序计数器是唯⼀⼀个不会出现 OutOfMemoryError 的内存区域,它的⽣命周期随着线程的创建⽽创建,随着线程的结束⽽死亡。
1.6 堆
Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
不过随着 JDK 的迭代,也并不是这么绝对了,从 jdk 1.7 开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤,那么对象可以直接在栈上分配内存。
正因为堆所占内存空间最大,所以 Java 堆也是垃圾回收的主要区域,同时正是由于分代垃圾收集算法的广泛应用,导致堆也被进一步的细化了。
堆内存通常被分为如下部分:
总的分为新生代(伊甸园区、from 幸存区、to 幸存区)、老生代和永久代。
对象在堆内存中的分配策略和垃圾回收请查看本章 4.7 内容。
1.7 方法区
⽅法区与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息(常量池)、常量、静态变量、即时编译器编译后的代码等数据。(你想想看,哪些数据是可以大家一起用的,就明白了)
虽然 Java 虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。
方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现。即一个是标准,一个是实现。(方法区 == 永久代)
1.8 运行时常量池
运⾏时常量池是⽅法区的⼀部分。类信息中除了类的版本、字段、⽅法、接⼝等描述信息外,还有⽤于存放编译期⽣成的各种字⾯量和符号引⽤的常量池表。
既然运⾏时常量池是⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛出 OutOfMemoryError 错误。
1.9 直接内存
元空间存放的位置。
直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤。⽽且也可能导致 OutOfMemoryError 错误出现。
参考文献:传送门
1.10 各部分的存储内容
- 虚拟机栈:八大基本类型的数据、对象引用和实例的方法;
- 本地方法栈:与虚拟机栈作用相似,区别在于虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈是为虚拟机使用到的 native 方法服务;
- 堆:存放几乎所有的对象实例、数组;
- 方法区:类信息(包括常量池)、常量、静态变量、即时编译器编译后的代码;
- 常量池:类信息中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有⽤于存放编译期⽣成的各种字⾯量和符号引⽤的常量池表。
1.11 简答
1 什么是 JVM
JVM 是运行 Java 字节码的虚拟机,它是 Java 的核心内容,它有针对不同系统的特定实现,使得相同的字节码在不同的系统上可以得到相同的结果,从而保证 Java 跨平台性。
2 JVM 的内存结构
JVM 主要由线程私有的虚拟机栈、本地方法栈、程序计数器和线程共享的堆和方法区组成。
- 虚拟机栈描述的是 Java 方法执行的内存模型。它实际上是由一个个栈帧组成的,每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
- 本地方法栈的话与虚拟机栈作用类似,区别在于虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈是为虚拟机使用到的 native 方法服务。
- 程序计数器的主要由两个作用:
- 字节码解释器通过程序计数器来依次读取指令,从⽽实现代码的流程控制;
- 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置。
- 堆用来存放对象实例,几乎所有的对象实例以及数组都在这里分配内存;
- 方法区是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息(常量池)、常量、静态变量、即时编译器编译后的代码等数据。
2 类加载器
2.1 类加载器的定义
类加载器负责将 Java 类动态地加载到虚拟机中并转换成对应的实例,第一次使用该类时才加载。 Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件),类加载器负责读取 Java 字节代码,并转换成 java.lang.Class
类的一个实例。
下面用代码来说明类加载器:
public class Test{
public static void main(String[] args) {
// 类的对象
Person zhao = new Person();
Person li = new Person();
Person pan = new Person();
// 不同的对象的哈希值不同
System.out.println(zhao.hashCode());
System.out.println(li.hashCode());
System.out.println(pan.hashCode());
// 从类的对象得到对应的类型
Class<? extends Person> zhaoClass = zhao.getClass();
Class<? extends Person> liClass = li.getClass();
Class<? extends Person> panClass = pan.getClass();
// 因为是同一个类型,所以哈希值相同
System.out.println(zhaoClass.hashCode());
System.out.println(liClass.hashCode());
System.out.println(panClass.hashCode());
// 得到类的类加载器
ClassLoader classLoader = zhaoClass.getClassLoader();
// AppClassLoader
System.out.println(classLoader);
// PlatformClassLoader
System.out.println(classLoader.getParent());
// null
System.out.println(classLoader.getParent().getParent());
}
}
class Person{
String name;
int age;
}
2.2 双亲委派机制
1) 定义
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载且无法加载,自己才会去加载这个类。
2) 类加载器的类别
- BootstrapClassLoader(启动类加载器):
c++
编写,加载java
核心库java.*
,构造ExtClassLoader
和AppClassLoader
。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作; - ExtClassLoader (标准扩展类加载器):
java
编写,加载扩展库,如classpath
中的jre
,javax.*
或者
java.ext.dir
指定位置中的类,开发者可以直接使用标准扩展类加载器; - AppClassLoader(系统类加载器):java
编写,加载程序所在的目录,如
user.dir所在的位置的
class; - CustomClassLoader(用户自定义类加载器):
java
编写,用户自定义的类加载器,可加载指定路径的class
文件。
3) 委派机制的流程图
4) 双亲委派机制的作用
- 防止重复加载同一个
.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全; - 保证核心
.class
不能被篡改。通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
参考文献:传送门
2.3 沙箱安全机制
1) 定义
Java 安全模型的核心就是 Java 沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
- 双亲委派机制-保证 JVM 不被加载的代码破坏;
- 沙箱安全机制-保证机器资源不被 JVM 里运行的程序破坏。
2) 基本组件
- 字节码校验器:确保 Java 文件遵循 Java 语言规范等,字节码校验器在编译过程就会报错,不需要到运行过程;
- 类加载器:通过双亲委派机制;
- 内置于 Java 虚拟机(及语言)的安全特性;
- 安全管理器及 Java API。
参考文献:传送门
2.4 Java 类加载过程
-
加载:通过类加载器把 class 字节码文件载入到内存中;
-
验证:保证加载进来的字节码文件符合虚拟机规范,不会造成安全错误。
-
准备:为静态变量以及常量分配内存,并赋予初值;
-
解析:将常量池内的符号引用替换为直接引用;
-
初始化:对静态变量或者语句进行初始化;
类加载过程是类生命周期的一部分,在这个过程之前,还有源代码的编译,在这个过程之后,还有垃圾回收等。
参考文献:传送门
2.5 Java 对象创建的过程
1 面试:Java 创建对象的过程
-
类加载检查:判断这个类是否被加载过,如果还没未被加载,则先进行类的加载、解析和初始化过程。
-
分配内存:类加载检查通过后,虚拟机会在堆中为新生对象分配内存;
-
初始化零值:,虚拟机将分配到的内存空间,除了对象头,都初始化为零值;
-
设置对象头:将对象的哈希码等信息放到对象头中;
-
执行初始化方法:比如通过构造方法将对象按照程序员的意愿进行初始化。
Java 对象的创建过程如下图所示:
- 类加载检查:当虚拟机遇到一条 new 指令时,先检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,如果没有找到这个符号引用,则说明该类还没有被加载过,则开始进行类的加载、解析和初始化过程。
- 分配内存:在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配⽅式由 Java 堆是否规整决定,⽽ Java 堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
- 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用。
- 设置对象头:初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。
- 执行 init 方法:上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。
2.6 简答
1 什么是类加载器
类加载器负责将 Java 类动态地加载到虚拟机中并转换成对应的实例,第一次使用该类时才加载。
2 Java 类加载过程
-
加载:通过类加载器把 class 字节码文件载入到内存中;
-
验证:保证加载进来的字节码文件符合虚拟机规范,不会造成安全错误。
-
准备:为静态变量以及常量分配内存,并赋予初值;
-
解析:将常量池内的符号引用替换为直接引用;
-
初始化:对静态变量或者语句进行初始化;
类加载过程是类生命周期的一部分,在这个过程之前,还有源代码的编译,在这个过程之后,还有垃圾回收等。
3 Java 创建对象的过程
- 类加载检查:判断这个类是否被加载过,如果还没未被加载,则先进行类的加载、检查、准备、解析和初始化过程。
- 分配内存:类加载检查通过后,虚拟机会在堆中为新生对象分配内存;
- 初始化零值:虚拟机将分配到的内存空间初始化为零值,除对象头;
- 设置对象头:将对象的类信息放到对象头中;
- 执行初始化方法:比如通过构造方法将对象按照程序员的意愿进行初始化。
4 双亲委派机制
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载且无法加载,自己才会去加载这个类。
作用:
- 防止重复加载同一个
.class
; - 保证核心
.class
不能被篡改。
5 沙箱安全机制
沙箱安全机制就是将 Java 代码限制在虚拟机的特定环境内运行,并且严格限制代码对本地资源的访问。
- 双亲委派机制-保证 JVM 不被加载的代码破坏;
- 沙箱安全机制-保证机器资源不被 JVM 里运行的程序破坏。
4 垃圾收集算法
4.1 标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。
标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
4.2 标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
4.3 复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收,这样做的效率较高。
4.4 分代收集算法
当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
延伸面试问题: 堆内存为什么要分为新⽣代和⽼年代?
将堆内存分为新生代和老年代,可以针对两个分区的不同特点使用不同的垃圾收集算法,从而提高垃圾回收的效率。
5 垃圾收集器
垃圾收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现。
虽然我们对各个收集器进⾏⽐较,但并⾮要挑选出⼀个最好的收集器。因为知道现在为⽌还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应⽤场景选择适合⾃⼰的垃圾收集器。
5.1 Serial 收集器
Serial(串⾏)收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( “Stop The World” ),直到它收集结束。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
5.2 ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全⼀样。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
并⾏和并发概念补充:
- 并发(Concurrent):指⽤户线程与垃圾收集线程同时执⾏(但不⼀定是并⾏,可能会交替执⾏),⽤户程序在继续运⾏,⽽垃圾收集器运⾏在另⼀个 CPU 上。
- 并行(Parallel) :指多条垃圾收集线程并⾏⼯作,但此时⽤户线程仍然处于等待状态。
5.3 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。它⾮常符合在注重⽤户体验的应⽤上使⽤。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。
5.4 Parallel Scavenge 收集器
CMS 等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验),而 Parallel Scavenge 收集器关注点是吞吐量(⾼效率地利⽤CPU)。所谓吞吐量就是 CPU 中⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值。 这是 JDK1.8 默认收集器。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
5.5 G1 收集器
G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器。以极⾼概率满⾜ GC 停顿时间要求的同时,还具备⾼吞吐量性能特征。
等。
7 堆内存中对象的分配策略
7.1 堆划分
在JDK1.7以及之前的版本中,堆内存通常被分为三块区域:新生代、老年代、永久代。 新生代又分为:Eden区、From Survivor 区(S0)、To Survivor 区(S1)。默认8:1:1。
而在JDK1.8中情况发生了变化,把存放元数据(永久代)中的永久内存从堆内存中移到了本地内存(native memory)中。
JDK1.8也提供了一个新的设置Matespace(元空间)内存大小的参数,通过这个参数可以设置Matespace内存大小,这样我们可以根据自己项目的实际情况,避免过度浪费本地内存,达到有效利用。
7.2 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden区 中分配。当 Eden区没有足够的空间进行分配时,虚拟机发起一次 Minor GC。
大致过程如下:
- 最初一次,当 Eden 区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到 From 幸存区(此时 To 幸村区是空白的,两个幸存区总有一个是空白的),然后清空 Eden 区。
- 下次 Eden 区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到 To 幸存区中,然后清空 Eden 区。同时,将From 幸存区中消亡的对象清理掉,将其中可以晋级的对象晋级到老年代,将存活的对象也复制到 To 幸存区,然后清空 From 幸存区,然后 To 幸存区和 From 幸存区的互换;
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制)之后,仍然存活的对象将被复制到老年代,即晋级;
- 注意:当发生 Minor GC 的时候,当发现幸存区空间不足,部分对象无法复制到幸存区时,虚拟机通过分配担保机制将这些对象提前转移到老年代。
其中,新生代使用的垃圾收集算法为“复制算法”,老生代使用的垃圾收集算法为“标记-清除”或“标记-整理”算法。
7.3 大对象直接进入老年代
大对象是指,需要大量连续内存空间的 java 对象,最典型的的大对象有:很长的字符串以及数组。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。
目的:避免在Eden区和两个Survivor区之间发生大量的内存复制(新生代采用复制算法进行GC)。
7.4 长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器。
如果对象在Eden区出生,经过第一次Minor GC之后任然存活,并且能被Survivor区容纳,那么对象年龄设置为1。以后,对象在Survivor区中每“熬过”一次Minor GC,年龄就加1,当对象的年龄达到一定程度(默认15岁),就会晋升到老年区中。
通过参数-XX:MaxTenuringThreshold设置。
7.5 动态对象年龄判定
虚拟机并不是永远要求对象年龄达到设定值才能晋升到老年代。
如果在幸存区空间中低于或等于某个年龄的所有对象大小(内存空间)的总和大于幸存区空间的一半,那么那些年龄大于或者等于该年龄的对象就可以直接进入老年代。
关于动态对象年龄判定的参考文献:传送门
7.6 空间分配担保策略
1)谁进行空间担保
空间担保指的是 JVM 在内存分配时,当新生代空间不足,新生代会存活的对象搬到老生代,然后将腾出来的空间用于分配给最新的对象。在这里老年代作为担保人进行空间分配担保。
2)策略内容
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时要改为进行一次Full GC。
3)为什么需要空间分配担保策略
是因为新生代采用复制收集算法,假如伊甸园区大量对象在Minor GC后仍然存活(最极端情况为内存回收后伊甸园区中所有对象均存活),而幸存区空间是比较小的,这时就需要老年代进行分配担保,把幸存区无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来减少晋级的对象数量。
参考文献:传送门
8 引用的介绍
8.1 强引用
我们平时代码中使用得最多的引用,对象的类是:StrongReference。就比如上面说的Object obj = new Object();我们再熟悉不过了,作为最强的引用,只要引用还存在着,垃圾收集器就不会将该引用给回收,即使会出现OOM(内存溢出)。就是说这种引用只要引用还一直指向的对象,垃圾收集器是不会去管它的,所以它被称为强引用。
Object obj = new Object();
obj = null;
不过有两种特殊的情况:
- obj 被赋值为了 null,该引用就断了,垃圾收集器会在合适的时候回收改引用的内存;
- 还有一种情况就是 obj 是成员变量,方法执行完了,obj随着被栈帧被回收了,obj 引用也是一起被回收了。
8.2 软引用
对于一个**软引用,如果内存空间⾜够,垃圾回收器就不会回收它,如果内存空间不⾜了,就会回收这些对象的内存。**只要垃圾回收器没有回收它,该对象就可以被程序使⽤。软引⽤可⽤来实现内存敏感的⾼速缓存。
软引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果软引⽤所引⽤的对象被垃圾回收,JAVA虚拟机就会把这个软引⽤加⼊到与之关联的引⽤队列中。
8.3 弱引用
**虚引用比上面两个引用就更菜了,只要垃圾收集器扫描到了它,被弱引用关联的对象就会被回收。**被弱引用关联对象的生命周期其实就是从对象创建到下一次垃圾回收。对应的类是WeakReference。
8.4 虚引用
**虚引用是最弱的一种引用,它不会影响对象的生命周期,对象被回收跟它没啥关系。它引用的对象可以在任何时候被回收,而且也无法根据虚引用来取得一个对象的实例。**仅仅当它指向的对象被回收的时候,它会受到一个通知。对应的类是PhantomReference。
8.5 总结
引用 | 回收时机 | 使用场景 |
---|---|---|
强 | 不会被回收 | 正常编码使用 |
软 | 内存不够了,被GC | 可作为缓存 |
弱 | GC 发生时 | 可作为缓存(WeakHashMap) |
虚 | 任何时候 | 监控对象回收,记录日志 |
参考文献:传送门
9 问题简答
对象的访问定位有哪两种⽅式
建⽴对象就是为了使⽤对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问⽅式有虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:
- 句柄: 如果使⽤句柄的话,那么 Java 堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;
2. 直接指针: 如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。
这两种对象访问⽅式各有优势。使⽤句柄来访问的最⼤好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,⽽ reference 本身不需要修改。使⽤直接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。
1 GC 机制
GC 机制就是虚拟机通过垃圾回收算法将堆中的对象进行管理与回收。
垃圾回收算法:
- 标记-清除算法:内存碎片;
- 标记-整理算法:清除之后会将存活对象往左端空闲空间移动;
- 复制算法:分为两块内存区域,一块使用完后,将其中存活的对象移动到另一块,同时清理当前这块区域;
- 分代收集算法:根据对象存活周期的不同将内存分为几块;
延伸面试问题: 堆内存为什么要分为新⽣代和⽼年代?
将堆内存分为新生代和老年代,可以针对两个分区的不同特点使用不同的垃圾收集算法,从而提高垃圾回收的效率。
2 堆内存中对象的分配策略
大多数情况下,对象在新生代 Eden区 中分配。当 Eden区没有足够的空间进行分配时,虚拟机发起一次 Minor GC。
大致过程如下:
- 最初一次,当 Eden 区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到 From 幸存区(此时 To 幸村区是空白的,两个幸存区总有一个是空白的),然后清空 Eden 区。
- 下次 Eden 区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到 To 幸存区中,然后清空 Eden 区。同时,将From 幸存区中消亡的对象清理掉,将其中可以晋级的对象晋级到老年代,将存活的对象也复制到 To 幸存区,然后清空 From 幸存区,然后 To 幸存区和 From 幸存区的互换;
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制)之后,仍然存活的对象将被复制到老年代,即晋级;同时大对象和长期存活的对象进入老年代。
- 注意:当发生 Minor GC 的时候,当发现幸存区空间不足,部分对象无法复制到幸存区时,虚拟机通过分配担保机制将这些对象提前转移到老年代。
其中,新生代使用的垃圾收集算法为“复制算法”,老生代使用的垃圾收集算法为“标记-清除”或“标记-整理”算法。
3 空间分配担保机制
空间担保指的是 JVM 在内存分配时,当新生代空间不足,新生代会将存活的对象搬到老生代,然后将腾出来的空间用于分配给最新的对象。在这里老年代作为担保人进行空间分配担保。
策略内容:
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时要改为进行一次Full GC。