JVM知识点汇总
JVM是什么?
- JVM它是 Java Virtual Machine 的缩写,主要是通过在实际计算机模仿各种计算机功能来实现的。(概念)
- 由堆、方法区、栈、本地方法栈、程序计算器等部分组成的,其中方法回收堆和方法区是共享区,也就是谁都可以使用,而栈和程序计算器、本地方法栈区是归JVM的。(组成)
- Java能够被称为“一次编译,到处运行”的原因就是Java屏蔽了很多的操作系统平台相关信息,使得Java只需要生成在JVM虚拟机运行的目标代码也就是所说的字节码,就可以在多种平台运行。(特点:平台无关性)
JRE、JDK和JVM的关系?
JRE(Java Runtime Environment, Java运行环境)是Java平台,所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。
JDK(Java Development Kit,Java开发工具包)是用来编译、调试Java程序的开发工具包。包括Java工具(javac/java/jdb等)和Java基础的类库(java API )。
JVM(Java Virtual Machine, Java虚拟机)是JRE的一部分。JVM主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。Java语言是跨平台运行的,不同的操作系统会有不同的JVM映射规则,使之与操作系统无关,完成跨平台性。
JVM生命周期(何时启动,何时退出)
Java实例对应一个独立运行的Java程序(进程级别)
-
启动。启动一个Java程序,一个JVM实例就产生。拥有public static void main(String[] args)函数的class可以作为JVM实例运行的起点。
-
运行。main()作为程序初始线程的起点,任何其他线程均可由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM使用,程序可以指定创建的线程为守护线程。
-
消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
JVM执行引擎实例则对应了属于用户运行程序线程它是线程级别的。
可以描述一下 class 文件的结构吗?
Class 文件包含了 Java 虚拟机的指令集、符号表、辅助信息的字节码(Byte Code),是实现跨操作系统和语言无关性的基石之一。
一个 Class 文件定义了一个类或接口的信息,是以 8 个字节为单位,没有分隔符,按顺序紧凑排在一起的二进制流。
用 “无符号数” 和 “表” 组成的伪结构来存储数据。
- 无符号数:基本数据类型,用来描述数字、索引引用、数量值、字符串值,如u1、u2 分别表示 1 个字节、2 个字节
- 表:无符号数和其他表组成,命名一般以 “_info” 结尾
组成部分
-
魔数 Magic Number
Class 文件头 4 个字节,0xCAFEBABE
作用是确定该文件是 Class 文件 -
版本号
4 个字节,前 2 个是次版本号 Minor Version,后 2 个主版本号 Major Version
从 45 (JDK1.0) 开始,如 0x00000032 转十进制就是 50,代表 JDK 6
低版本的虚拟机跑不了高版本的 Class 文件 -
常量池
常量容量计数值(constant_pool_count),u2,从 1 开始。如 0x0016 十进制 22 代表有 21 项常量
每项常量都是一个表,目前 17 种
特点:Class 文件中最大数据项目之一、第一个出现表数据结构 -
访问标志
2 个字节,表示类或接口的访问标志 -
类索引、父类索引、接口索引集合
类索引(this_class)、父类索引(super_class),u2
接口索引集合(interfaces),u2 集合
类索引确定类的全限定名、父类索引确定父类的全限定名、接口索引集合确定实现接口
索引值在常量池中查找对应的常量 -
字段表(field_info)集合
描述接口或类申明的变量
fields_count,u2,表示字段表数量;后面接着相应数量的字段表
9 种字段访问标志 -
方法表(method_info)集合
描述接口或类申明的方法
methods_count,u2,表示方法表数量;后面接着相应数量的方法表
12 种方法访问标志
方法表结构与字段表结构一致 -
属性表(attribute_info)集合
class 文件、字段表、方法表可携带属性集合,描述特有信息
预定义 29 项属性,可自定义写入不重名属性
Java虚拟机在执行Java程序的过程中的内存区域划分是怎样的
根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
-
程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
-
虚拟机栈:虚拟机栈中执行每个方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
-
本地方法栈:与虚拟机栈发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
-
堆:堆是Java对象的存储区域,任何用new字段分配的Java对象实例和数组,都被分配在堆上,Java堆可使用-Xms -Xmx进行内存控制,值得一提的是从JDK1.7版本之后,运行时常量池从方法区移到了堆上。
-
方法区:它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,方法区在JDK1.7版本及以前被称为永久代,从JDK1.8永久代被移除。
JVM类加载顺序是怎样的?什么事双亲委派模型?
这里的双亲会造成理解误差,实际上只是去委派给父级加载器或是父级的父级加载器去进行加载。这里的parents不应该是指双亲,而是多级的父类层层向上传递的意思。
JDK 9 之前
- 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
- 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
- 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
- 自定义类加载器
JDK 9 开始 Extension ClassLoader 被 Platform ClassLoader 取代,启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader
类加载代码逻辑
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载时
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
列举一些你知道的打破双亲委派机制的例子。为什么要打破?
-
JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
-
Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
-
OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
-
JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。
强引用、软引用、弱引用、虚引用是什么,有什么区别?
-
强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)
-
软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
用处: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
- 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
2.如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
如下代码:
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
}else{
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
-
弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现
-
虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现
JVM 如何确定垃圾对象?
判断对象是否可回收的算法有两种:
- Reference Counting GC,引用计数算法
- Tracing GC,可达性分析算法
JVM 各厂商采用的基本都是可达性分析算法,通过 GC Roots 来判定对象是否存活,从 GC Roots 向下追溯、搜索,会产生 Reference Chain。当一个对象不能和任何一个 GC Root 产生关系时,就判定为垃圾。
软引用和弱引用,也会影响对象的回收。内存不足时会回收软引用对象;GC 时会回收弱引用对象。
哪些是 GC Roots?
-
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
-
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
-
在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
-
在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
-
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
-
所有被同步锁(synchronized关键字)持有的对象。
-
反映 Java 虚拟机内部情况的 JMXBean、JVMTI中注册的回调、本地代码缓存等。
谈谈你知道的垃圾回收算法
大部分垃圾收集器遵从了分代收集(Generational Collection)理论。
针对新生代与老年代回收垃圾内存的特点,提出了 3 种不同的算法:
-
标记-清除算法(Mark-Sweep)
标记需回收对象,统一回收;或标记存活对象,回收未标记对象。
缺点:
大量对象需要标记与清除时,效率不高
标记、清除产生的大量不连续内存碎片,导致无法分配大对象 -
标记-复制算法(Mark-Copy)
可用内存等分两块,使用其中一块 A,用完将存活的对象复制到另外一块 B,一次性清空 A,然后改分配新对象到 B,如此循环。
缺点:
不适合大量对象不可回收的情况,换句话说就是仅适合大量对象可回收,少量对象需复制的区域
只能使用内存容量的一半,浪费较多内存空间 -
标记-整理算法(Mark-Compact)
标记存活的对象,统一移到内存区域的一边,清空占用内存边界以外的内存。
缺点:
移动大量存活对象并更新引用,需暂停程序运行
说一下垃圾分代收集的过程
分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。
新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下:
-
在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
-
Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
-
移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代
-
Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%
-
Survivor 区内存不足会发生担保分配
-
超过指定大小的对象可以直接进入老年代
-
Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC
-
FullGC,整个堆的垃圾收集,触发条件:
- 每次晋升到老年代的对象平均大小>老年代剩余空间
- MinorGC后存活的对象超过了老年代剩余空间
- 元空间不足
- System.gc() 可能会引起
- CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成
- 堆内存分配很大的对象
谈谈你知道的垃圾收集器
Serial
特点:
- JDK 1.3 开始提供
- 新生代收集器
- 无线程交互开销,单线程收集效率最高
- 进行垃圾收集时需要暂停用户线程
- 适用于客户端,小内存堆的回收
ParNew
特点:
- 是 Serial 收集器的多线程并行版
- JDK 7 之前首选的新生代收集器
- 第一款支持并发的收集器,首次实现垃圾收集线程与用户线程基本上同时工作
- 除 Serial 外,只有它能与 CMS 配合
Parallel Scavenge
特点:
- 新生代收集器
- 标记-复制算法
- 多线程并行收集器
- 追求高吞吐量,即最小的垃圾收集时间
- 可以配置最大停顿时间、垃圾收集时间占比
- 支持开启垃圾收集自适应调节策略,追求适合的停顿时间或最大的吞吐量
Serial Old
特点:
- 与 Serial 类似,是 Serial 收集器的老年代版本
- 使用标记-整理算法
Parallel Old
特点:
- JDK 6 开始提供
- Parallel Scavenge 的老年代版
- 支持多线程并发收集
- 标记-整理算法
- Parallel Scavenge + Parallel Old 是一个追求高吞吐量的组合
CMS
特点:
- 标记-清除算法
- 追求最短回收停顿时间
- 多应用于关注响应时间的 B/S 架构的服务端
- 并发收集、低停顿
- 占用一部分线程资源,应用程序变慢,吞吐量下降
- 无法处理浮动垃圾,可能导致 Full GC
- 内存碎片化问题
G1
特点:
- JDK 6 开始实验,JDK 7 商用
- 面向服务端,JDK 9 取代 Parallel Scavenge + Parallel Old
- 结合标记-整理、标记-复制算法
- 首创局部内存回收设计思路
- 基于 Region 内存布局,采用不同策略实现分代
- 不再使用固定大小、固定数量的堆内存分代区域划分
- 优先回收价收益最大的 Region
- 单个或多个 Humongous 区域存放大对象
- 使用记忆集解决跨 Region 引用问题
- 复杂的卡表实现,导致更高的内存占用,堆的 10%~20%
- 全功能垃圾收集器
- 追求有限的时间内最高收集效率、延迟可控的情况下最高吞吐量
- 追求应付内存分配速率,而非一次性清掉所有垃圾内存
- 适用于大内存堆
Shenandoah
特点:
- 追求低延迟,停顿 10 毫秒以内
- OpenJDK 12 新特性,RedHat 提供
- 连接矩阵代替记忆集,降低内存使用与伪共享问题出现概率
ZGC
特点:
- JDK 11 新加的实验性质的收集器
- 追求低延迟,停顿 10 毫秒以内
- 基于 Region 内存布局
- 未设分代
- 读屏障、染色指针、内存多重映射实现可并发的标记-整理算法
- 染色指针和内存多重映射设计精巧,解决部分性能问题,但降低了可用最大内存、操作系统受限、只支持 32 位、不支持压缩指针等
- 成绩亮眼、性能彪悍
生产环境用的什么JDK?如何配置的垃圾收集器?
Oracle JDK 1.8
JDK 1.8 中有 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,默认使用 Parallel Scavenge + Parallel Old。
- Serial 系列是单线程垃圾收集器,处理效率很高,适合小内存、客户端场景使用,使用参数 -XX:+UseSerialGC 显式启用。
- Parallel 系列相当于并发版的 Serial,追求高吞吐量,适用于较大内存并且有多核CPU的环境,默认或显式使用参数 -XX:+UseParallelGC 启用。可以使用 -XX:MaxGCPauseMillis 参数指定最大垃圾收集暂停毫秒数,收集器会尽量达到目标;使用 -XX:GCTimeRatio 指定期望吞吐量大小,默认 99,用户代码运行时间:垃圾收集时间=99:1。
- CMS,追求垃圾收集暂停时间尽可能短,适用于服务端较大内存且多 CPU 的应用,使用参数 -XX:+UseConcMarkSweepGC 显式开启,会同时作用年轻代与老年代,但有浮动垃圾和内存碎片化的问题。
- G1,主要面向服务端应用的垃圾收集器,适用于具有大内存的多核 CPU 的服务器,追求较小的垃圾收集暂停时间和较高的吞吐量。首创局部内存回收设计思路,采用不同策略实现分代,不再使用固定大小、固定数量的堆内存分代区域划分,而是基于 Region 内存布局,优先回收价收益最大的 Region。使用参数 -XX:+UseG1GC 开启。
我们生产环境使用了 G1 收集器,相关配置如下
-Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:G1HeapRegionSize 未指定
核心思路:
每个内存区域设置上限,避免溢出
堆设置为操作系统的 70%左右,超过 8 G,首选 G1
根据老年代对象提升速度,调整新生代与老年代之间的内存比例
等过 GC 信息,针对项目敏感指标优化,比如访问延迟、吞吐量等
G1适合8/16G以上的内存使用,原因在于G1rescan更快,清除垃圾时虽然是stop the world但是可控,CMS虽然是并发但是不可控,大块内存要回收会影响到应用程序的性能。另外由于G1在清理垃圾时使用STW,所以可以采用标记整理算法,没有内存碎片问题