JVM面经
JVM面经
内存管理
JDK1.8:
虚拟机栈+本地方法栈
内容:类似其他语言
堆
线程共享的,里面装着所有的对象(现代编译器会进行一定的优化,一部分对象会直接放到栈里面)。
Java 堆从 GC 的角度还可以细分为:新生代(Eden 区、SurvivorFrom 区和 SurvivorTo 区)和老年代。
S0即SF,S1即ST。对象都会首先在 Eden 区域分配,在一次新生代垃圾回收(轻GC) 后,如果对象还存活,则会进入S0,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
每次新生代垃圾回收的过程是:复制 --> 清空 --> 互换
- eden、servicorFrom 复制到 ServicorTo,年龄+1(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区)
- 清空 eden、servicorFrom
- ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。
老年的标准:是动态的,首先有一个MAX,默认是15。Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加,当累加到某个年龄时,所累加的大小超过了 Survivor 区的一半,则取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。
堆里面还有字符串常量池,是直接写在代码里面的用引号包围的字符串常量,会随着其他对象一起被GC。
方法区
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
这里还包括运行时常量池,可以理解为类的符号表,包含字面量和静态常量(例如指向字符串常量池某个特定指定对象的引用,数字等)。
永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。永久代以及元空间的区别可以理解为一个在本地内存,一个在JVM内存。
对象创建过程
- 遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。同操作系统一样,可以让堆内存是有序的,那么新分配内存就只需要从最后面的指针处那一块内存就行,也可以让堆内存无序,那么就需要维护一个空闲列表。【此处利用乐观锁保证线程安全,具体是CAS】
- 内存空间都初始化为零值。
- 设置对象头,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。
- 执行构造方法。
对象访问过程
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定。
目前主流的访问方式有:使用句柄、直接指针。
垃圾回收
哪些要回收?何时回收?
何时进行垃圾回收?
分两种:轻GC(只GC新生区)、重GC(GC整个堆)。
哪些对象需要回收?
利用可达性分析,分析一个对象到GCRoots是不是可达的,如果不可达就是可回收的。老Java在回收前还会处理finalize方法,现在正在逐步废弃。
GCRoots是指:
Java 虚拟机栈(栈帧中的本地变量表)中引用的对象、本地方法栈中引用的对象、方法区中常量引用的对象、方法区中类静态属性引用的对象。
而在第一点上,Java有一些独特的操作:
Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
强引用就是我们平时说的引用。而软引用是一般不GC,在JVM内存不够时才GC。弱引用就是GC时就会回收的引用,不管内存充不充足。虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。无法通过虚引用来取得一个对象实例,设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
垃圾收集算法(垃圾清除到底怎么做?)
首先有三种具体操作方案:标记-清除(Mark-Sweep)算法、复制(Copying)算法、标记-整理(Mark-Compact)算法。
当一个对象被创建时,给一个标记位,假设为 0 (false);在标记阶段,我们将所有可达对象(或用户可以引用的对象)的标记位设置为 1 (true);扫描阶段清除的就是标记位为 0 (false)的对象。
但是这样会产生大量的内存碎片,因此第二种:
由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
因此第三种:复制方式,如图所示,问题就是会浪费一半的空间。但实际上,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间。将堆内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
而现代VM都会采用分代收集,采用不同的策略回收不同代的垃圾。
大流程:
具体GC的操作
GC有哪些呢?有这些:
-
最简单的Serial:单线程回收,它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(STW阶段)。而Serial Old几乎是一样的,不过是把复制算法改成标志-整理。
-
Parallel并行:核心是多线程,可以利用多CPU。ParNew就是这样。但它引申出了Parallel Scavenge:可以调整垃圾回收强度,减少每次垃圾回收的时间。Parallel Old就是老年代版本。
-
CMS:并发标记清除,核心是使得清除过程中一部分工作的可以和用户线程并行,减少停顿。这东西分四步:
- 初始标记:标记Root的下级(仅下一级),会很快,会导致停顿。
- 并发标记:从第二级开始接着往下标记,但这个过程与用户线程并行。
- 重新标记:第 2 步并没有阻塞其它工作线程,在第2步过程中很有可能会产生新的垃圾,但花费时间也相对较少,因为新垃圾肯定不多。
- 并行清除:再并行地清除。
-
G1:设计很不一样
G1详解
在G1中,region是不连续的,每个对象占用region一部分,而不同代是混杂的。
这里的H是什么呢,是指大对象,大小超过Region一半的对象会直接放进老年代,避免频繁GC。
从大类上分,G1有两种回收模式:完全年轻化GC、部分年轻化GC。
技术细节
- RememberSet(CMS和G1使用)
很多时候会存在跨区域引用的情况,比如某个新生代对象没有被任何新生代对象引用,但对老年代对象引用,那么做部分GC的时候为了确切的区分出到底是不是可以安全GC的对象,实际上就要遍历全部对象,显然效率很低。
于是就有了RememberSet(RS or RSet)。
在对象晋升的时候,将晋升对象记录下来,这个存储跨区引用关系的容器称之为RSet。利用一个RSet来记录这个跨区域引用的关系,每个区域都有一个RSet,这样在进行标记的时候,将RSet也作为ROOTS进行遍历即可。
类加载
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
在其中,加载->连接->初始化是类加载过程