背景
公司*****系统出现操作无响应的情况。操作界面是报表直接预览和表报编辑后预览
根据同事描述排除网络、硬件等外在因素,主要是cpu飙高和内存吃紧的情况。
线上jvm参数如下:jvm参数基本没有,只有内存溢出dump和gc打印
分析
内存分析
首先操作报表预览,通过jstat分析gc和内存情况。
如图:这是点击了三次预览界面的过程。每次预览都会进行一次YoungGC,偶尔两次。第三次点击伴随一次FullGC。从数据中大致判断一次预览会使用600M左右的内存。这是一个非常庞大的内存使用了
知道内存使用非常庞大,那么现分析为何内存使用如此之大。点击预览报表后线上马上使用jmap查看(稍微快点,此处导出堆栈的话会生成大概6G的文件,传送太慢了),此处可以多次执行命令对比前后数据变化。图中是内存占用比较多的一次。发现占用最多的依次是 Object数组、HashMap的Node、ArrayList、HashMap中Node数组、byte数组、int数组、Cell。
前面的数据都是JDK自带的类,无法分析。此处排名靠前的自己的对象是urport 中的Cell。现重点分析Cell。
先看Cell源码,发现Cell对象的成员变量太多了。Object[]占用最想最多,但是Object[]暂时未知,所以先分析HashMap$Node和ArrayList。
public class Cell implements ReportCell {
private String name;
private int rowSpan;
private int colSpan;
/**
* 下面属性用于存放分页后的rowspan信息
* */
private int pageRowSpan=-1;
private String renderBean;
/**
* 当前单元格计算后的实际值
*/
private Object data;
/**
* 存储当前单元格对应值在进行格式化后的值
*/
private Object formatData;
private CellStyle cellStyle;
private CellStyle customCellStyle;
private Value value;
private Row row;
private Column column;
private Expand expand;
private boolean processed;
private boolean blankCell;
private boolean existPageFunction;
private List<Object> bindData;
private Range duplicateRange;
private boolean forPaging;
private String linkUrl;
private String linkTargetWindow;
private List<LinkParameter> linkParameters;
private Map<String,String> linkParameterMap;
private Expression linkUrlExpression;
private List<ConditionPropertyItem> conditionPropertyItems;
private boolean fillBlankRows;
/**
* 允许填充空白行时fillBlankRows=true,要求当前数据行数必须是multiple定义的行数的倍数,否则就补充空白行
*/
private int multiple;
/**
* 当前单元格左父格
*/
private Cell leftParentCell;
/**
* 当前单元格上父格
*/
private Cell topParentCell;
/**
* 当前单元格所在行所有子格
*/
private Map<String,List<Cell>> rowChildrenCellsMap=new HashMap<String,List<Cell>>();
/**
* 当前单元格所在列所有子格
*/
private Map<String,List<Cell>> columnChildrenCellsMap=new HashMap<String,List<Cell>>();
private List<String> increaseSpanCellNames;
private Map<String,BlankCellInfo> newBlankCellsMap;
private List<String> newCellNames;
...后面省略...
public Cell newCell(){
Cell cell=new Cell();
cell.setColumn(column);
cell.setRow(row);
cell.setLeftParentCell(leftParentCell);
cell.setTopParentCell(topParentCell);
cell.setValue(value);
cell.setRowSpan(rowSpan);
cell.setColSpan(colSpan);
cell.setExpand(expand);
cell.setName(name);
cell.setCellStyle(cellStyle);
cell.setNewBlankCellsMap(newBlankCellsMap);
cell.setNewCellNames(newCellNames);
cell.setIncreaseSpanCellNames(increaseSpanCellNames);
cell.setDuplicateRange(duplicateRange);
cell.setLinkParameters(linkParameters);
cell.setLinkTargetWindow(linkTargetWindow);
cell.setLinkUrl(linkUrl);
cell.setPageRowSpan(pageRowSpan);
cell.setConditionPropertyItems(conditionPropertyItems);
cell.setFillBlankRows(fillBlankRows);
cell.setMultiple(multiple);
cell.setLinkUrlExpression(linkUrlExpression);
return cell;
}
首先Cell对象比较多,因为一个Cell代表一个网格,当时数据是158列,3000多行数据。这样Cell数量约=158*3000
rowChildrenCellsMap和columnChildrenCellsMap在new对象的时候就会初始化对象。从图二中也可以发现HashMap的对象数量大致是Cell的两倍(HashMap=1064348,Cell=515786)。而这两个Map中的的Value都是ArrayList。这就是为什么ArrayList实例数那么多的原因。
那为什么HashMap$Node中的实例数那么多呢?查看HashMap源码就知道,因为HashMap每一个Value都会有一个Node对象来包装。所以Node和ArrayList实例数差不多,都是2000多万个。有200多万的差距是因为其他Map比如LinkedHashMap(100多万实例)、HashTable(34万多实例数),或者HashMap的Value不是ArrayList。
那为什么Object[]会那么多呢?这个比较简单,查看ArrayList源码就知道每一个ArrayList都会对应一个Object[]。所以ArrayList和Object[]几乎一摸一样。
知道了为什么rowChildrenCellsMap和columnChildrenCellsMap中会有那么多的ArrayList呢?
Cell源码中如下两个方法会不断的new ArrayList。
public void addRowChild(Cell child){
String name=child.getName();
List<Cell> cells=rowChildrenCellsMap.get(name);
if(cells==null){
//发现此处创建的ArrayList过多
cells=new ArrayList<Cell>();
rowChildrenCellsMap.put(name, cells);
}
if(!cells.contains(child)){
cells.add(child);
}
if(leftParentCell!=null){
//此处会不停的添加,越左边的元素将会越多,导致内存暴涨
leftParentCell.addRowChild(child);
}
}
public void addColumnChild(Cell child){
String name=child.getName();
List<Cell> cells=columnChildrenCellsMap.get(name);
if(cells==null){
cells=new ArrayList<Cell>();
columnChildrenCellsMap.put(name, cells);
}
if(!cells.contains(child)){
cells.add(child);
}
if(topParentCell!=null){
topParentCell.addColumnChild(child);
}
}
修改思路:
1、要想解决内存飙高的情况首先应从Cell源码处入手进行修改,看能不能模仿newCell()思路,对每个Cell的rowChildrenCellsMap和columnChildrenCellsMap进行重复利用(重点考虑,但是改动可能很大甚至不能实现,难度最大),本地测试直接注释代码可以优化将近一半的内存占用,而且随着总内存升高优化比例越高
2、查看Cell的addRowChild()和addColumnChild() 在new ArrayList的时候没有初始容量,在ArrayList源码中在new的ArrayList的时候如果没有初始容量,那么自动扩容的初始值为10,后续扩容每次增加为当前容量的1.5倍,因此占用内存比较多。那么考虑在初始化的时候对容量进行设置(实测可以优化内存12%左右)
3、对sql查询部分进行优化,不能用进行查询,首先效率低,mysql会多一次查询;其次*会查出来很多无用数据,首先是网络IO占用多,其次是内存占用多。本次测试的报表就是300多个字段,但是实际用的只有150多个字段,当数据量上去了,内存浪费显而易见。本地测试了2000条数据,HashMap$Node占用内存非常多,因为Durid查询出来后会把每条数据保存为一个Map
4、设置合适jvm参数,这个需要在对Cell修改后进调试,因为现在的情况怎么设置参数都是有问题的,下面给了一个比较简单的估计值
JAVA_OPTS="${JAVA_OPTS} -server -Xms6000m -Xmx6000m -Xmn2048m -XX:MaxMetaspaceSize=200m -Xss512k -XX:MaxTenuringThreshold=10"
JAVA_OPTS="${JAVA_OPTS} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/pmr/"
JAVA_OPTS="${JAVA_OPTS} -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/pmr/gc_%t.log -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"
JAVA_OPTS="${JAVA_OPTS} -XX:+DisableExplicitGC -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
JAVA_OPTS="${JAVA_OPTS} -XX:+AlwaysPreTouch"
JAVA_OPTS="${JAVA_OPTS} -XX:-OmitStackTraceInFastThrow"
5、不修改源码,对每个报表的接口进行加锁,不让其并发处理。这样防止并发的时候出现内存溢出(治标不治本,暂不考虑)
cpu
cpu的问题只是在本地环境复现了。(原因未知)
跟代码发现进入如下方法会走不出来。因为遍历的数据越来越多。ReportBuilder类中
public void buildCell(Context context,List<Cell> cells)
其实最终也是因为addRowChild()和addColumnChild()方法一直执行的问题。
只要找到了addRowChild()和addColumnChild()方法的问题,cpu问题也就解决了。