如何优雅的学习JVM,终结篇(六)

时间:2020-05-30
本文章向大家介绍如何优雅的学习JVM,终结篇(六),主要包括如何优雅的学习JVM,终结篇(六)使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、如何计算一个对象占用的内存大小?

我们在编写代码的过程中会创建大量的对象,有没有考虑到底某个对象占用了多少内存呢?

在C/C++中,我们可以通过sizeof()函数来计算一个变量或者类型所占用的大小,然而在Java中并没有这样的系统调用,那么在Java中如何实现类似的计算对象占用的内存大小呢?Java对象的内存结构,我们在前面的章节也进行了学习,它包括对象头(标记位、对象指针)、实例数据、对齐填充。那么我们只要按照顺序计算出各个区域所占用的内存并求和就可以了。其实仔细想想肯定没有这么简单,其中还有很多细节问题需要考虑。

  • 对象头

在不开启JVM对象头压缩的情况下:

32位 JRE中一个对象头大小是8个字节(4 + 4)

64位 JRE中则是16个字节(8 + 8)

  • 实例数据

成员变量主要包括两种:基本类型和引用类型,非静态成员变量所占用的数据。在确定JRE运行环境中,基本类型和引用类型占用的内存大小都是确定的,因此需要简单通过反射做个加法似乎就可以了。但是实际情况不是你想的这样简单,让我们来写代码测试一下吧。

通过jol工具可以查看到一个对象的实际内存布局,我们使用OpenJDK,提供了JOL包,官网: http://openjdk.java.net/projects/code-tools/jol/

POM依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.8</version>
</dependency>

创建一个如下示例代码的测试类:

import org.openjdk.jol.info.ClassLayout;

public class Pojo {
    public int a;
    public String b;
    public int c;
    public boolean d;
    private long e; // e设置为私有的,后面讲解为什么
    public Object f;
    Pojo() { e = 1024L;}

    public static void main(String[] args) {
        Pojo pojo = new Pojo();
        System.out.println(ClassLayout.parseInstance(pojo).toPrintable());
    }
}

使用 jol 工具查看其内存布局如下:

 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4                int Pojo.a                                    0
     16     8               long Pojo.e                                    1024
     24     4                int Pojo.c                                    0
     28     1            boolean Pojo.d                                    false
     29     3                    (alignment/padding gap)                  
     32     4   java.lang.String Pojo.b                                    null
     36     4   java.lang.Object Pojo.f                                    null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

由此可以看出对象头所占用的大小为12字节。从这个内存布局表上不难看出,成员变量在实际分配内存时,并不是按照声明的顺序来储存的,此外在变量d之后,还出现了一块用于对齐内存的padding gap,这说明计算对象实际数据所占用的内存大小时,并不是简单的求和就可以的。

在上面的内存布局表中,可以看到OFFSET一列,这就是对应变量的偏移地址,如同C++中的指针,其实就是告诉了CPU要从什么位置取出对应的数据。举个例子,假设 Pojo 类的一个对象pojo存放在以 0x0010 开始的内存空间中,我们需要获取它的成员变量 b ,由于其偏移地址是 32(转换成十六进制为20),占用大小是 4 ,那么实际储存变量b的内存空间就是 0 x0030 ~ 0x0033 ,根据这个 CPU 就可以很容易地获取到变量了。

实际上在反射中,正式通过这样的方式来获取指定属性的值,具体实际上则需要借助强大的Unsafe工具。Unsafe可以实现系统底层不可思议的操作(比如修改变量的可见性,分配和回收堆外内存等),不过正因为其功能的强大性,随意使用有可能引发程序崩溃,所以官方不建议在除系统之外(如反射等)以外的场景使用,使用Unsafe如何通过变量偏移地址来获取一个变量。

示例代码:

@Test
    public void testUnsafe() throws Exception {
        Class<?> unsafeClass = null;
        Unsafe unsafe = null;
        try {
            unsafeClass = Class.forName("sun.misc.Unsafe");
            final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            unsafe = (Unsafe) unsafeField.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        Pojo pojo = new Pojo();//上面案例的测试实体类
        Field field = Pojo.class.getDeclaredField("e");
        long offset = unsafe.objectFieldOffset(field);
        if(offset > 0L) {
            long eVal = unsafe.getLong(pojo, offset);
            System.out.println(eVal);
        }
    }

运行结果打印:1024

出于安全起见,一般情况下在正常的代码中是无法直接获取 Unsafe的实例的,这里我们通过反射的方式hack了一把来拿到 unsafe实例。接着通过调用 objectFieldOffset 方法获取到成员变量 e的地址偏移为 16(和 jol 中的结果一致),最终我们通过 getLong() 方法,传入e 的地址偏移量,便获取到了 e 的值。可以看到尽管 Pojo 类中 e是一个私有属性,通过这种方法依然是可以获取到它的值的

有了调用Unsafe的objectFieldOffset,我们可以通过代码精确的计算一个对象在内存中所占用的空间大小了,递归遍历对象中所有的引用并计算他们指向的实际对象的浅内存占用,最终求和即可。考虑到会有大量重复的类出现,我们可以使用一个数组来缓存已经计算过浅内存占用的class,避免重复计算。

如果引用指向了数组或者集合类型,那么只需要计算其基本元素的大小,然后乘以数组长度/集合大小即可。

具体实现代码在此不过多赘述,可以直接参考源代码( from Apache luence ,入口方法为 sizeOf ( Object ))。

源代码: https://github.com/MarkLux/Java-Memory-Monitor/blob/master/src/main/java/cn/marklux/memory/RamUsageEstimator.java

需要注意的是,这种计算对象内存的方法并不是毫无代价的,由于使用了递归、反射和缓存,在性能和空间上都会有一定的消耗。

二、性能优化经验总结

2.1 性能优化的背景

大家平时一定遇到过这样的问题:单机的线程池队列爆满,使用集群扩容增加集群;系统内存占用高,高峰时段OOM,重启就分分钟解决了等一系列的问题。如果临时性的补救措施只能是给应用埋雷,同时只能解决部分问题,治标不治本。

添加硬件资源并不一定能够解决系统的性能问题,反而有时候会造成资源的浪费,得不偿失。因此对系统进行合理的性能优化,可以在系统稳定性、成本核算获得很大的收益。

假设现在我们的系统已经出现了性能问题,需要准备开始进行优化工作,那么在这个优化过程中,潜在的痛点有哪些呢?

1、对性能优化的流程不是很清晰。 最终需要解决的问题其实是一个浅层次的性能瓶颈,真实的问题的根源并不能触达。

2、对性能瓶颈点的分析思路不是很清晰。 网路、CPU、内存等这么多的性能指标,到底该关注什么,应该从哪一块开始入手呢?

3、对性能优化的工具不是很了解。 遇到问题后,不清楚使用哪个工具,不知道通过工具得到的指标代表着什么。

2.2 性能优化的流程

1、准备阶段: 通过性能测试,了解系统的概况、瓶颈的大概方向,明确优化目标;

2、分析阶段: 通过各种工具或手段,初步定为性能瓶颈点;

3、调优阶段: 根据定位到的瓶颈点,进行系统性能调优。

4、测试阶段: 让调优后的系统进行性能测试,与准备阶段的各项指标进行对比,观察其是否属于预期,如果瓶颈点没有消除或者性能指标不符合预期,则重复2和3步骤。

  • 准备阶段详解:

    对性能问题进行粗略评估, 比如线上应用日志级别不合理,可能会在大流量的时候导致CPU和磁盘的负载过高,这种情况需要调整日志级别;

    了解应用的总体架构, 应用依赖的外部接口和本身核心接口有哪些,使用了哪些框架和组件,哪些接口,模块的使用率较高,上下游的数据链路是怎么样的;

    了解应用对应的服务器信息, 服务器所在的集群信息、服务器的CPU、内存等信息,安装的操作系统Linux版本信息,服务器是容器还是虚拟机,所在的宿主机混部(把集群混合起来,将不同类型的任务调度到相同的物理资源上,通过调度,资源隔离等控制手段 , 在保障 SLO(服务等级目标) 的基础上,充分使用资源能力,极大降低成本)后是否当前应用有影响等。

    a、通过压测工具或者压测平台(如果公司有的话),对应用进行压力测试,获取当前应用的宏观指标。也可以结合当前的实际业务和过往的监控数据,去统计一些核心业务指标,比如午高峰的服务TPS。

    • 响应时间

    • 吞吐量

    • TPS

    • QPS

    • 消费速率(对于使用MQ的中间件)

    b、可以Linux基准测试工具,得到文件系统、磁盘I/O、网络等性能报告,还有GC、Web服务器、网卡流量等信息。

    • jmeter

    • ab

    • loadrunnerwrk

    • wrk

  • 测试阶段详解:

    性能瓶颈点通常呈现2/8分布, 即80%的性能问题通常是由20%的性能瓶颈点导致的,2/8原则也以为着并不是所有的性能问题都值得去优化;

    不要过度追求应用的单机性能, 如果单机表现良好,则应该从架构的角度去思考;比如过滤追求CPU的性能而忽略了内存方面的瓶颈;

    整个应用的优化,应与线上系统隔离, 新的代码上线应该有降级方案。

2.3 工具箱

工欲善其事必先利其器,我们该如何选择合适的工具呢?先来看看Linux性能工具图吧。

上面的这张图非常经典,是做性能优化时候非常好的参考资料,但是事实上,我们在实际运用的时候,会发现可能并不合适。所以需要给出一张更为实用的图,该图从系统层、应用层(含组件层)的角度,列举我们在分析性能问题需要关注的各项指标,这些点是最有可能出现性能瓶颈的地方。

下面针对不同层次的核心性能指标做如下分析,同时也会介绍如何初步根据这些指标,判断系统或者应用是否存在性能瓶颈点。

2.3.1 网络

这里说的网络指的是应用层的网络,通常指的是:

  • 网络带宽:表示链路的最大传输速率;
  • 网络吞吐:表示单位时间内成功传输的数据量大小;
  • 网络延时:表示从网络从请求发出后直到收到远端响应,所需要的时间;
  • 网络连接数和错误数。

应用层的网络瓶颈有如下几类:

  • 网络出现分区;
  • 集群或机器所在的机房的网络带宽饱和,影响应用的TPS/QPS的提升;
  • 网络吞吐出现异常,如接口存在大量的数据传输,造成贷款占用过高;
  • 网络连接出现异常或错误。

带宽和网络吞吐两个指标,一般会关注整个应用的,并通过监控系统可以直接得到,如一段时间内出现了明显的指标上升,说明存在网络瓶颈。对于单机,可以使用sar命令得到网络接口,进程的网络吞吐。

使用ping或hping3可以得到是否出现网络分区、网络具体时延。整个应用链路的时延,可以通过中间件埋点后输出的trace日志得到链路上各个环节的时延信息。

使用netstat、ss和sar可以获取网络连接数或网络错误数。系统可以支撑的网络连接数是有限的,一是会占用文件描述符,二是会占用缓存。

2.3.2 磁盘和文件

磁盘以及文件系统主要关注的指标有,常用命令有iostat(用于真个系统)和pidstat(用于具体的I/O进程):

  • 磁盘I/O利用率:是指磁盘处理I/O的时间百分比;
  • 磁盘吞吐量:是指每秒的I/O请求大小,单位为KB;
  • I/O响应时间:是指I/O请求从发出到收到响应的间隔,包含在队列中等待的时间和处理时间;
  • IOPS(Input/Output Per Second):每秒I/O请求数;
  • I/O等待队列大小:是指平均I/O队列长度,队列长度越短越好。
[root@iz2zea13o0oyywo7z5hawlz ~]# iostat -dx
Linux 3.10.0-693.2.2.el7.x86_64 (iz2zea13o0oyywo7z5hawlz) 	02/16/2020 	_x86_64_	(1 CPU)

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.11    0.01    0.29     0.23     2.61    18.68     0.00    5.97    4.32    6.03   0.16   0.00

%util: 磁盘的I/O利用率,同CPU利用率一样,这个值也可能超过100%(存在并行I/O);

rKB/s、wKB/s: 每秒从磁盘读取和写入的数据量,即吞吐量,单位为KB;

r_await、w_await: 读和写请求处理的响应时间;

r/s、w/s: 每秒发送给磁盘的读请求数和写请求数;

svctm: 该指标废弃,表示处理I/O所需的平均时间;

pidstat 的输出大部分和 iostat 类似,区别在于它可以实时查看每个进程的 I/O 情况。

如何判断磁盘的指标出现了异常?

1、当 %util (磁盘利用率)长时间超过80%,或者响应时间过大(SSD,从0.0x毫秒到1.x毫秒不等,机械硬盘一般为5毫秒到10毫秒),通常意味着磁盘I/O存在性能瓶颈。

2、如果 %util 很大,而rKB/s和wKB/s很小,一般因为存在较多的磁盘随机读写,最好把随机读写优化成顺序读写,可以通过strace或blktrace观察I/O是否连续判断是否是顺序读写行为,随机读写应该关注IOPS指标,顺序读写可以关注吞吐量指标。

3、如果avgqu-sz 比较大,说明有很多I/O在请求队列中等待。如果单块磁盘的队列长度持续超过2,一般认为该磁盘存在I/O性能问题。

2.3.3 CPU和线程

CPU关注的指标主要有以下几个。常用的命令有top、ps、uptime、vmstat、pidstat等。

  • CPU利用率(CPU Utilization)
  • CPU平均负载(Load Average)
  • 上下文切换次数(Context Switch)
top - 14:58:36 up 18 days,  1:31,  1 user,  load average: 0.00, 0.01, 0.05
Tasks:  63 total,   1 running,  62 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1883420 total,   109840 free,   187712 used,  1585868 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  1494900 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                                              
 1008 root      10 -10  132300  14672   9420 S  1.0  0.8 210:54.23 AliYunDun                                                            
    1 root      20   0   43264   3708   2500 S  0.0  0.2   0:12.20 systemd 

第一行显示内容:当前时间、系统运行时间以及正在登录的用户数。load average后的三个数字,依次表示过去1分钟、5分钟、15分钟的平均负载。CPU的平均负载和CPU的使用率没有直接的关系。

第三行显示内容:表示CPU利用率,计算公式:CPU利用率 = 1 - CPU空闲时间/CPU总的时间。 注:top工具显示的CPU利用率是把所有的CPU核的数值加起来,即8核CPU的利用率最大可以达到800%,可以用(htop命令代替top进行查看)。

查看CPU上下文切换次数可以用vmstat命令进行查看:

[root@iz2zea13o0oyywo7z5hawlz ~]# vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 109592 141200 1445092    0    0     0     3   12   22  0  0 99  0  0

上面的 cs(Context Swtich) 就是每秒上下文切换次数,过多的上下文切换次数,会把CPU时间消耗在寄存器、内核栈以及虚拟内存等数据保存和恢复上,从而缩短进程真正的运行时间,导致系统的整体性能大幅下降。

us、sy分别是用户态和内核态的CPU利用率。

vmstat给出的是系统总体的上下文切换情况,想要查看每个进程的上下文切换详情,需要使用pidstat命令,该命令还可以查看某个进程用户态和内核态的CPU利用率。

CPU相关指标异常的分析思路是什么呢?

1、CPU利用率: 观察某段时间系统或者应用进程的CPU利用率一直很高(单个超过80%),可以多次使用jstack命令dump应用线程栈查看热点线程代码。

2、CPU平均负载: 平均负载高与CPU数量70%,意味着系统存在瓶颈,通过监控系统检测平均负载的变化趋势,更容易定位问题,有时候加载大文件的时候,也会导致平均负载瞬时升高。如果1分钟/5分钟/15分钟的三个值相差不是很大,则说明系统负载很平稳,如果这三个值逐渐降低,说明负载在逐渐升高,需要重点关注。

3、CPU上下文切换次数: 这个指标取决于系统本身的CPU性能,以及当前应用的工作情况。如果系统或者应用的上下文切换次数出现数量级的增长的时候,则说明有很大概率存在性能问题,如果是非自愿的上下文切换次数大幅度上升,说明有太多的线程竞争CPU。

这三个指标是密切相关的,如频繁的CPU上下文切换,可能会导致平均负载升高。

关于线程,可关注的异常有:

  • 线程的总数是否过多。 线程过多,就会在CPU上频繁的进行上下文切换,同时线程过多也会消耗内存,线程的总数大小和应用本身和机器配置相关。
  • 线程的状态是否异常。 观察WAITING/BLOCKED 状态线程是否过多(线程数量设置过多或锁竞争剧烈),综合应用内部锁使用的情况进行进一步分析。
  • 结合CPU利用率,观察是否存在大量消耗CPU的线程。

2.3.4 内存和堆

和内存相关的指标主要有以下几个,常用命令是top、free、vmstat、pidstat以及JDK自带的一些工具。

  • 系统内存使用情况,包括剩余内存、已用内存、可用内存、缓存/缓冲区;
  • 进程(包含Java进程)的虚拟内存、常驻内存、共享内存;
  • 进程的缺页异常数、包含主缺页异常和次缺页异常;
  • Swap换入和换出的内存大小、Swap参数配置;
  • JVM堆的分配、JVM启动参数;
  • JVM堆的回收、GC情况。

使用free查看系统内存使用情况和Swap分区使用情况。

[root@iz2zea13o0oyywo7z5hawlz ~]# free -h
              total        used        free      shared  buff/cache   available
Mem:           1.8G        182M        106M        364K        1.5G        1.4G
Swap:            0B          0B          0B

Swap:把一个本地文件或者一块磁盘的空间作为内存来使用,包括换入和换出两个过程。Swap分区的升高一般和磁盘的使用强相关,具体分析,需要结合缓存的使用情况,swappiness阈值以及匿名页和文件页的活跃情况综合分析。

buff/cache:缓存和缓冲区大小。缓存(cache): 从磁盘读取的文件或者向磁盘写文件的临时存储数据,面向文件。使用cachestat可以查看真个系统缓存的读写命中情况,使用cachetop可以观察每个进程缓存的读写命中情况。 缓冲区(buff): 写入磁盘数据或者从磁盘直接读取的数据的临时存储,面向块设备。free 命令的输出中,这两个指标是加在一起的,使用 vmstat 命令可以区分缓存和缓冲区,还可以看到 Swap 分区换入和换出的内存大小。

性能优化中常见的内存问题有哪些呢?

1、系统剩余内存/可用内存不足(某个进程占用太多、系统本身内存不足),内存溢出;

2、内存回收异常:内存泄漏(进程在一段时间内内存使用持续走高),GC频率异常;

3、缓存使用过大(大文件读取或写入)、缓存命中率不高;

4、缺页异常过多(频繁的I/O读);

5、Swap分区使用异常(使用过大)。

内存相关指标异常后,分析的思路是怎么样的?

1、使用free发现缓存/缓冲区占用不大,排序缓存/缓冲区对内存的影响;

2、使用vmstat或者sar观察一下各个进程内存使用变化的趋势,发现某个进程的内存使用持续走高;

3、Java应用,使用jmap/VisualVM/heap dump等分析工具观察对象内存的分配,或者通过jstat观察GC后应用的内存变化;

4、结合业务场景,定位内存泄漏/GC参数配置不合理/业务代码异常等。

2.4 使用总结

有一些工具频繁出现,总结如下:

  • CPU:top、vmstat、pidstat、sar、perf、jstack、jstat;

  • 内存:top、free、vmstat、cachetop、cachestat、sar、jmap;

  • 磁盘:top、iostat、vmstat、pidstat、du/df;

  • 网络:netstat、sar、dstat、tcpdump;

  • 应用:profiler、dump。

三、 性能优化思路总结

来源:安徽网站优化

$flag 上一页 下一页