记一次线上环境无响应的问题分析和解决(OOM?CPU飙高?)

背景

公司*****系统出现操作无响应的情况。操作界面是报表直接预览表报编辑后预览

根据同事描述排除网络、硬件等外在因素,主要是cpu飙高和内存吃紧的情况。

线上jvm参数如下:jvm参数基本没有,只有内存溢出dump和gc打印
(img-DbBJ5mRp-1595230323268)(/Users/liguoxin/Library/Application Support/typora-user-images/image-20200717112244094.png)]

分析

内存分析

首先操作报表预览,通过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问题也就解决了。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

年迈程序

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值