flyEn'blog

学习笔记之JVM性能调优

image-20210426131152957

Java语言特性:

跨平台:代码一次编写,到处运行。

本来在不同操作系统要开发不同的汇编代码,对java语言来说不需要,写一份Java代码在各平台都可以运行。

跨平台怎么实现:

字节码文件都会放到JVM中去运行,通过Java虚拟机帮我们最终生成基于不同操作系统平台的二进制机器码。

因为不同平台底层计算机可执行的指令集都不一样。

总结:Java虚拟机从软件层面屏蔽不同操作系统在底层硬件与指令上的区别。

不同平台下载不同的JDK,Java虚拟机是JDK的组成部分。

JVM组成部分

image-20210426131938797

image-20210426155941086

image-20210526102657831

image-20210526103510753

栈(线程栈/虚拟机栈VM Stack)

所有的都是内存区域,放数据。

栈(线程栈):程序运行时候的局部变量。

分配专属的运行空间(需要内存空间来存放局部变量)

内部有些组件:

栈帧

栈帧内存空间:只要有个方法开始运行,给它分配方法对应的栈帧内存空间。

为什么要用栈这种数据结构来存储我们内部的栈帧内存空间?

因为它跟我们程序方法嵌套调用的前后执行顺序是非常相匹配的。

先调用的方法先分配内存,后调用的方法后分配内存,后调用的先结束(先销毁),先调用的后结束。

栈FILO:先进后出

image-20210426135703937

字节码文件探究

javap 命令:

image-20210426141158173

1
javap -c Math.class > math.txt

image-20210426141458244

这两个字节码文件是一样的。但是右边的可读性更高。

都是JVM底层执行的代码。

越底层的语言代码量会越多,高级语言帮我们屏蔽了很多底层的细节。

Oracle官方网站有对这些class代码解释的命令手册。

程序计数器

程序计数器是每一块线程都独有的。(专属的内存空间)

当前线程执行字节码的行号指示器。

用来放当前线程正在运行或者马上要运行的代码的行号

(实际上是这行代码在方法区中的内存地址)

为什么用它?

比如正在执行某一行代码时,有个更高优先级的线程出现了,把CPU的时间片抢过去了,当前线程被挂起,当那个线程执行完,CPU才会切回当前这个线程继续执行,可以根据这个值知道继续在哪行代码之后执行。

字节码文件实际上是加载到方法区(元空间)的。

程序计数器的值是怎么修改的?

随着java代码的运行不断的作修改,是由字节码执行引擎去修改的。

操作数栈:

程序在运行过程中,操作数在操作过程中临时存放的一块内存空间。

运算完就不需要了。

动态链接

把一些符号引用转变为直接引用。

main方法内调用computer方法,程序在运行过程中,调到computer方法,可以通过动态链接去到方法区里去找到这个方法的代码。

方法出口

调用computer方法完了会把结果存储在方法出口。

方法区(元空间)

存放常量+静态变量+类信息

对象放在堆

所以方法区会访问 堆

本地方法栈

native修饰的方法是本地方法。

比如main线程里,加了一行 new Thread().start();

start0 就是本地方法。

main方法在运行过程中,有本地方法就需要用本地方法栈来分配内存空间。

image-20210426160131331

Eden园区和survivor区,老年代。

8:1:1的比例 。

new出来的对象,优先放在eden园区。

minor GC(回收整个年轻代)

放不下的时候,字节码执行引擎在后台开启一个线程,垃圾收集线程,收集垃圾对象。

垃圾收集的时候会运用到一个算法(找垃圾)可达性分析算法

GC ROOT根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。

首先找出所有的GC ROOT变量。(user, math …)

比如找到math对象,假设math还有成员变量引用了其他对象,再往下找,直到找到最后一个对象没有成员变量引用其他对象了,结束。整个引用链上的所有对象打上”非垃圾“的标签,其余未标记的是垃圾对象。

把这些对象复制到survivor区,其他Eden园区中的对象把清空。

这些对象每经历了一次minor GC,如果没有被回收的话,会挪右边一次,每挪一次,分代年龄就会加1。当对象分带年龄达到了15的话,会被复制到老年代里去。(还有其他情况:对象太大,前面的放不下;前面满了)

如果老年代放满了之后: full gc

对整个堆的所有区域做垃圾收集。

如果没有什么垃圾可以收集,之后再有新对象进来(OOM内存溢出)

调优

调优最重要的减少full GC

为什么?——stop the World(STW)

full GC的时候会停掉用户线程

对吞吐量和性能影响很大。

既然会影响很大,为什么要设计STW机制?

假设没有STW机制,产生了Full GC了,找垃圾找完所有的GC ROOT,都标记了“非垃圾”对象,GC线程没结束用户线程已经结束了,在所有的内存空间都会被释放掉,意味着又变成“垃圾了”。索性在full gc线程过程中先停掉,之后再恢复。不能让我们的对象一会是垃圾一会不是垃圾。

  1. 使用-XX:UseSerialGC, 新生代使用Serial GC,老年代自动使用Serial Old GC

对频繁fullGC的时候如何调优?

image-20210426195604129

image-20210426195737537

8个G:java虚拟机4-5G(堆3G、元空间1-2G),1-2G操作系统。

堆:老年代 2G,年轻代1G

假设每个订单对象假设1KB (所有成员变量占用的空间之和+对象头+对象补齐)

下单还涉及其他对象,如库存、优惠券、积分等我们放大20倍

(300KB*20)/秒对象生成

可能同时还有其他操作,如订单查询等 再放大10倍

(300KB2010)/秒

每秒生成60MB对象,1秒后都变成垃圾对象。

image-20210426192836042

image-20210426194647262

14s的时候,前面的12-13秒的在做minor gc的时候结束,被回收。

最后1-2s的部分线程还没结束,被挂起了。会复制到survivor区。

很有可能不一定会放到survivor区,会直接放到老年代。

什么对象会进入老年代?

  1. 大对象直接进入老年代
  2. 分代年龄到一定程度(15岁,CMS回收器默认6岁,不同垃圾回收器稍微不同)

对象动态年龄判断:

一批对象的总大小大于这块survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),(对象动态年龄机制)那么此时大于等于这批对象年龄最大值的对象,就直接进入老年代了。

所以在做minor gc的时候将“非垃圾”对象放到survivor区域的时候触发了对象动态年龄机制,直接放了一批到老年代。

4/5分钟老年代放满了,触发full GC。

能否对JVM调优,让其几乎不发生full GC?

Old:1G;eden:1.6G;S0/S1:200M

image-20210526103829136

那对单机超高并发(几十万并发)的系统怎么调优呢?

kafka、Racket MQ

需要大内存CPU比较多的系统才扛得住。(单机32G/64G以上起步,才可能抗的住这么高的并发)

比如分配:Old:1G;Eden:30G;s0/s2:200M

但是对于这种,在minor gc上就要进行调优了,如果不调一次也回收太多可能要回收几秒钟,在这个过程中,用户线程会超时。

遍历30G的eden区,至少要2-3s。客户端发消息的过程中,做了minor GC,会停顿超时了。

可以让垃圾收集器边收集垃圾边回收,不一定需要等到30G空间放满了才触发mnior GC,比如说可以等到2G空间放满了就收集,收集一部分区域,也就是大概300ms的时间。可以使用一些比较合适的垃圾收集器。

CMS垃圾收集器:最普通的

G1垃圾收集器 -XX:+UseG1GC 如果对那种大内存的回收可以做到部分回收,可以让一次GC的时间尽可能的短(STW的时间,客户停顿时间不至于太长)。

通过设置 -XX:MaxGCPauseMills:目标停顿时间。

垃圾收集器

image-20210526101048649

调优诊断优化工具

JVM调优工具:jvisualvm

阿里巴巴开源的工具:Arthas

官网文档:http://arthas.gitee.io/

image-20210426184442015

命令:

  • dashboard
  • thread 8
  • jad com.tuling.jvm.ArthasTest 反编译代码

image-20210426185717702

异常关闭查看进程:jps

资料:

[美团技术博客]

https://tech.meituan.com/2020/11/12/java-9-cms-gc.html

https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html

Fork me on GitHub