服务器内存监测

2023-10-22 16:24:00 浏览数 (1)

内存,作为计算机的四大件之一,当它充足的时候,我们不会察觉到它的存在,直到它悄无声息的一点点失去,才会越加珍惜。

而对于程序员而言,如何避免内存泄漏也是一门学问,倘若不加以控制,那么无论多大的内存都会有消耗殆尽的那天。本文当然不是研究如何分析内存泄漏的产生原因与解决方案,而是在此之前的一步,通过简单的内存监测方式来预测内存泄漏的 潜在可能性 或者 偶发性 等。

对于不同的主流编程语言,都有着读取系统内存与应用堆内存的相关类,因为本网站后端是springboot编写的,所以这里就介绍java语言的实现方式。

后端相关类编写

首先是pojo类的创建,用于存储每个时间点的系统信息数据。我这边需要监测 系统内存jvm堆内存 ,最终的结果会展示各个时间点的内存情况,所以需要一个时间类,表示每个切片的时间点。

代码语言:javascript复制
package top.dreamcenter.dreamcenter.entity;

import lombok.Data;

import java.util.Calendar;

/**
 * 系统信息
 */
@Data
public class SystemInfo {

    /**
     * 系统总内存 (MB)
     */
    private long totalMemory;

    /**
     * 现存内存 (MB)
     */
    private long nowMemory;

    /**
     * 总堆大小 (MB)
     */
    private long totalHeap;

    /**
     * 现堆大小 (MB)
     */
    private long nowHeap;

    /**
     * 记录的时间
     */
    private Calendar time;
}

接着,是最为核心的内存数据获取方式,采用工具类的方式封装。这边一定要注意的一点是 类OperatingSystemMXBean 引用的包是 com.sun.management包 下的,而默认的导入会是java.lang.management包下的!不要引错!不要引错!不要引错!其次,获取到的结果默认是字节B作为单位的long类型结果,对于如今的内存,都是GB级别,只需要知道MB数量级的结果即可,所以需要 val / 1024 / 1024 转化成MB表示的数值,更简单高效的,用位运算 val>>20,也可以达到同样的转化效果。

代码语言:javascript复制
package top.dreamcenter.dreamcenter.utils;

import com.sun.management.OperatingSystemMXBean;
import top.dreamcenter.dreamcenter.entity.SystemInfo;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.Calendar;

/**
 * 基础信息分析工具
 */
public class InfoAnalyzeUtil {
    /**
     * 获取系统信息
     * @return
     */
    public static SystemInfo getSystemInfo(){
        SystemInfo tmp = new SystemInfo();

        OperatingSystemMXBean osmx = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
        // a: all, n: now, m : memory, h : heap
        long a_m = osmx.getTotalPhysicalMemorySize();
        long n_m = osmx.getFreePhysicalMemorySize();

        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
        long a_h = heapMemoryUsage.getMax();
        long n_h = heapMemoryUsage.getUsed();

        tmp.setTotalMemory(a_m>>20);
        tmp.setNowMemory((a_m - n_m)>>20);
        tmp.setTotalHeap(a_h>>20);
        tmp.setNowHeap(n_h>>20);
        tmp.setTime(Calendar.getInstance());

        return tmp;
    }
}

接着就是要有个存储单元,用来存储不同时间切片的内存数据,可以采用内存或者redis方式存储,我这边简单起见,就直接用内存存储这些数据了,注册一个实例到spring的容器中,用于在系统的任何地方都能调用。为该集合实例设置名称。

代码语言:javascript复制
    @Bean("SystemInfoList")
    public List<SystemInfo> getSystemInfoList() {
        return new LinkedList<>();
    }

定时任务调用InfoAnalyzeUtil.getSystemInfo()来定时获取系统内存信息载入存储单元。我这边的设定是每分钟获取一次,while循环则是限制了存储单元最大的存储量为60,在这里表示的现实含义即是只记录近一小时的每分钟切片内存信息。另外设计这个60阈值的原因是——避免内存泄漏,如果不设定阈值,那么将会一直追加数据,而且还都无法释放,不断的消耗jvm堆空间。如果不深入计算的话,单个SystemInfo实例56B大小,最大61*56B=3416B≈3.34KB,深入计算Calendar对象,总和也不会达到MB级别。

代码语言:javascript复制
package top.dreamcenter.dreamcenter.schedule;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import top.dreamcenter.dreamcenter.entity.SystemInfo;
import top.dreamcenter.dreamcenter.utils.InfoAnalyzeUtil;

import javax.annotation.Resource;
import java.util.List;

@Component
public class CommonSchedule {

    @Resource(name = "SystemInfoList")
    private List<SystemInfo> systemInfoList;

    @Scheduled(cron = "0 * * * * ?")
    public void FreshSystemInfo(){
        systemInfoList.add(InfoAnalyzeUtil.getSystemInfo());
        while (systemInfoList.size() >= 60) systemInfoList.remove(0);
    }

}

定时任务已经不断的向存储单元装载数据了,接下来就是向前端页面提供接口获得数据。简单的返回即可。

代码语言:javascript复制
    @Resource(name = "SystemInfoList")
    private List<SystemInfo> systemInfoList;

    @GetMapping("/system")
    public RetResult<List<SystemInfo>> getSystemInfo() {
        return RetResult.success(systemInfoList);
    }

前端展示数据

本来想要找个轻量级的图表来绘制的,但是找来找去只有echarts可以使用,emmm。(前端用的Vue编写的)

首先按需导入echarts组件,减少打包的包体大小。(js/EchartsMini.js)

代码语言:javascript复制
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core'
// 引入柱状图图表,图表后缀都为 Chart
import {
  LineChart
} from 'echarts/charts'
// 引入提示框,标题,直角坐标系组件,组件后缀都为 Component
import {
  TitleComponent,
  TooltipComponent,
  GridComponent
} from 'echarts/components'
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import {
  SVGRenderer
} from 'echarts/renderers'

// 注册必须的组件
echarts.use(
  [TitleComponent, TooltipComponent, GridComponent, LineChart, SVGRenderer]
)

export {
  echarts
}

在合适位置加入图表div。

代码语言:javascript复制
<div>
  <!-- 图表 -->
  <div id="physicMemory" style="width: 600px;height:400px;float:left"></div>
  <div id="heapMemory" style="width: 600px;height:400px;float:left"></div>
</div>

引入echarts并且初始化将要用到的数据。timeMarkInterval是存储定时器id的,在销毁之前释放定时器;physicMemory和heapMemory获取图表div节点,用于echarts节点获取;systemInfo则会存储定时从服务器拉取到的数据。之后定时拉取数据存储,并且设定图表数据即可。

代码语言:javascript复制
import { echarts } from '../../js/EchartsMini'
import axios from 'axios'
export default {
  data () {
    return {
      ...,
      systemInfo: [],
      physicMemory: null,
      heapMemory: null,
      timeMarkInterval: null
    }
  },  mounted () {
    this.timeMarkInterval = setInterval(() => {
      axios.get('/api/info/system').then(res => {
        this.systemInfo = res.data.data
        this.initChart()
      })
    }, 60 * 1000)
  },
...

图表数据设置,formatDate格式化日期显示,initChart 数据解析并且调用setLineData设置图表数据。

代码语言:javascript复制
initChart () {
  var timeList = []
  var totalMemory = []
  var nowMemory = []
  var totalHeap = []
  var nowHeap = []
  this.systemInfo.forEach(item => {
    timeList.push(this.formatDate(Date.parse(item.time)))
    totalMemory.push(item.totalMemory)
    nowMemory.push(item.nowMemory)
    totalHeap.push(item.totalHeap)
    nowHeap.push(item.nowHeap)
  })

  if (this.physicMemory == null || this.heapMemory == null) {
    this.physicMemory = echarts.init(document.getElementById('physicMemory'))
    this.heapMemory = echarts.init(document.getElementById('heapMemory'))
  }
  this.setLineData(this.physicMemory, '物理内存大小(MB)', '总物理内存大小', '现内存大小', totalMemory, nowMemory, timeList)
  this.setLineData(this.heapMemory, '堆大小(MB)', '总堆大小', '现堆大小', totalHeap, nowHeap, timeList)
},
setLineData (myChart, title, totalDesc, nowDesc, total, now, timeList) {
  myChart.setOption({
    title: {
      text: title,
      textAlign: 'center',
      left: 'middle'
    },
    tooltip: {},
    xAxis: {
      data: timeList
    },
    yAxis: {},
    series: [
      {
        name: totalDesc,
        type: 'line',
        data: total,
        color: '#5f5',
        areaStyle: {
          color: '#5f5',
          opacity: 0.7
        }
      },
      {
        name: nowDesc,
        type: 'line',
        data: now,
        color: '#f55',
        areaStyle: {
          color: '#f55',
          opacity: 0.7
        }
      }
    ]
  })
},
formatDate (timeStamp) {
  var datetime = new Date(timeStamp)
  var hourStr = datetime.getHours() < 10 ? '0'   datetime.getHours() : datetime.getHours()
  var minuteStr = datetime.getMinutes() < 10 ? '0'   datetime.getMinutes() : datetime.getMinutes()
  var secondStr = datetime.getSeconds() < 10 ? '0'   datetime.getSeconds() : datetime.getSeconds()
  return `${hourStr}:${minuteStr}:${secondStr}`
}

最终的效果

以上大段的代码,看上去确实有些枯燥,直接上结果图吧。由图可见我这个系统堆内存通常消耗不到一百兆,后续可以将堆内存设定的再小一些,以提供给其它服务使用。总体内存是稳定状态,达到一定值会自动回收垃圾,占用率不会逐步提高,是个可控的系统。

倘若jvm内存出现了溢出的情况也可以使用arthas将堆快照dump出来,结合jvisualvm来定位问题,这边暂且也没有遇到该问题,暂不做赘述。

0 人点赞