前言
在面试中通常会考察JVM判断候选人的技术热情,对于Javaer还是比较重要的,整理一下JVM相关的知识点,包括JVM的内存管理、垃圾回收、类加载机制、JVM调优参数
参考资料:
JavaGuide:Java内存区域详解(重点) | JavaGuide
二哥面渣逆袭:JVM面试题,54道Java虚拟机八股文(2.3万字113张手绘图),面渣逆袭必看👍 | 二哥的Java进阶之路
一、内存管理
1. 讲一下JVM内存区域是怎么划分的
JVM将内存区域划分为:堆、方法区、程序计数器、虚拟机栈和本地方法栈。
其中线程共享的是堆和方法区、线程私有的是程序计数器、虚拟机栈和本地方法栈
程序计数器又称PC寄存器,指向当前线程所执行的字节码的行号,主要用于分支跳转、方法调用、线程切换和恢复
虚拟机栈用于执行Java方法,每次调用一个Java方法,就会将一个栈帧入栈,栈帧包括方法的局部变量表、操作数栈、动态链接、方法出口等信息。类的静态方法的局部变量表没有this的引用。操作数栈是用于保存临时变量
本地方法栈用于执行native方法,同虚拟机栈一样,在执行方法时会创建栈帧
堆是JVM中最大的一块内存区域,被划分为Young Generation和Old Generation,Young Generation又可以划分为Eden区和s0、s1区
方法区存储已被JVM加载的类信息、静态变量和即时编译器编译后的代码缓存等。在Java7之前,方法区由永久代(PermGen)实现,在Java7,静态变量和字符串常量池被放到了堆上,从Java8开始,方法区由元空间(MetaSpace)实现。之所以用元空间替代永久代,是因为永久代受限于JVM内存大小,容易导致OOM,而元空间只受限于机器的内存大小
2. 在new一个对象时会发生什么
首先会检测该类是否被加载、解析和初始化,如果尚未,则执行类加载
第二步会为对象分配内存。内存分配有两种方式:一是指针碰撞,即堆内存被规整的分为已分配和空闲两部分,其间有一指针,在需要分配内存时将指针向后移动;二是空闲列表,即堆内存存在很多内存碎片,需要维护一个空闲列表,每次分配内存时找到足够大小的内存块并更新列表。
内存分配过程中可能出现多线程的抢占问题,如果开启TLAB(Thread Local Allocation Block),会为每一个线程保留一小块内存分配缓冲区,避免抢占,如果未开启TLAB或者TLAB空间不足,则会使用CAS+失败重试的机制保证更新操作的原子性
随后会完成初始化,分配0值
再生成对象头,对象头包含类的元数据信息、对象的哈希码以及对象的GC年龄信息等
随后执行init方法,将变量按照程序员的意愿赋值,这样一个对象就创建完成了
3. 一个对象的内存布局如何
一个对象在内存中由三部分构成,分别是:对象头、实际数据和对齐填充
对象头可以由三部分构成
- 第一部分是对象运行时的信息,包括对象的哈希码、GC年龄信息、线程持有的锁、锁状态标志等信息
- 第二部分是类型指针,指向类的元数据信息,也就是Class对象,类型指针可以由8字节被压缩为4字节(在JDK8默认开启)
- 第三部分只有数组拥有,用于记录数组长度
实际数据是指对象实际的成员变量值,会被重排对齐以提高内存访问的速度
对齐填充是指整个对象的大小需要是8字节的倍数,缺失的部分需要填充额外的字节。对齐的意义是避免跨缓存行访问
4. 如何访问一个对象
访问对象的方式有两种:
一种是使用句柄,也就是局部变量表的reference指向的是堆中句柄池的一个句柄地址,句柄会指向实际的对象信息和方法区的类信息,优点是对象被移动时不需要改变reference的值,只需要修改句柄的值
一种是直接引用,也就是局部变量表的reference指向的是实际堆中的对象信息,优点是访问速度快,减少了一次指针定位的时间开销
5. 对象有几种引用类型
对象有四种引用类型
使用new创建的引用类型为强引用, 强引用(StrongReference) 除了不可达会被GC回收,就算内存不足也不会被回收
而 软引用(SoftReference) 在内存不足时会被回收
弱引用(WeakReference) 无论内存是否充足,在下一次垃圾回收就会被回收
虚引用(PhantomReference) 必须与 引用队列(Reference Queue) 关联起来,当对象被回收时,如果发现有虚引用,会将该引用加入到引用队列,故可以通过判断引用队列是否加入该虚引用来了解垃圾回收的过程
一般来说用软引用比较多,用于加速垃圾回收的速度,防止OOM
6. 堆区分配内存和回收原则是怎么样的
新建的对象会优先在Eden区分配,如果Eden区空间不足,会根据JVM的空间分配担保机制触发Minor GC或Full GC,把对象从Eden区转移到Survior区和OldGen
空间分配担保机制
Minor GC之前会检查OldGen最大可用连续空间大于YoungGen所有对象的空间
如果是,则会触发一次Minor GC
如果不是,则会根据参数
HandlePromotionFailure
,该参数为true且OldGen最大可用连续空间大于历次晋升到OldGen的平均大小时,则触发一次Minor GC,否则则触发一次Full GC
如果Eden区的对象在GC后存活,则会移动到Survior区,并且GC年龄加一,之后每次在GC中存活都会使GC年龄加一,如果年龄达到一个阈值(默认15,最大15,因为记录年龄的空间只有4位),则会被移动到OldGen。该年龄阈值可以通过MaxTenuringThreshold
配置,在JVM运行中可能动态更改,当积累的某个年龄所占百分比超过了Survior区的TargetSurviorRatio
(默认为50)时,年龄阈值会更改为该年龄和MaxTenuringThreshold
之间更小的值
除此之外,大对象会直接分配到OldGen,在G1垃圾回收器中,根据G1HeapRegionSize
(堆大小)和G1MixedGCSizeTheresholdPercent
(占堆大小的阈值)判断哪些是需要进入OldGen的大对象,在Parrellel Scavenge垃圾回收器中,由虚拟机根据内存情况和历史数据动态决定
二、垃圾回收
1. 怎么判断对象已经死亡
一种是使用引用计数法,当对象每被引用一次,计数加一,计数为0说明对象不再被使用。无法解决循环依赖问题
一种是使用可达性分析算法,从一系列的“GC Roots”出发,向下搜索所有引用到的类,形成引用链,不被引用链关联的类说明需要被回收
GC Roots一般为所有必须活跃的对象,比如:虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区常量引用的对象、方法区中类静态属性引用的对象等
2. 如何判断一个类是无用的
当一个类满足三个条件
- 类的所有实例都被GC
- 类的ClassLoader被GC
- 类的Class不被引用无法通过反射访问
则说明该类是无用的类,JVM可以对其进行回收
3. 垃圾回收算法有哪些
垃圾回收算法主要有以下三种:
- 标记-清除:标记出需要被回收(或不需要回收)的对象,并进行统一回收。缺点:效率不高,容易产生内存碎片
- 标记-复制:将内存分为相同的两块,标记出需要被回收(或不需要回收)的对象,将存活的对象复制到另外一块。缺点:可利用内存减半,对象数量多时复制效率低下
- 标记-整理:标记出需要被回收(或不需要回收)的对象,将存活对象整理到一侧
通常对新生代使用标记-复制算法,付出少量对象的复制完成每次回收,对老年代使用标记-整理算法
4. 垃圾回收器有哪些
- Serial:单线程执行垃圾回收,回收期间会STW(Stop The World),在新生代使用标记-复制,老年代版本叫做Serial Old,使用标记-整理
- ParNew:Serial的多线程版,在新生代执行标记-复制进行垃圾回收,通常与CMS搭配使用
- Parrallel Scavenge:追求高吞吐量(总停顿时间时间尽可能少),自适应调节暂停时间,通常与老年代版本Parrallel Old搭配使用
- CMS:CMS全称Cocurrent Mark Sweep,也就是并发标记清除,目标是低停顿(单次停顿时间尽可能少),先对root进行初始标记,用户线程和GC线程同时运行并进行并发标记,为修正用户线程运行导致的不一致而进行重新标记,最后在进行并发清除
- G1:G1全称Garbage First,会维护优先列表,回收价值更高的区域;能以较高概率满足低停顿同时高吞吐;利用分代收集,无需与其它收集器配合;JDK9开始默认是G1;整体基于标记-整理,局部基于标记-复制;先对GC Roots进行初始标记,然后与应用并发,进行并发标记,随后进行最终标记,最后筛选回收
5. 吞吐量和低停顿为什么是矛盾的
高吞吐指运行用户代码的时间占CPU总运行时间的比值尽可能高,为了追求高吞吐量,会降低GC回收频率,当堆内存达到一定比例才进行回收,同时单次GC时间更长
低停顿指单次GC暂停时间尽可能短,那就需要及时、频繁地进行回收,导致暂停总时间延长
三、类加载
1. Class文件长什么样
- Class文件前四个字节为魔数(Magic Number),固定为0xCAFEBABE,标识是JVM可接收的文件
- 然后是Class文件的大版本号和小版本号,大版本号比如Java8就是8
- 常量池数量和常量池,保存类和接口的全限定名、方法和字段的名称和描述符
- 访问标志,表示是public/abstract/final等信息,是否为接口或枚举,以及父类方法的调用方式等
- 当前类、父类和实现的接口集合
- 字段数量和字段表
- 方法数量和方法表
- 属性数量和属性表
2. 类的加载过程是怎么样的
类的加载过程包括:加载->链接->初始化,其中,链接包括:验证->准备->解析
加载是由类加载器完成的,根据全类名获取该类的字节流,转换为方法区的数据结构,在内存中生成Class对象,作为访问入口
验证是确保Class文件符合约束,包括文件格式验证(魔数、版本号等)、元数据验证(类的继承关系等)、字节码验证(代码语法)、符号引用验证(引用的方法、字段的存在性和合法性等)
准备是指为类的静态变量分配内存和设置初始值
解析是指将符号引用转换为直接引用的过程
初始化是指执行初始化方法clinit()
,之后JVM才真正地执行字节码。当类被主动引用时,会触发初始化,包括new一个对象、通过反射对类进行调用、使用类的静态变量或方法、子类被引用等
3. 类加载器是什么
类加载器的作用是将.class文件加载到JVM中成为Class对象,每一个Java类都有对应的类加载器,数组类的类加载器与数组元素的类加载器相同
除了BootstrapClassLoader是由C++实现的,其它类加载器都是继承自ClassLoader的,
BootstrapClassLoader是最顶层的加载器,用于加载%JAVA_HOME%/bin
下的JDK核心类库
ExtensionClassLoader用于加载%JRE_HOME%/bin/ext
下的jar包,和系统变量java.ext.dirs
指定路径下的所有类
AppClassLoader是面向用户的类加载器,加载classpath下的jar包和类
4. 什么是双亲委派模型
双亲委派模型是指类加载器会先将加载请求传递给父加载器,当父加载器无法加载时,再由自己执行加载
双亲委派模型实现了两个安全目标,一个是避免类的重复加载,一个是避免类的核心API被修改
通常继承ClassLoader可以重写loadClass()
和findClass()
方法,其中loadClass()
方法中实现了双亲委派模型去加载类,findClass()
根据二进制名查找类
如果想要绕过双亲委派模型,可以选择重写loadClass()
,但是Java仍然具备更底层的安全机制,如在preDefineClass()
方法进行类名校验,防止恶意代码定义或加载伪造的核心类
四、JVM参数
1. 堆内存
- 指定堆内存最小值
-Xms
,指定堆内存最大值-Xmx
- 指定新生代内存最小大小和最大大小:
-XX:NewSize
和-XX:MaxNewSize
,前两者一致则-Xmn<young size>[unit]
如-Xmn256m - 指定永久代初始大小和最大大小:
-XX:PermSize
和-XX:MaxPermSize
,指定元空间触发Full GC的阈值和元空间最大大小:-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
2. 垃圾回收
2.1 垃圾回收器选择
- 使用串行垃圾收集:
-XX:+UseSerialGC
- 使用并行垃圾收集:
-XX:+UseParallelGC
- 使用CMS垃圾收集器:
-XX:+UseConcMarkSweepGC
- 使用G1垃圾收集器:
-XX:+UseG1GC
2.2 GC信息打印
- 打印基本GC信息:
-XX:+PrintGCDetails
和-XX:+PrintGCDateStamps
- 打印对象分布:
-XX:+PrintTenuringDistribution
- 打印堆数据:
-XX:+PrintHeapAtGC
- 打印引用相关处理信息:
-XX:+PrintReferenceGC
- 打印STW时间:
-XX:+PrintGCApplicationStoppedTime
3. 处理OOM
- 启用OOM转储文件:
XX:+HeapDumpOnOutOfMemoryError
- OOM转储文件路径:
XX:HeapDumpPath=./java_pid<pid>.hprof
- OOM时启用紧急命令:
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
- 避免长时间无效GC,设置GC开销超出限制:
-XX:+UseGCOverheadLimit