Java性能权威指南-总结21

Java EE性能调优

对象序列化

不同系统间的数据交换可以使用XMLJSON和其他基于文本的格式。Java进程间交换数据,通常就是发送序列化后的对象状态。尽管序列化在Java中随处可见,但Java EE中还有两点需要重点考虑。

  • Java EE服务器间的EJB调用——远程EJB调用——通过序列化交换数据。
  • HTTP会话状态通过对象序列化的方式来保存,这让HTTP会话可以高可用。

JDK提供了默认的序列化对象机制,以实现SerializableExternalizable接口。实际上,默认序列化的性能还有提升的空间,但此时进行过早的优化的确不太明智。特定的序列化和反序列化代码需要很多时间编写,而且也比默认的序列化代码更难维护。编写正确的序列化代码会有一些棘手,试图优化代码也会增加出错的风险。

transient字段

一般来说,序列化的数据越少,改进性能所需的代价就越少。 将字段标为transient,默认就不会序列化了。类可以提供特定的writeobject()readobject()以处理这些数据。如果不需要这些数据,简单地将它标记为transient就足够了。

覆盖默认的序列化

writeobject()readobject()可以全面控制数据的序列化。序列化很容易出错。为了了解序列化优化的困难性,以一个表示位置的简单对象Point为例:

	public class Point inplements Serializable {
		private int x;
		private int y;
		...
	}

在测试的机器上,100000个这样的对象可以在133毫秒内序列化,在741毫秒内反序列化。但即便像这么简单的对象,性能——即便非常困难——也能改善。

	public class Point implements Serializable {
		private transient int x;
		private transient int y;
		...
	
		private void writeObject(ObjectOutputStream oos) throws IOException {
			oos.defaultWriteobject();
			oos.writeInt(x);
			oos.writeInt(y);
		}
		private void readobject(ObjectInputStream ois) throws IOException,ClassNotFoundException {
			ois.defaultReadobject();
			x= ois.readInt();
			y = ois.readInt();
		}
	}

在测试机器上序列化100000个这样的对象仍然要花费132毫秒,但反序列化只需要468毫秒——改善了30%。如果简单对象的反序列化占用了相当大一部分程序运行的时间,像这样优化就比较有意义。然而请当心,这会使得代码难以维护,因为字段被添加、移除了,等等。

到目前为止,代码更为复杂了,但功能上依然正确(且更快)。注意,将此技术应用到一般场景时务必要谨慎:

	public class TripHistory implements Serializable {
		private transient Point[] airportsVisited;
		....
		//注意,这段代码不正确!
		private void writeObject(ObjectoutputStream oos) throws IOException {
			oos.defaultwriteobject();
			oos.writeInt(airportsVisited.length);
			for (int i = 0; i < airportsVisited.length; i++) {
				oos.writeInt(airportsvisited[i].getx());
				oos.writeInt(airportsVisited[i].getY());
			}
		}
		
		private void readobject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
			ois.defaultReadobject();
			int length = ois.readInt();
			airportsVisited = new Point[length];
			for (int i = 0; i < length; i++) {
				airportsVisited[i]= new Point(ois.readInt(), ois.readInt());
			}
		}
	}

此处的字段airportsVisited是表示出发或到达的所有机场的数组,按照离开或到达它们的顺序排列。有些机场,像JFK,在数组中出现得比较频繁,SYD(目前)只出现过一次。

由于序列化对象引用的代价比较昂贵,所以上述代码要比默认的数组序列化机制快:在测试的机器上,100000个Point对象的数组序列化用时4.7秒,反序列化用时6.9秒。上述“优化”使得序列化只用了2秒,反序列化只用了1.7秒。

然而这段代码是不正确的。指定JFK位置的数组引用都指向相同的对象。这意味着,如果发现数据不正确而更改单个JFK,那数组中的所有引用都会受到影响(因为它们引用的是相同的对象)。

用上述代码反序列化数组时,这些JFK引用就会变为独立的、不同的对象。当某个对象更改时,就只有它发生改变,结果它的数据就不同于其他那些表示JFK的对象了。

这条原则非常重要,应该铭记于心,因为序列化的调优常常就是如何对对象的引用进行特殊处理。 做对了,序列化的性能可以获得极大提升;做错了,就会引入不易察觉的bug。鉴于此,来考察一下StockPriceHistory的序列化,看看如何优化序列化。以下是这个类的字段:

	public class StockPriceHistoryImpl implements StockPriceHistory {
		private String symbol;
		protected SortedMap<Date, StockPrice> prices = new TreeMap<>();
		protected Date firstDate;protected Date lastDate;
		protected boolean needsCalc = true;
		protected BigDecimal highPrice;
		protected BigDecimal lowPrice;
		protected BigDecimal averagePrice;
		protected BigDecimal stdDev;
		private Map<BigDecimal, ArrayList<Date> histogram;
		..
		public StockPriceHistoryImpl(String s, Date firstDate, Date lastDate) {
			prices = ...
		}
	}

当以给定标志s构造StockPriceHistoryImpl对象时,会创建和存储SortedMap类型的变量prices,键值为startend之间的所有股票价格的时间。构造函数也设置保存了firstDatelastDate。除此之外,构造函数没有设置任何其他字段,它们都是延迟初始化。当调用这些字段的getter方法时,getter会检查needsCalc是否为真。如果为真,就会立即计算这些字段的值。

计算包括创建histogram,它记录了该股票特定的收盘价出现在哪些天。histogram包含的BigDecimalDate对象的数据与prices中的相同,只是看待数据的方式不同。所有的延迟加载字段都可以由prices数组计算得来,所以它们都可以标记为transient,并且在序列化和反序列化时不需要为它们做额外的工作。这个例子比较简单,因为代码已经完成了字段的延迟初始化,因此在接收数据时,可以一直延迟初始化。即便字段要即刻初始化,也仍然可以将可计算字段标记为transient,而在readobject()方法中重新计算它们的值。

注意,上述做法也维护了priceshistogram对象之间的关系:重新计算histogram时,会将已存在的对象塞到新的map中。这种做法在绝大多数情况下都能收到优化效果,但有时也会降低性能。下表就是这种情况,该表显示了histogram对象有无transient字段时进行序列化和反序列化所花费的时间,以及序列化数据的大小。
在这里插入图片描述
目前来看,这个例子中的对象序列化和反序列化节约了大约15%的时间。但这个测试实际上没有在接收时重建histogram对象:对象只有在接收数据的代码首次对其进行访问时才会创建。

有些时候并不需要histogram对象;客户端可能只关心特定日子里的股价,而不是整个histogram。还有一些不常见的情况,比如如果总是需要histogram,且测试中计算所有的histogram用时超过了3.1秒,那么延迟初始化字段就确实会导致性能下降。

在这个例子中,计算histogram并不属于这种情况——这是一种非常快的操作。一般来说,重新计算数据片段的代价很少会高于序列化和反序列化数据。 但在代码优化时仍然需要考虑。这个测试实际上并不向系统外传播数据,只是在预先分配的字节数组中写数据和读数据,所以它只是衡量了序列化和反序列化所用的时间。另外,histogram字段标为transient也减少了13%的数据大小。通过网络传送数据时,这就变得非常重要了。

压缩序列化数据

上述两种方法引出了改善序列化代码性能的第3种方法:数据序列化之后再进行压缩,使得它可以更快地在慢速网络上传输。StockPriceHistoryCompress在序列化时对prices进行了压缩:

	public class StockPriceHistoryCompress implements StockPriceHistory, Serializable {
		private byte[] zippedPrices;
		private transient SortedMap<Date, StockPrice> prices;
		
		private void writeobject(ObjectoutputStream out) throws IOException {
			if (zippedPrices == null) {
				makeZippedPrices()
			}
			out.defaultWriteObject();
		}
		
		private void readobject(ObjectInputStream in) throws IOException, ClassNotFoundException {
			in.defaultReadObject();
			unzipPrices();
		}
		
		protected void makeZippedPrices() throws IOException {
			ByteArrayoutputStream bais = new ByteArrayOutputStream();
			GZIPOutputStream zip = new GZIPOutputStream(bais);
			ObjectoutputStream oos = new ObjectoutputStream(new BufferedoutputStream(zip));
			oos.writeObject(prices);
			oos.close();
			zip.close();
			zippedPrices = bais.toByteArray();
		}
		
		protected void unzipPrices() throws IOException, ClassNotFoundException {
			ByteArrayInputStream bais = new ByteArrayInputStream(zippedPrices);
			GZIPInputStream zip = new GZIPInputStream(bais);
			ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(zip));
			prices = (SortedMap<Date, StockPrice>) ois.readobject();
			ois.close();
			zip.close();
		}
	}

makeZippedPrices()prices序列化成字节数组后保存,然后通常在writeobject()中调用defaultwriteobject()进行序列化。(事实上,如果可以定制序列化,将zippedPrices数组变成transient直接序列化数组的长度和字节会好一些。不过这个代码示例要清楚一点,且简单一些也更好。)在反序列化时,操作反过来执行。

如果目标是序列化成字节流(就像原先的示例代码一样),这就是个糟糕的提议。这并不令人惊奇,因为压缩字节所需的时间大大超过了写入本地字节数组的时间。参见下表。
10000个对象序列化和反序列化、带压缩和不带压缩时所用时间的对比
在这里插入图片描述
表中最有趣的是最后一行。在该轮测试中,数据在发送前进行了压缩,但readobject()并没有调用unzipPrices(),而是依据需要,在客户端首次调用getPrice()时才调用该方法。readobject()不再调用unzipPrices()后,就只有几个BigDecimal对象需要反序列化,速度非常快。

在这个例子中,很可能会出现客户端永远不需要实际的股票价格的情况:客户端可能只需要调用getHighPrice()和类似的方法获取合计数据。如果所有方法都是只在需要时获取数据,那么延迟解压价格数据信息就能节省大量时间。如果对象可能需要持久化,延迟解压也会有用(比如,备份HTTP会话状态,以防应用服务器失败)。延迟解压既节约CPU时间(因为跳过了解压),也节约内存(因为压缩后的数据需要的内存空间更小)。

所以,即便应用在高速局域网络中运行——尤其当目标是节约内存而不是时间时——对序列化数据进行压缩并延迟解压也仍然很有用。如果序列化是为了在网络中传输,那任何数据压缩都会有益处。 下表同样是对10000个股票对象进行序列化,不过这次它将数据传向了另一个进程。这个进程可以是在同一个机器上,也可以在通过宽带连接访问的其他机器上。
10000个对象的网络传输时间对比
在这里插入图片描述
同一机器上的两个进程之间的网络通信是最快的——虽然通信数据会发送到操作系统层,但压根不用通过网络。即便在这种情况下,压缩数据和延迟解压的性能仍然是最快的(至少在这个测试中是如此——但小数据量还是会有所衰退)。可以预料的是,一旦网络速度比较慢,传输数据又有数量级上的差别,总的耗费时间就会有巨大的差别。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值