目录
[toc]
Class 文件内容
JVM(Java Virtual Machine)类文件是 Java 程序编译后生成的二进制文件,包含了 Java 程序的字节码。字节码是 JVM 的一种指令集,可以被 JVM 解释执行,从而实现跨平台性。JVM 类文件包含以下内容:
- Magic Number:标志着该文件是一个有效的JVM类文件,它的值为0xCAFEBABE。
- 版本信息:指示了该文件所使用的JVM版本号和Java编译器版本号。
- 常量池(Constant Pool):是一张表格,包含了所有编译期生成的字面量(如字符串、数字)和符号引用(如类、方法、字段名)。
- 访问标志(Access Flags):指示了该类的访问级别(public、private、protected等)和一些其他特性(是否为抽象类、是否为接口等)。
- 类信息:包括类名、父类名、实现的接口等。
- 字段信息(Fields):包含了该类声明的所有成员变量信息,包括名称、类型、访问级别等。
- 方法信息(Methods):包含了该类声明的所有方法信息,包括名称、参数类型、返回类型、访问级别等。
- 属性表(Attributes):包含了一些额外的信息,如源代码行号、局部变量表等。
以上是JVM类文件包含的基本内容,每个文件还可以包含一些其他的信息。JVM通过解析该文件的内容,创建对应的Java类实例,并执行其中的方法,实现了Java程序的运行。
jvm 内存模型
- 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
- 方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。以及字符串。
- 虚拟机栈(JVM Stack):线程私有。存储局部变量表、操作数栈、动态链接、方法出口。
- 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的 Native 方法服务。如 Java 使用 c 或者 c++编写的接口服务时,代码在此区运行。
- 程序计数器(Program Counter Register):线程私有。有些文章也翻译成 PC 寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
如何判断对象是否该回收
描述类加载的过程
类加载的全过程是指 加载、连接(验证、准备、解析)、初始化、卸载 五个阶段。
五个阶段并不是按照严格先后顺序执行,是交叉执行的。
5.3.1 加载
类的加载阶段需要完成:
- 利用全类名获取此类的二进制字节流(可以从本地、网络、运行时动态生成等方式获取)
- 将字节流代表的结构加载的 JVM 中的方法区(共享的)
- 在内存中(堆中)生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
数组类不通过类加载器创建,直接在内存中动态构造,但数组类的元素类型(指去掉所有维度后的类型)需要类加载器来完成。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
5.3.2 验证(连接)
主要是对 Class 文件进行格式验证和语义验证,是否满足虚拟机规范。确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要是对其进行文件格式验证、语义合法性验证等。
5.3.3 准备(连接)
准备阶段主要是为类中定义的变量(静态变量)分配内存并设置初始默认值(默认为 0、null、false、常量之类的)。
准备阶段是针对类变量(存储在方法区中),而不是实例化变量(存储在 Java 堆中),实例化变量的初值设置在对象的初始化阶段完成。
实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
5.3.4 解析(连接)
将常量池中的符号引用替换成直接引用的过程。
符号引用:一组用来描述所引用目标的符号(字面量)。
直接引用:可以直接指向目标的指针、相对偏移量或者能间接定位目标的句柄。
5.3.5 初始化
初始化阶段是虚拟机执行 <clinit>()
方法,对静态变量进行赋值、执行静态语句块。(clinit 方法由 Javac 编译时通过收集类中所有的静态语句块和赋值操作而自动生成的)的过程。虚拟机会保证一个类的 <clinit>()
方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>()
方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>()
方法完毕时被唤醒,但不会再次执行该方法。
初始化时类加载过程中的最后一个步骤,JVM 此时真正开始执行类中编写的 Java
代码,将主导权交给应用程序。
触发类/接口执行初始化的六大条件见 [5.2 类初始化的时机](# 5.2 类初始化的时机) 。
接口中不可以使用静态语句块,但仍然有成员变量(默认public static final修饰)初始化的赋值操作,因此接口与类一样都会生成
<clinit>()
方法。但接口与类不同的是,执行接口的<clinit>()
方法不需要先执行父接口的<clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()
方法。
参考: Java中init(实例构造器)和clinit(类构造器)区别 (此处的初始化需要和 new 一个对象调用的构造方法有所区别!) 注意:类中定义的静态变量有两次赋值过程:一是在 准备 阶段赋予默认初值,而是在 初始化 阶段赋予程序员定义的值。
类的初始化顺序
了解哪些垃圾回收算法,介绍一下
基础:标记 - 清除算法
- 算法描述:
- 先标记出所有需要回收的对象(图中深色区域);
- 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……)。
- 不足:
- 效率问题:标记和清理两个过程的效率都不高。
- 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC。
解决效率问题 :复制算法
- 算法描述:
- 将可用内存分为大小相等的两块,每次只使用其中一块;
- 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
- 不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。
- 节省内存的方法:
- 新生代中的对象 98% 都是朝生夕死的,所以不需要按照 1:1 的比例对内存进行划分;
- 把内存划分为:
- 1 块比较大的 Eden 区;
- 2 块较小的 Survivor 区;
- 每次使用 Eden 区和 1 块 Survivor 区;
- 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空;
- JVM 参数设置:
-XX:SurvivorRatio=8
表示Eden 区大小 / 1 块 Survivor 区大小 = 8
。
解决空间碎片问题:标记 - 整理算法
- 算法描述:
- 标记方法与 “标记 - 清除算法” 一样;
- 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
- 不足: 存在效率问题,适合老年代。
进化:分代收集算法
- 新生代: GC 过后只有少量对象存活 —— 复制算法
- 老年代: GC 过后对象存活率高 —— 标记 - 整理算法
介绍一下双亲委派机制,怎么打破
你确定你真的理解”双亲委派”了吗?! - HollisChuang - 博客园 (cnblogs.com)
分代回收算法的详细过程
JVM分代回收机制和垃圾回收算法_JVM_Ayue、_InfoQ写作社区
垃圾收集器
垃圾收集器就是内存回收操作的具体实现,HotSpot 里足足有 7 种,为啥要弄这么多,因为它们各有各的适用场景。有的属于新生代收集器,有的属于老年代收集器,所以一般是搭配使用的(除了万能的 G1)。关于它们的简单介绍以及分类请见下图。
Serial / ParNew 搭配 Serial Old 收集器
Serial 收集器是虚拟机在 Client 模式下的默认新生代收集器,它的优势是简单高效,在单 CPU 模式下很牛。
ParNew 收集器就是 Serial 收集器的多线程版本,虽然除此之外没什么创新之处,但它却是许多运行在 Server 模式下的虚拟机中的首选新生代收集器,因为除了 Serial 收集器外,只有它能和 CMS 收集器搭配使用。
Parallel 搭配 Parallel Scavenge 收集器
首先,这俩货肯定是要搭配使用的,不仅仅如此,它俩还贼特别,它们的关注点与其他收集器不同,其他收集器关注于尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目的是达到一个可控的吞吐量。
吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )
因此,Parallel Scavenge 收集器不管是新生代还是老年代都是多个线程同时进行垃圾收集,十分适合于应用在注重吞吐量以及 CPU 资源敏感的场合。
可调节的虚拟机参数:
-XX:MaxGCPauseMillis
:最大 GC 停顿的秒数;-XX:GCTimeRatio
:吞吐量大小,一个 0 ~ 100 的数,最大 GC 时间占总时间的比率 = 1 / (GCTimeRatio + 1)
;-XX:+UseAdaptiveSizePolicy
:一个开关参数,打开后就无需手工指定-Xmn
,-XX:SurvivorRatio
等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自行调整。
CMS 收集器
参数设置:
-XX:+UseCMSCompactAtFullCollection
:在 CMS 要进行 Full GC 时进行内存碎片整理(默认开启)-XX:CMSFullGCsBeforeCompaction
:在多少次 Full GC 后进行一次空间整理(默认是 0,即每一次 Full GC 后都进行一次空间整理)
关于 CMS 使用 标记 - 清除 算法的一点思考:
之前对于 CMS 为什么要采用 标记 - 清除 算法十分的不理解,既然已经有了看起来更高级的 标记 - 整理 算法,那 CMS 为什么不用呢?最近想了想,感觉可能是这个原因,不过也不是很确定,只是个人的一种猜测。
标记 - 整理 会将所有存活对象向一端移动,然后直接清理掉边界以外的内存。这就意味着需要一个指针来维护这个分隔存活对象和无用空间的点,而我们知道 CMS 是并发清理的,虽然我们启动了多个线程进行垃圾回收,不过如果使用 标记 - 整理 算法,为了保证线程安全,在整理时要对那个分隔指针加锁,保证同一时刻只有一个线程能修改它,加锁的这一过程相当于将并行的清理过程变成了串行的,也就失去了并行清理的意义了。
所以,CMS 采用了标记 - 清除算法。
存在的问题
CMS 相比前面讲到的回收器是比较优秀的,主要就是体现在它的并发和低停顿,但同时它也存在一些缺点,主要表现在这 3 个方面: CPU 敏感:CMS 对处理器资源敏感,因为采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大。 浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为浮动垃圾。 并发模式失败:由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值 (92%) 如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 简单来说就是在老年代内存要满的时候会进行 Full GC,但在 Full GC 的过程中可能会有新的对象进入老年代,那此时必定会进入 STW 的状态,并且 CMS 会自动切换到用 Serial old 垃圾收集器来回收。Serial 是一个单线程的垃圾回收器。那这种情况出现是不是会严重降低我们的执行效率? 内存碎片:CMS 采用的是标记 - 清除算法,因此会导致产生不连续的内存碎片。 总体来说,CMS 是 JVM 推出了第一款并发垃圾收集器,所以还是非常有代表性。但是最大的问题是 CMS 采用了标记清除算法,所以会有内存碎片,当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个参数:-XX:+UseCMSCompactAtFullCollection,一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程。
那为什么 CMS 采用标记-清除?
在实现并发的垃圾回收时,如果采用标记整理算法,那么还涉及到对象的移动(对象的移动必定涉及到引用的变化,这个需要暂停业务线程来处理栈信息,这样使得并发收集的暂停时间更长,而 CMS 的主要目的就是为了降低 STW 的时间),所以使用简单的标记-清除算法才可以降低 CMS 的 STW 的时间。该垃圾回收器适合回收堆空间几个 G~ 20G 左右。
G1 收集器
G1(Garbage First),被 Oracle 官方称为全功能的垃圾收集器。
设计思想
随着 JVM 中内存的增大,STW 的时间成为 JVM 急迫解决的问题,但是如果按照传统的分代模型,总跳不出 STW 时间不可预测这点。为了实现 STW 的时间可预测,首先要有一个思想上的改变。G1 将堆内存化整为零,将堆内存划分成多个大小相等独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region 到底是什么?
Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize
设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的进行回收大多数情况下都把 Humongous Region 作为老年代的一部分来进行看待。
因此,对于 G1 最主要的特点是 G1 的内存区域是不固定的。如下 E 变为 S,O 变为了 E:
运行过程
CMS 和 G1 的区别
- CMS 只负责老年代,通常和 ParNew 配合;G1 新生代和老年代都要负责
- CMS 标记清除;G1 标记整理
- G1 引入了 region 的概念,每个 region 的分类可以变化
- 第二次标记的时候 G1 会暂停用户线程,CMS 则是和用户线程并发执行
- 大对象处理:CMS 直接放入老年代;G1 则是横跨多个 region 来存放
哪些操作可能导致 out of memory
OOM 的 error 类型
java.lang.OutOfMemoryError: Java heap space
Java堆 内存溢出
,是最常见的一种情况。原因:
- 一般由于
内存泄露
或者堆的大小设置不当
引起。解决:
对于
内存泄露
:需要通过内存监控软件,查找程序中的泄露代码,对于
堆大小
,可以通过虚拟机VM参数进行修改:
-Xms1024M -Xmx2048M
java.lang.OutOfMemoryError: PermGen space
方法区
溢出
PermGen space
的全称是Permanent Generation space
(指内存的永久保存区域
)。原因:
- 加载了大量的Class(类)
- 在单一的 Tomcat 实例下运行多个 Web 应用程序(大量 jsp 页面)
- 在运行的Tomcat实例中反复“热部署”Web应用程序
- 采用
cglib
等反射机制- 过多的常量也会导致方法区溢出,尤其是字符串
解决:
- 修改
方法区
的大小(缺省默认为64M):
-XX:PermSize=128M -XX:MaxPermSize=256M
java.lang.StackOverflowError
- 不会抛出
OOM Error
,但是也是比较常见的Java内存溢出
情况。Java虚拟机栈
or本地方法栈
,在栈深度溢出(线程请求的栈深度大于虚拟机所允许的深度)
,将抛出StackOverflowError
异常原因:
- 最常见的:无限递归循环调用(死循环)
- 栈深度溢出
- 执行了大量方法,导致线程栈空间耗尽
- 方法内声明了大量的局部变量
解决:
- 通过程序抛出的异常堆栈,利用内存监控软件,查找程序中执行死循环的代码
- 排查是否存在类之间的循环依赖
- 设置JVM启动参数
-Xss
,增加线程栈内存空间
- 线程栈的默认大小依赖于操作系统、JVM 版本和供应商
OOM分析
Heap Dump(堆转储文件)
是一个Java进程在某个时间点上的内存快照。
Heap Dump
是有着多种类型的。
不过总体上Heap Dump
在触发快照的时候都保存了Java对象
和类
的信息。
-
通常在写
Heap Dump
文件前会触发一次FullGC
,所以Heap Dump
文件中保存的是FullGC后留下
的对象信息。 -
配置参数:
-XX:+HeapDumpOnOutOfMemoryError
,可以在发生OutOfMemoryError
后获取到一份HPROF
二进制Heap Dump
文件,生成的文件会直接写入到工作目录。
注意:该方法需要
JDK5
以上版本。
转存堆内存
信息后,需要对文件进行分析,可以使用以下工具,从而找到OOM
的原因:
-
JProfiler:IDEA继承了对应插件,详细参考 《Dump分析实战》
-
MAT(Memory Analyzer Tool):基于Eclipse RCP的内存分析工具。具体使用参考:www.eclipse.org/mat/
对象分代,及其在堆中是如何分布的
三色标记法
一文带你弄懂 JVM 三色标记算法! - 陈树义 - 博客园 (cnblogs.com)
GC Roots 包括哪些对象
- Java 中可作为 “GC Root” 的对象:
- 栈中(本地变量表中的reference)
- 虚拟机栈中,栈帧中的本地变量表引用的对象;
- 本地方法栈中,JNI 引用的对象(native方法);
- 方法区中
- 类的静态属性引用的对象;
- 常量引用的对象;
- 所有被同步锁持有的对象
- 栈中(本地变量表中的reference)
什么情况下新生代会成为老年代
长期存活的对象将进入老年代
- 固定对象年龄判定: 虚拟机给每个对象定义一个年龄计数器,对象每在 Survivor 中熬过一次 Minor GC,年龄 +1,达到
-XX:MaxTenuringThreshold
设定值后,会被晋升到老年代,-XX:MaxTenuringThreshold
默认为 15; - 动态对象年龄判定: Survivor 中有相同年龄的对象的空间总和大于 Survivor 空间的一半,那么,年龄大于或等于该年龄的对象直接晋升到老年代。
空间分配担保
新生代采用的是复制算法清理内存,每一次 Minor GC,虚拟机会将 Eden 区和其中一块 Survivor 区的存活对象复制到另一块 Survivor 区,但 当出现大量对象在一次 Minor GC 后仍然存活的情况时,Survivor 区可能容纳不下这么多对象,此时,就需要老年代进行分配担保,即将 Survivor 无法容纳的对象直接进入老年代。
JVM 常用参数
线上 jvm 必须配置 -XX:+HeapDumpOnOutOfMemoryError
,-XX:HeapDumpPath=/path/heap/dump
。因为这样就是说 OOM 的时候自动导出一份内存快照,你就可以分析发生 OOM 时的内存快照了,到底是哪里出现的问题。
常用参数
Parallel 常用参数
CMS 常用参数
G1 常用参数
方法区演变
JDK 1.6 及之前:
有永久代(permanent generation),静态变量存放在永久代上。
JDK 1.7:
有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中。
JDK 1.8及以后:
无永久代,类型信息、字段、方法、常量、保存在本地内存的元空间,但字符串常量池、静态变量仍在堆上。
永久代为什么要被元空间替换?
(1)为永久代设置空间大小是很难确定的。
元空间不在虚拟机中,而是使用本地内存;一般情况下元空间的大小仅受本地内存限制。
而永久代在虚拟机中,往往不能很好的确定其大小。
(2)对永久代进行调优是很困难的。