菜鸟笔记
提升您的技术认知

JVM基础和调优

身边有同学实习面试被问,jvm性能调优等问题,来总结一下JVM

什么是JVM

JDK1.8已经不存在方法区,增加了元空间;

JAVA对象的组成

如对象组成中的分代年龄(4bit),跟GC的分代相关;

JVM如何执行.java文件以及运行期数据区

基础的概念不写了,文章后面补上;

什么是程序计数器,作用?

什么是 Java 虚拟机栈,作用?

什么是 本地方法栈,作用?

什么是 堆,作用?

什么是方法区,作用?

说出java文件运行的整个过程?

栈桢

java类文件转换为class文件,在经过类装载系统,到达运行时数据区运行(我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间));运行时数据区包括(程序计数器,虚拟机栈,本地方法栈,堆(线程共享),方法区(线程共享));

Java虚拟机栈存放着许多栈帧,每个方法执行的同时都会创建一个栈帧(stack Frame),方法执行完成,出栈;整个过程可以看成在虚拟机栈的入栈出栈过程;

帧结构:1. 局部变量表,2. 操作数栈,3.动态链接,4.方法出口

发现java文件与对应字节码文件的关系

 public int compute() {
    #java文件
        int a = 0;
        int b = 1;
        int c = (a + b) * 10;
        return c;
    }
 public int compute();  #对应的字节码
    Code:
       0: iconst_0 #
       1: istore_1
       2: iconst_1
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn

GC初识

GC是什么,工作方式和区域等?

  • 运行一个GCtest类()
public class GCtest {
  
    //100kb
    byte[] a = new byte[1024 * 100];
    public static void main(String[] args) throws InterruptedException {
  
        ArrayList<GCtest> gCtests = new ArrayList<>();
        while (true) {
  
            gCtests.add(new GCtest());
            Thread.sleep(10);
        }
    }
}
  • 使用JAVA自带GC工具=>jvisualvm,命令行输入jvisualvm启动

  • 点开GCtest类,查看GC运行情况

  • 程序一直运行,堆内存不足,报错(OOM)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.gjf.GCtest.<init>(GCtest.java:13)
	at com.gjf.GCtest.main(GCtest.java:17)

JVM调优

Arthas(阿尔萨斯)是阿里巴巴开源的 Java 诊断工具,深受开发者喜爱。

  • 为什么要JVM调优?

减少GC,特别是FullGC

  1. 对象优先在堆的 Eden 区分配
  2. 大对象直接进入老年代
  3. 长期存活的对象将直接进入老年代. 当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC.Minor Gc 通 常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高, 回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。
  • GC 的两种判定方法:
    1. 引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就 会回收但是 JVM 没有用这种方式,因为无法判定相互循环引用(A 引用 B,B 引用 A) 的情况
    2. 引用链法: 通过一种 GC ROOT 的对象(方法区中静态变量引用的对象等-static 变 量)来判断,如果有一条链能够到达 GC ROOT 就说明,不能到达 GC ROOT 就说明 可以回收

JVM的垃圾回收器和内存分配

  1. 串行垃圾回收器

是指使用单线程进行垃圾回收的回收器,每次回收只有一个工作线程(对并行能力比较弱的电脑,运行性能较好)

注意:串行回收器运行时,所有应用程序的线程都停止工作,属于独占式的垃圾回收方式

线程进行等待的现象称为–>Stop-The-World,造成非常糟糕的用户体验;

  1. 并行垃圾回收器

多个线程同时进行垃圾回收,适合并行能力强的计算机;

  1. 新生代ParNew回收器,只是简单将串行回收器多线程化,也是独占式的回收器
  2. 还有其他的回收器,都关注吞吐量,其中包括复制算法,标记压缩算法等回收算法

注意:还是会造成线程等待现象–>Stop-The-World(STW),但是减少垃圾回收的停顿时间就会同时减小系统的吞吐量

  1. CMS回收器(jdk1.8以前)

CMS回收器主要关注系统的停顿时间,并发标记清除,是一个基于标记清除算法的回收器;

CMS掉工作过程相对复杂,不是独占式的回收器,工作过程中,应用程序仍然工作;

不会等到堆内存饱和后进行回收,而是到达一定阈值才开始垃圾回收

参数 说明
-XX:CMSInitiatingOccupancyFraction 默认堆老年代使用达到68%,执行CMS回收,如果在执行过程中内存不足,就会启动串行回收器进行垃圾回收,应用程序将完全中断;
根据此参数进行调优,增大阈值可以降低CMS的触发,减少老年代的回收次数;如果内存使用增长很快,应该降低阈值;

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的垃圾收集器。从名字可以看出,CMS 是基于标记-清除算法的。它的运作过程主要分为四个步骤:

  1. 初始标记(CMS initial mark):STW,标记GC Roots能直接关联到的对象,速度很快。单线程
  2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时较长,不需要停顿用户线程
  3. 重新标记(CMS remark):STW,修正并发标记期间,因用户程序继续运作而导致标记发生变动的那一部分对象的标记记录(增量更新),时间稍长于初始标记,但远低于并发标记
  4. 并发清除(CMS concurrent sweep):清除已死亡对象,因为不需要移动对象,所以与用户线程是并发的关系

G1回收器(JDK1.7推出,1.9默认)

Garbages First(G1)垃圾回收器,作为CMS的长久替代方案,使用了全新的分区算法;

  1. 并行性:G1回收期间,多个GC线程可以同时工作
  2. 并发性:G1可以跟应用程序交替执行的能力,不会在回收期间完全阻塞应用程序
  3. 分代GC:G1依然是一个分代回收器,与之前回收器不同,G1兼顾年轻代和老年代,如CMS工作在老年代
  4. 空间整理:G1回收过程中,会适当进行对象移动,如CMS若干次GC后,CMS必须进行一次碎片整理,但是G1,每次回收都会有效复制对象,减少碎片空间;
  5. 可预见性:由于分区原因,G1只对选取的部分区域进行回收。可以很好的控制全局停顿;
新生代GC(主要回收eden区和survivor区,复制算法)

eden区被占满,新生代GC启动,ednn区会被全部回收,至少存在一个survivor区,老年代区域增大;

G1的并发标记周期

  1. 初始化标记:标记从根节点直接可达的对象,发生一次新生代GC,产生全局停顿
  2. 根区域扫描:eden区已经清空,扫描标记由survivor区直接可达的老年代区域,无法与新生代GC同时执行
  3. 并发标记:与CMS类似,全局标记堆中存活的对象
  4. 重新标记:G1会使用(SATB)算法,为存活对象创建快照,有助于加速重新标记速度
  5. 独占清理:计算各个区域存活对象和GC回收比例,识别可供混合回收的区域
  6. 并发清理:会识别并清理空闲区域,并发清理不会引起停顿

混合回收

在并发标记后就知道哪个区域的垃圾较多,G1就会优先回收垃圾比例高的区域

FullGC

  1. 堆内存不足时,就会触发FullGC,

  2. 对于并行回收器的FullGC之前,都会触发一次新生代GC

  3. 使用system.gc()方法,触发一次GC,在并行回收器中,FullGC之前会发生一个新生代GC,这样可以缩短停顿时间(STW)

对象进入老年代

  1. 新对象在eden区
  2. JVM提供一个参数来控制新生代对象的年龄(MaxTenuringThreshold),默认初始值为15,新生代对象最多经历15次GC就可以到老年代
  3. 新生代无法容纳的大对象直接进入老年代,可以通过调节参数(PretenureSizeThreshold),设置对象晋升到老年代的阈值;
  • 如使用的JVM测试参数(使用开发工具可以自定义)
-Xmx32m -Xms32m -XX:+UseSerialGC -XX:+PrintGCDetails 

G1和CMS的区别

G1具备如下特点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
  • 分代收集
  • 空间整合:与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。

CMS 用于老年代的回收,而 G1 用于新生代和老年代的回收。

G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生。CMS使用“标记-清理”算法会产生大量的空间碎片;

相关参数



JVM调优很大程度上是对GC的调优,导出堆,对导出的dump文件进行分析,我们可以找到内存热点,可以找到哪个类型的对象数量最多,且占用的内存最多;哪个对象的体积大,还频繁被销毁创建;
但是JVM的调优本质实际上是通过JVM监控来分析JAVA代码的工作情况,找出不合理的设计和低质量的代码,进行改进;

实战案例

  • 扩大堆以提升系统性能
-Xmx512m -XX:MaxPermSize=32M -Xloggc:gc:gc.log -XX:+PrintGCDetails\
//MaxPermSize为最大的永久区大小。

JVM中的内存区域一般分为3个部分: 年轻代、年老代和永久代;永久代在JDK 7中逐渐变化,到JDK 8之后完全消失,合并到了Native堆中,JDK8中,PermSize和MaxPermSize参数也一并移除了。

监控和诊断性能问题

  • Linux下性能监控命令和工具
  • JConsole,Visual VM等工具使用

对症下药

内存溢出(OOM)

  1. 直接内存溢出
  2. 堆溢出
  3. 永久区(元数据区)溢出
  4. GC效率低引起OOM

待续,如何看问题做性能优化

补充JVM基础知识

程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

**局部变量表:**存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址),第一位放的是0,代表this当前对象;

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则使用 Native 方法。也会有 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。(GC,G1主要工作区域)

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

方法区

属于共享内存区域存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

一键三连