JVM的内存管理、垃圾回收、类加载和参数调优

前言

在面试中通常会考察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. 一个对象的内存布局如何

一个对象在内存中由三部分构成,分别是:对象头、实际数据和对齐填充

对象头可以由三部分构成

  1. 第一部分是对象运行时的信息,包括对象的哈希码、GC年龄信息、线程持有的锁、锁状态标志等信息
  2. 第二部分是类型指针,指向类的元数据信息,也就是Class对象,类型指针可以由8字节被压缩为4字节(在JDK8默认开启)
  3. 第三部分只有数组拥有,用于记录数组长度

实际数据是指对象实际的成员变量值,会被重排对齐以提高内存访问的速度

对齐填充是指整个对象的大小需要是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. 如何判断一个类是无用的

当一个类满足三个条件

  1. 类的所有实例都被GC
  2. 类的ClassLoader被GC
  3. 类的Class不被引用无法通过反射访问

则说明该类是无用的类,JVM可以对其进行回收

3. 垃圾回收算法有哪些

垃圾回收算法主要有以下三种:

  1. 标记-清除:标记出需要被回收(或不需要回收)的对象,并进行统一回收。缺点:效率不高,容易产生内存碎片
  2. 标记-复制:将内存分为相同的两块,标记出需要被回收(或不需要回收)的对象,将存活的对象复制到另外一块。缺点:可利用内存减半,对象数量多时复制效率低下
  3. 标记-整理:标记出需要被回收(或不需要回收)的对象,将存活对象整理到一侧

通常对新生代使用标记-复制算法,付出少量对象的复制完成每次回收,对老年代使用标记-整理算法

4. 垃圾回收器有哪些

  1. Serial:单线程执行垃圾回收,回收期间会STW(Stop The World),在新生代使用标记-复制,老年代版本叫做Serial Old,使用标记-整理
  2. ParNew:Serial的多线程版,在新生代执行标记-复制进行垃圾回收,通常与CMS搭配使用
  3. Parrallel Scavenge:追求高吞吐量(总停顿时间时间尽可能少),自适应调节暂停时间,通常与老年代版本Parrallel Old搭配使用
  4. CMS :CMS全称Cocurrent Mark Sweep,也就是并发标记清除,目标是低停顿(单次停顿时间尽可能少),先对root进行初始标记 ,用户线程和GC线程同时运行并进行并发标记 ,为修正用户线程运行导致的不一致而进行重新标记 ,最后在进行并发清除
  5. G1 :G1全称Garbage First,会维护优先列表,回收价值更高的区域;能以较高概率满足低停顿同时高吞吐;利用分代收集,无需与其它收集器配合;JDK9开始默认是G1;整体基于标记-整理,局部基于标记-复制;先对GC Roots进行初始标记 ,然后与应用并发,进行并发标记 ,随后进行最终标记 ,最后筛选回收

5. 吞吐量和低停顿为什么是矛盾的

高吞吐指运行用户代码的时间占CPU总运行时间的比值尽可能高,为了追求高吞吐量,会降低GC回收频率,当堆内存达到一定比例才进行回收,同时单次GC时间更长

低停顿指单次GC暂停时间尽可能短,那就需要及时、频繁地进行回收,导致暂停总时间延长

三、类加载

1. Class文件长什么样

  1. Class文件前四个字节为魔数(Magic Number),固定为0xCAFEBABE,标识是JVM可接收的文件
  2. 然后是Class文件的大版本号小版本号,大版本号比如Java8就是8
  3. 常量池数量常量池,保存类和接口的全限定名、方法和字段的名称和描述符
  4. 访问标志,表示是public/abstract/final等信息,是否为接口或枚举,以及父类方法的调用方式等
  5. 当前类父类 和实现的接口集合
  6. 字段数量字段表
  7. 方法数量方法表
  8. 属性数量属性表

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. 堆内存

  1. 指定堆内存最小值-Xms,指定堆内存最大值-Xmx
  2. 指定新生代内存最小大小和最大大小:-XX:NewSize-XX:MaxNewSize,前两者一致则-Xmn<young size>[unit]如-Xmn256m
  3. 指定永久代初始大小和最大大小:-XX:PermSize-XX:MaxPermSize,指定元空间触发Full GC的阈值和元空间最大大小:-XX:MetaspaceSize-XX:MaxMetaspaceSize

2. 垃圾回收

2.1 垃圾回收器选择

  1. 使用串行垃圾收集:-XX:+UseSerialGC
  2. 使用并行垃圾收集:-XX:+UseParallelGC
  3. 使用CMS垃圾收集器:-XX:+UseConcMarkSweepGC
  4. 使用G1垃圾收集器:-XX:+UseG1GC

2.2 GC信息打印

  1. 打印基本GC信息:-XX:+PrintGCDetails-XX:+PrintGCDateStamps
  2. 打印对象分布:-XX:+PrintTenuringDistribution
  3. 打印堆数据:-XX:+PrintHeapAtGC
  4. 打印引用相关处理信息:-XX:+PrintReferenceGC
  5. 打印STW时间:-XX:+PrintGCApplicationStoppedTime

3. 处理OOM

  1. 启用OOM转储文件:XX:+HeapDumpOnOutOfMemoryError
  2. OOM转储文件路径:XX:HeapDumpPath=./java_pid<pid>.hprof
  3. OOM时启用紧急命令:-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
  4. 避免长时间无效GC,设置GC开销超出限制:-XX:+UseGCOverheadLimit
相关推荐
缺点内向3 小时前
Java:创建、读取或更新 Excel 文档
java·excel
带刺的坐椅3 小时前
Solon v3.4.7, v3.5.6, v3.6.1 发布(国产优秀应用开发框架)
java·spring·solon
四谎真好看5 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程5 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t5 小时前
ZIP工具类
java·zip
lang201509285 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan6 小时前
第10章 Maven
java·maven
百锦再7 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说7 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多7 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring