目录

[toc]

Class 文件内容

JVM(Java Virtual Machine)类文件是 Java 程序编译后生成的二进制文件,包含了 Java 程序的字节码。字节码是 JVM 的一种指令集,可以被 JVM 解释执行,从而实现跨平台性。JVM 类文件包含以下内容:

  1. Magic Number:标志着该文件是一个有效的JVM类文件,它的值为0xCAFEBABE。
  2. 版本信息:指示了该文件所使用的JVM版本号和Java编译器版本号。
  3. 常量池(Constant Pool):是一张表格,包含了所有编译期生成的字面量(如字符串、数字)和符号引用(如类、方法、字段名)。
  4. 访问标志(Access Flags):指示了该类的访问级别(public、private、protected等)和一些其他特性(是否为抽象类、是否为接口等)。
  5. 类信息:包括类名、父类名、实现的接口等。
  6. 字段信息(Fields):包含了该类声明的所有成员变量信息,包括名称、类型、访问级别等。
  7. 方法信息(Methods):包含了该类声明的所有方法信息,包括名称、参数类型、返回类型、访问级别等。
  8. 属性表(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算法.png

解决效率问题 :复制算法

  • 算法描述:
    • 将可用内存分为大小相等的两块,每次只使用其中一块;
    • 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
  • 不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代
  • 节省内存的方法:
    • 新生代中的对象 98% 都是朝生夕死的,所以不需要按照 1:1 的比例对内存进行划分;
    • 把内存划分为:
      • 1 块比较大的 Eden 区;
      • 2 块较小的 Survivor 区;
    • 每次使用 Eden 区和 1 块 Survivor 区;
    • 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空;
    • JVM 参数设置:-XX:SurvivorRatio=8 表示 Eden 区大小 / 1 块 Survivor 区大小 = 8

复制GC算法.png

解决空间碎片问题:标记 - 整理算法

  • 算法描述:
    • 标记方法与 “标记 - 清除算法” 一样;
    • 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
  • 不足: 存在效率问题适合老年代

标记整理GC算法.png

进化:分代收集算法

  • 新生代: GC 过后只有少量对象存活 —— 复制算法
  • 老年代: GC 过后对象存活率高 —— 标记 - 整理算法

介绍一下双亲委派机制,怎么打破

你确定你真的理解”双亲委派”了吗?! - HollisChuang - 博客园 (cnblogs.com)

分代回收算法的详细过程

JVM分代回收机制和垃圾回收算法_JVM_Ayue、_InfoQ写作社区

垃圾收集器

垃圾收集器就是内存回收操作的具体实现,HotSpot 里足足有 7 种,为啥要弄这么多,因为它们各有各的适用场景。有的属于新生代收集器,有的属于老年代收集器,所以一般是搭配使用的(除了万能的 G1)。关于它们的简单介绍以及分类请见下图。

垃圾收集器们.png

Serial / ParNew 搭配 Serial Old 收集器

Serial_ParNew收集器.jpg

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 收集器

CMS垃圾收集器.png

CMS收集器运行示意图.jpg

参数设置:

  • -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垃圾收集器.png

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:

运行过程

G1收集器运行示意图.jpg

CMS 和 G1 的区别

  • CMS 只负责老年代,通常和 ParNew 配合;G1 新生代和老年代都要负责
  • CMS 标记清除;G1 标记整理
  • G1 引入了 region 的概念,每个 region 的分类可以变化
  • 第二次标记的时候 G1 会暂停用户线程,CMS 则是和用户线程并发执行
  • 大对象处理:CMS 直接放入老年代;G1 则是横跨多个 region 来存放

哪些操作可能导致 out of memory

OOM 的 error 类型

  1. java.lang.OutOfMemoryError: Java heap space
  • Java堆 内存溢出,是最常见的一种情况。

原因:

  • 一般由于内存泄露或者堆的大小设置不当引起。

解决:

  • 对于内存泄露:需要通过内存监控软件,查找程序中的泄露代码,

  • 对于堆大小,可以通过虚拟机VM参数进行修改:

    • -Xms1024M -Xmx2048M
  1. java.lang.OutOfMemoryError: PermGen space
  • 方法区溢出
    • PermGen space的全称是Permanent Generation space(指内存的永久保存区域)。

原因:

  • 加载了大量的Class(类)
  • 在单一的 Tomcat 实例下运行多个 Web 应用程序(大量 jsp 页面)
  • 在运行的Tomcat实例中反复“热部署”Web应用程序
  • 采用cglib等反射机制
  • 过多的常量也会导致方法区溢出,尤其是字符串

解决:

  • 修改方法区的大小(缺省默认为64M):
    • -XX:PermSize=128M -XX:MaxPermSize=256M
  1. 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的原因:

  1. JProfiler:IDEA继承了对应插件,详细参考 《Dump分析实战》

  2. MAT(Memory Analyzer Tool):基于Eclipse RCP的内存分析工具。具体使用参考:www.eclipse.org/mat/

对象分代,及其在堆中是如何分布的

三色标记法

一文带你弄懂 JVM 三色标记算法! - 陈树义 - 博客园 (cnblogs.com)

GC Roots 包括哪些对象

  • Java 中可作为 “GC Root” 的对象:
    • 栈中(本地变量表中的reference)
      • 虚拟机栈中,栈帧中的本地变量表引用的对象;
      • 本地方法栈中,JNI 引用的对象(native方法);
    • 方法区中
      • 类的静态属性引用的对象;
      • 常量引用的对象;
    • 所有被同步锁持有的对象

什么情况下新生代会成为老年代

长期存活的对象将进入老年代

  • 固定对象年龄判定: 虚拟机给每个对象定义一个年龄计数器,对象每在 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)对永久代进行调优是很困难的。