内存,作为计算机的四大件之一,当它充足的时候,我们不会察觉到它的存在,直到它悄无声息的一点点失去,才会越加珍惜。
而对于程序员而言,如何避免内存泄漏也是一门学问,倘若不加以控制,那么无论多大的内存都会有消耗殆尽的那天。本文当然不是研究如何分析内存泄漏的产生原因与解决方案,而是在此之前的一步,通过简单的内存监测方式来预测内存泄漏的 潜在可能性 或者 偶发性 等。
对于不同的主流编程语言,都有着读取系统内存与应用堆内存的相关类,因为本网站后端是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来定位问题,这边暂且也没有遇到该问题,暂不做赘述。