目录
1. JVM的主要组成部分及其作用?
1.1 运行时数据区划分?
1.2 哪些区域可能会发生OOM?
1.3 堆和栈的区别?
1.4 内存模型中的happen-before是什么?
2. HotSpot虚拟机对象创建流程?
2.1 类加载过程?
2.2 类加载器有哪些?
2.3 什么是指针碰撞和空闲列表?
2.4 初始化和实例化的区别?
2.5 有哪些方法可以在运行时动态生成一个 Java 类?
3. 简述垃圾回收机制?
3.1 怎么判断对象可以被回收?
3.2 垃圾回收算法有哪些?
3.3 常见的垃圾回收器有哪些?
3.4 介绍一下CMS垃圾收集器以及收集过程?
3.5 介绍一下G1垃圾收集器及收集过程?
3.6 CMS和G1对比?
3.7 分代收集对象的过程?
4. 日常工作中GC调优?
4.1 发现问题?
4.1 诊断问题?
4.3 修复问题?
1. JVM的主要组成部分及其作用?
JVM包含两个子系统和两个组件。两个子系统为class loader(类加载)和Execution engine(执行引擎),两个组件为Runtime Data Area(运行时数据区域)和Native Interface(本地接口)。
作用:首先编译器把Java代码转换为字节码,类加载器再把字节码加载到内存,将其放在运行时数据区的方法区内。而字节码不能直接被底层操作系统执行,所以需要特定的命令解析器执行引擎,将字节码翻译乘底层系统指令,再交给CPU去执行。而这个过程中会调用到其他的一些本地库接口来实现整个程序的功能。
Java程序运行机制步骤:
- 编写Java源代码,源文件后缀.java
- 编译器将源代码编译成字节码文件,字节码文件后缀.class
- 解释器来运行字节码
1.1 运行时数据区划分?
JVM运行时区域划分主要为五个部分,分别是线程共享的堆、方法区,和线程私有的本地方法栈、虚拟机栈、程序计数器。
具体每块区域存放的内容及作用:
- 程序计数器 :每个线程只有一个程序计数器,所以每个程序计数器是线程私有的;它是内存中最小的一块,里面保存了当前线程下一条执行的指令的地址。
- Java虚拟机栈 :它是线程私有的,声明周期与线程相同。虚拟机栈描述的Java方法执行的线程内存模型:每个方法在执行的时候,Java虚拟机栈都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。,在我们调用方法的时候,每调用一个方法,该方法就会进入栈中,当该方法执行完毕的时候就会从栈中移除。
- 本地方法栈:本地方法栈也是线程私有的,与虚拟机栈发挥作用几乎相同,只是它为虚拟机使用到的本地方法服务。
- 方法区 :是线程共享的区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码缓存等数据。
- 堆 :堆是线程共享的区域,是Java虚拟机管理的内存种最大的一块,所有对象实例以及数组都在堆上分配。Java堆是垃圾收集器管理的内存区域。
1.2 哪些区域可能会发生OOM?
- 首先是堆,堆内存不足是最常见的 OOM 原因之一,抛出的错误信息是java.lang.OutOfMemoryError:Java heap space。原因可能存在内存泄漏问题;也很有可能就是堆的大小不合理等。
- 其次是Java 虚拟机栈和本地方法栈,如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
- 直接内存不足,也会抛出OOM异常
1.3 堆和栈的区别?
主要从四个方面对比区别:
- 物理地址:堆的物理地址分配对对象是不连续的,因此性能慢些。而且再垃圾回收时也要考虑到不连续的分配,采用各种垃圾回收算法。而栈使用的是数据结构中的栈,先进先出的原则,所以物理地址分配是连续的,性能更快。
- 内存分别:堆因为分配不连续的内存,所以分配内存时在运行期确认的,因此大小不固定,一般来说堆远远大于栈的。栈是连续的,所以分配内存在编译器就要确认,大小固定。
- 存放内容:堆存放的是对象的实例和数组,因此更关注的是数据的存储。栈存放的是局部变量、操作数栈、返回结果,所以更关注的是方法的执行。
- 程序可见度:堆对于整个应用程序共享,栈是线程私有的,它的生命周期与线程相同。
1.4 内存模型中的happen-before是什么?
Happen-before 关系,是 Java 内存模型中保证多线程操作可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精确定义。它的具体表现形式,包括但远不止是我们直觉中的 synchronized、volatile、lock 操作顺序等方面,例如:
- 线程内执行的每个操作,都保证 happen-before 后面的操作,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
- 对于 volatile 变量,对它的写操作,保证 happen-before 在随后对该变量的读取操作。
- 对于一个锁的解锁操作,保证 happen-before 加锁操作。
- 对象构建完成,保证 happen-before 于 finalizer 的开始动作。
- 甚至是类似线程内部操作的完成,保证 happen-before 其他 Thread.join() 的线程等。
这些 happen-before 关系是存在着传递性的,如果满足 a happen-before b 和 b happen-before c,那么 a happen-before c 也成立。它不仅仅是对执行时间的保证,也包括对内存读、写操作顺序的保证。仅仅是时钟顺序上的先后,并不能保证线程交互的可见性。
2. HotSpot虚拟机对象创建流程?
首先Java中对象创建的几种方式:
- 使用new关键字创建对象,调用构造函数
- 使用Class的newInstance方法创建对象,调用构造函数
- 使用Constructor的newInstance方法创建对象,调用构造函数
- 使用clone方法创建对象,不适用构造方法
- 使用反序列化创建对象,不使用构造函数
然后创建对象的主要流程,再虚拟机遇到一条new执行时:
- 先检查常量池是否已经加载相应的类,如果没有则先进行类加载;
- 类加载后开始分配内存,如果Java堆中内存时规整的,使用”指针碰撞“的方式分配内存,如果不规整就使用”空闲列表“分配;
- 分配内存时考虑并发问题,两种处理方式:CAS同步处理,或者本地线程分配缓冲(TLAB);
- 然后内存空间初始化操作
- 接着做一些必要对象设置,最后执行
2.1 类加载过程?
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:
类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
2.2 类加载器有哪些?
Java类加载器主要有四种,分别是引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、系统类加载器(System ClassLoader),以及自定义类加载器(Custom ClassLoader)。
- 引导类加载器:这是最顶层的类加载器,由原生代码(如C语言)编写,不继承自java.lang.ClassLoader。它负责加载Java的核心库,这些库存储在/jre/lib目录中。由于它是用原生代码实现的,因此在Java代码中无法直接获取其引用。
- 扩展类加载器:这个类加载器负责在/jre/lib/ext目录或由系统属性java.ext.dirs指定目录中加载Java的扩展库。它由sun.misc.Launcher$ExtClassLoader实现,用于加载Java的扩展库。
- 系统类加载器:也被称为应用类加载器(Application ClassLoader),它根据Java应用程序的类路径(java.class.path或CLASSPATH环境变量)来加载Java类。这是应用程序中最常用的类加载器,负责加载用户编写的应用程序类,即自定义类。
- 自定义类加载器:开发者可以通过继承java.lang.ClassLoader类来创建自定义的类加载器。自定义类加载器可以实现特定的类加载方式,以满足一些特殊的需求。
Ps:上图中除了对类加载器的说明,还有箭头指向,其实就是双亲委派模型:类加载器之间存在父子层级关系,启动类加载器是最高级别的加载器,没有父加载器;扩展类加载器的父加载器是启动类加载器;应用类加载器的父加载器是扩展类加载器。在类的加载过程中具体使用的类加载器的选择,遵循一种双亲委派机制:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。
2.3 什么是指针碰撞和空闲列表?
- 指针碰撞:如果Java内存规整(使用过的区域放在一侧,空闲区域放在另一侧),分配内存就是将位于中间的指针向空闲侧移动一段与对象大小相等的距离,就是完成了对象的内存分配;
- 空闲列表:是在Java内存不规整的情况,需要虚拟机维护一个列表来记录哪些内存可用。这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表。
2.4 初始化和实例化的区别?
- 实例化是创建对象的过程,而初始化是为对象的属性赋值的过程;
- 实例化只会发生一次,而初始化可以发生多次;
- 实例化是在堆内存中为对象分配空间,而初始化是为对象的属性赋予初始值;
- 实例化是通过使用new关键字调用构造方法来创建对象的过程,而初始化可以通过构造方法、静态代码块、实例代码块、默认值等方式来进行。
2.5 有哪些方法可以在运行时动态生成一个 Java 类?
通常的开发过程是,开发者编写 Java 代码,调用 javac 编译成 class 文件,然后通过类加载机制载入 JVM,就成为应用运行时可以使用的 Java 类了。从上面过程得到启发,其中一个直接的方式是从源码入手,可以利用 Java 程序生成一段源码,然后保存到文件等,下面就只需要解决编译问题了。有一种笨办法,直接用 ProcessBuilder 之类启动 javac 进程,并指定上面生成的文件作为输入,进行编译。最后,再利用类加载器,在运行时加载即可。
前面的方法,本质上还是在当前程序进程之外编译的,还有可以考虑使用 Java Compiler API,这是 JDK 提供的标准 API,里面提供了与 javac 对等的编译器功能,具体请参考java.compiler相关文档。
3. 简述垃圾回收机制?
Java中的垃圾回收机制是指:
程序员在编写代码时不用显式的去释放对象内存,而是交给Java虚拟机去完成。在JVM中有一个垃圾回收线程,它是低优先级的,正常情况下不会执行,只有在虚拟机空闲湖泊这内存不足时才会触发执行,去扫描没有使用时对象,将他们添加到要回收的集合里进行回收,防止内存泄漏。
垃圾收集也叫GC,GC的存在是因为程序员在编码过程中经常容易忘记进行内存处理,忘记或者错误的垃圾回收会导致程序的崩溃,所以Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收的目的。Java也没有提供释放已分配内存的显式擦欧总方法。
3.1 怎么判断对象可以被回收?
通常有两种算法,分别是引用计数法和可达性分析算法。
- 引用计数法:为每个对象创建一个引用计数器,有对象引用时计数器+1,引用被释放时计数器-1,当计数器为0时说明没有有效的引用,可以被回收。但它的缺点是:不能解决循环引用的问题。
- 可达性分析算法: 是从GC root开始向下检索,搜索走过的路径被称为引用链。当一个对象到GC root间没有任何一条引用链时,此对象可以被回收。
3.2 垃圾回收算法有哪些?
- 标记-清除算法:标记无用的对象,进行清除回收。优点:实现简单,不需要对象进行移动。缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的 频率。
- 复制算法:按容量划分两个区域,当其中一块用完的时候将活着的对象复制到另一块区域上,然后再把前一块内存一次性清理掉。优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
- 标记-整理算法:标记无用的对象,让活着的对象都向一端移动,然后清理掉端边界以外的内存。优点:解决了标记-清理算法存在的内存碎片问题。 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
- 分代算法:根据对象存周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代使用标记-整理算法。
3.3 常见的垃圾回收器有哪些?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体 实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器 包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器 之间的连线表示它们可以搭配使用:
新生代垃圾回收器:
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点 是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程 版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效 利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高 效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不 高的场景;
老年代垃圾回收器:
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年 代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先, Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集 器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最 短GC回收停顿时间。
G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是 JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会 产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的 范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代 或老年代。
3.4 介绍一下CMS垃圾收集器以及收集过程?
CMS垃圾收集器时Java虚拟机中回收老年代的一种垃圾回收器,它主要目标是减少垃圾收集时的应用程序停顿(STW)时间。使用并发的方式进行垃圾回收,使用的算法是标记-清除算法。适用于对服务响应速度要求较高的场景,例如互联网站或B/S系统的服务端Java应用。也基于它的实现原理,他也存在一些缺点:
- 对CPU敏感:因为并发收集,所以虽然不会导致用户线程停顿,但会占用一部分线程(CPU资源),可能导致应用程序整体变慢,降低总吞吐量;
- 无法处理浮动垃圾:由于CMS在并发清理阶段用户线程仍在运行,新的垃圾可能在标记过程之后(重新标记之后)的“并发清理阶段”产生。因为在并发清理阶段用户线程和GC线程是并发运行的,而CMS不能在当前收集中处理这部分浮动垃圾;
- 对CPU数量要求较高 :CMS默认启动的回收线程数为(CPU数量+3)/4,当CPU不足4个时,可能对用户程序影响较大;
- 内存碎片问题 :基于“标记-清除”算法的CMS会导致大量空间碎片的产生,可能对大对象分配的时候可能会产生Full GC,因为可能出现老年代空间虽有剩余但无法找到足够大连续空间来分配当前对象。
CMS垃圾收集器的主要流程包括:初始标记、并发标记、最终标记和并发清理,其中初始标记和最终标记需要STW但是速度都很快,尽量降低了系统的停顿时间。并发标记和并发清理等耗时较长的阶段采用了并发的方式,来减少系统暂停。回收流程:
- 初始标记:初始标记只是标记GC Roots能直接关联到的对象,但需要“Stop The World”停顿,即在此期间暂停所有应用线程。这个过程在JDK 7 之前是单线程(因为GC Roots直接关联的对象相对较少),JDK 8之后是多线程的方式进行初始标记。
- 并发标记:进行GC Roots Tracing,即从GC Roots出发标记上所有和GC Root相连的存活对象,这一过程是和用户线程并发执行的,不需要“Stop The World”,因此该阶段对系统整体的性能影响较小。
- 重新标记:由于在并发标记阶段,用户线程还是在工作的,因此有可能会产生新的对象。JVM会通过Card(卡片)的方式将发生变化的老年代区域标记为“脏”区域,也就是所谓的卡片标记(Card Marking)来对新增对象的存活状态进行重新标记。由于这个阶段由于需要确定最终的GC视图,需要避免该阶段再引入新的垃圾,因,此需要“Stop The World”停顿。与并发标记相比,停顿时间相对较短,主要关注标记发生变化的对象。上述新对象主要通过以下三个途径产生:
- 年轻代对象晋升到老年代,可能产生新的存活对象;
- 大对象直接被分配到老年代,可能产生新的存活对象;
- 老年代和年轻代对象的引用关系发生变化;
- 并发清理:最后GC线程会清除不再被引用的对象,并回收他们占用的内存空间。由于前面的标记阶段已经将还在使用的对象标记了出来,因此该过程与用户线程并发执行,不需要全局停顿(“Stop The World”),整个垃圾回收过程完成。
3.5 介绍一下G1垃圾收集器及收集过程?
- G1收集器是垃圾收集器技术发展史上里程碑式的成果,它摒弃了传统垃圾收集器的严格的内存划分,而是采用局部回收的设计思路和基于Region的内存布局形式。它的目的是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。如今已经完全替代CMS垃圾收集器,CMS收集器在JDK9 中被废弃,在JDK 14中被移除。使用的算法是标记-整理算法。虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,而是一系列区域(不需要连续,逻辑连续即可)的动态集合。由于G1这种基于Region回收的方式,可以预测停顿时间。G1会根据每个Region里面垃圾“价值”的大小,在后台维护一个优先级列表,每次根据用户设定的允许收集停顿的时间(-XX:MaxGCPauseMillis,默认为200毫秒)优先处理价值收益最大的Region。
- G1垃圾收集器也是基于分代收集理论设计的,但是它的堆内存的布局与其他垃圾收集器的布局有很明显的区别,G1收集器不再按照固定大小以及固定数量的分代区域划分,而是把JAVA堆划分为2048个大小相等的独立的Region,每个Region大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1-32MB,且必须为2的N次幂。每一个Region都可以根据需要充当新生代的Eden区、S0和S1区或者老年代。在一般的垃圾收集中对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。 G1的大多数行为都把H区作为老年代的一部分来看待。当一个对象的大小超过了一个Region容量的一半,即被认为是大对象。
G1垃圾收集器的主要流程包括:初始标记、并发标记、最终标记和筛选处理。除了并发标记阶段,其他阶段均需要STW。回收流程:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能够在Region上正确的分配对象。这个阶段需要STW,耗时很短,而且是借用MinorGC(上一轮垃圾回收时触发GC)时候同步完成的。
- 并发标记:从GC Roots 开始对堆中的对象进行可达性分析,递归扫描整个堆里的对象,这个过程耗时较长,但是是与用户线程并发执行的。对象扫描完之后还需要重新处理STAB记录下的在并发时有引用变动的对象。
- 最终标记:这个阶段也需要STW,用于处理并发阶段结束后仍然遗留下来的最后少量的STAB记录。
- 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本排序,根据用户期望的停顿时间来执行回收计划,然后把决定回收的Region里的存活对象复制到空的Region,然后清空旧Region的空间。由于涉及到对象的移动,所以这个阶段也是需要STW的。
3.6 CMS和G1对比?
CMS的优点是:低停顿时间、适用于大内存应用。但缺点是:由于CMS采用标记-清除算法,可能会导致内存碎片,进而影响内存分配效率,以及CMS在并发标记和清除阶段需要额外的内存,可能导致更高的内存使用。
G1的优点是:可预测停顿时间、减少内存碎片、适用于大内存或多核环境。但缺点是:配置相比于CMS更为复杂,在高负载下,G1的年轻代回收可能带来较高的开销。
所以,在选择CMS和G1垃圾回收器时,开发者需要根据具体的应用场景和性能需求进行权衡:
- 响应时间敏感的应用:如果应用对响应时间要求较高,可以优先考虑CMS,以减少停顿时间。
- 大内存和多核环境:在大内存和多核环境下,G1表现更为出色,适合大规模的Java应用。
- 可预测性需求高:如果需要更加可预测的停顿时间,G1是一个更好的选择。
3.7 分代收集对象的过程?
(1)首先一个新生的对象(假设在对象的大小在新生代区域可以放的下的情况,因为大对象会被直接放到老年代)放在新生代的Eden区域;
(2)在第一次进行Minor GC时,对于Eden区新创建的对象大部分是垃圾对象,对于少数活跃的对象会被复制到Survival To区中,并且让其寿命加1,内存图如下图所示:
(3)紧接着会把To区的对象都会被通过复制算法,复制到From区。此时Eden区和Survival To区的都是空闲的,内存图如下所示:
(4)程序继续运行,经过第一次GC,Eden又可以保存新建对象了,随着程序的运行,此时Eden区的内存又满了。需要进行第二次GC操作,同第一次一样,暂时活跃的对象,需要复制到Survival To区,同时当熬过这一次GC时,对象的年龄加1,进行保存。和第一次GC不同的是,此时Survival From区中也有幸存的对象,这时,需要根据Survival From区的对象年龄大小来确定该区中对象的去向,看其年龄是否达到设定的指定阈值。如果达到指定阈值,会被分配到Old Generation,如果没有则会被复制到Survival To区,当熬过这一次GC时,From区的年龄也会加1,具体内存分配图如下所示:
(5)紧接着会把Survival To区的对象,复制到Survival From中去。这时候Eden和Survival区中的内存就空余出来了,供后续新创建的对象来存储,经过第二次GC后的内存分配图:
(5)GC会一直重复这样的过程,有的对象会进入到老年区,有的则会游离在To和From之间,游离在两者之间的对象可能会达到阈值,到达老年代中。当“To”区被填满,这时“To”区被填满之后,会将所有对象移动到年老代中,当老年代空间被用完,就要进行 Full GC了。
(6)如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收Full GC当老年代内存不足,即老年代的垃圾回收。
4. 日常工作中GC调优?
社招一般会问你在工作中遇到的GC调优的实际案例,描述案发的现象,再描述使用什么工具排查的过程,最后是解决问题的方案和解决后的成果。看了个博主写的比较专业:
4.1 发现问题?
GC调优的问题都比较主观,对于研发来说,除了熟悉JVM原理去排查,其他的几乎都是吃一堑长一智,需要自己实际遇到问题去具体分析。对于发现问题,我个人的工作环境来讲,发现问题一般是通过这几个方面:
- 公司内部的监控系统:内存/CPU监控/日志监控,指标异常时会有报警和图形趋势图,方便研发分析和及时排查问题
- 监控工具(参考《深入理解Java虚拟机》第四章):
- jps:虚拟机进程状况工具
- jstat:虚拟机统计信息监视工具,它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面只提供文本控制台环境的服务器上,是最常用的工具
- jinfo:Java配置信息工具,作用是实时查看和调整虚拟机各项参数。
- jmap:Java内存映像工具,用于生成堆转储快照。
- jstack:Java堆栈跟踪工具,用于生成虚拟机当前时刻的线程快照
4.1 诊断问题?
直接登录报警机器查看日志:通过GC日志,可以更好的看到垃圾回收细节上的数据,同时也可以根据每款垃圾回收器的不同特点更好地发现存在的问题。
- 使用方法(JDK 8及以下):-XX:+PrintGCDetails -Xloggc:文件名
- 使用方法(JDK 9+):-Xlog:gc*:file=文件名
注: -verbose:gc 是将GC日志输出到控制台上,而上面是将GC日志单独输出到一个文件
根据问题表现去锁定使用具体哪些工具分析问题,比如内存问题可以使用jstat或者jmap工具分析、CPU使用率问题可能是线程暴增相关导致的,可以使用jstack工具查看等等。
4.3 修复问题?
具体问题具体分析,有的需要优化代码,有的需要优化JVM配置。这里罗列一些常见的JVM参数:
- -Xmx参数设置的是最大堆内存,但是由于程序是运行在服务器或者容器上,计算可用内存时,要将元空间、操作系统、 其它软件占用的内存排除掉。
- -Xms用来设置初始堆大小,建议将-Xms设置的和-Xmx一样大。
- -XX:MaxMetaspaceSize=值 参数指的是最大元空间大小,默认值比较大,如果出现元空间内存泄漏会让操作系统可用内存不可控,建议根据测试情况设置最大值,一般设置为256m。当元空间大小超过这个值时,会抛出OutOfMemoryError。
- -XX:MetaspaceSize=值 参数指的是到达这个值之后会触发FULL GC(指的不是初始元空间大小), 后续什么时候再触发JVM会自行计算。如果设置为和MaxMetaspaceSize一样大,就不会FULL GC,但是对象也无法回收。
- -Xss虚拟机栈大小 :如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。 比如Linux x86 64位 : 1MB,如果不需要用到这么大的栈内存,完全可以将此值调小节省内存空间,合理值为256k – 1m之间。
- -Xmn 年轻代的大小,默认值为整个堆的1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年轻代,不进入老年代。但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不 确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。G1垃圾回收器尽量不要设置该值,G1会动态调整年轻代的大小。
- ‐XX:SurvivorRatio是Eden区和survival区的大小比例,默认值为8。
- ‐XX:MaxTenuringThreshold 最大晋升阈值,年龄大于此值之后,会进入老年代。另外JVM有动态年龄判断机制:当 survior 区域的存活对象的总大小占用了 survior 区域大小的50%(可以通过参数指定),那么此时将按照这些对象的存活年龄从小到大排序,然后依次累加,当累加到对象大小超过50%,则将大于等于当前对象年龄的存活对象全部挪到老年代。
- -XX:+DisableExplicitGC 禁止在代码中使用System.gc(), System.gc()可能会引起FULL GC,在代码中尽量不要使用。使用DisableExplicitGC参数可以禁止使用System.gc()方法调用。
- -XX:+HeapDumpOnOutOfMemoryError 发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
- -XX:HeapDumpPath= 指定hprof文件的输出路径。
Ps:最后是对问题解决方案的验证,根据发现问题的现象去观察修改后的状况。