`
zhang_xzhi_xjtu
  • 浏览: 538306 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

实践中的重构27_不要忘了内存空间

阅读更多
方法在设计中,一般关注的是方法的功能契约,即方法需要什么样的参数,方法运行时会保持什么样的不变量,方法运行后会得到什么样的输出。较少会关注到方法的非功能性特征,典型的为方法的执行时间,方法执行时的内存空间消耗等等。
最近关注到一段代码,因为该段代码是导致OutOfMemoryError的一个因素,所以拿来一看。
	public enum WorkingDay {

		Monday("星期一"), Tuesday("星期二"), Wednesday("星期三"), Thursday("星期四"), Friday(
				"星期五");

		private String des;

		public static WorkingDay findWorkingDayByDes(String des) {
			return getDesWorkingDayMap().get(des);
		}

		private static Map<String, WorkingDay> getDesWorkingDayMap() {
			Map<String, WorkingDay> map = new HashMap<String, WorkingDay>();
			for (int i = 0; i < WorkingDay.values().length; i++) {
				map.put(WorkingDay.values()[i].des, WorkingDay.values()[i]);
			}
			return map;
		}

		private WorkingDay(String des) {
			this.des = des;
		}
	}

注意,原有问题的枚举定义中的枚举值个数较多,这里使用只包含5个枚举值的WorkingDay来说明问题。
该代码提供一个功能,使用des查找对应的WorkingDay枚举。因为每次查找的时候都会新建一个map,在并发较多的时候,新建了大量重复的对象,给jvm内存管理带来了不必要的压力,最后和其他因素共同导致OutOfMemoryError。
想到的修复方法很直接。
		public static WorkingDay findWorkingDayByDes_1(String des) {
			for (WorkingDay workingDay : WorkingDay.values()) {
				if (workingDay.des.equals(des)) {
					return workingDay;
				}
			}
			return null;
		}

咋看没有问题,但是这个实现还是有可能多分配空间的,因为不清楚values返回的数组是不是同一个,还是写个test比较保险。
	@Test
	public void test() {
		WorkingDay[] t1 = WorkingDay.values();
		WorkingDay[] t2 = WorkingDay.values();

		Assert.assertNotSame(t1, t2);

		Assert.assertEquals(t1.length, t2.length);

		for (int i = 0; i < t1.length; i++) {
			Assert.assertSame(t1[i], t2[i]);
		}
	}

恩,确定了,返回的数组不是同一个,虽然里面的元素是相同的。也就是说,还是存在一些内存的重复浪费。
Cache是计算机的编程利器啊。看来要控制内存分配较少的空间还是用cache比较靠谱。
		public static Map<String, WorkingDay> cache;

		static {
			cache = new HashMap<String, WorkingDay>();
			for (int i = 0; i < WorkingDay.values().length; i++) {
				cache.put(WorkingDay.values()[i].des, WorkingDay.values()[i]);
			}
		}

		public static WorkingDay findWorkingDayByDes_2(String des) {
			return cache.get(des);
		}

如此一来,整个程序运行期间所消耗的内存基本是确定的,不会随着压力的增大消耗太多的内存空间。
分享到:
评论
13 楼 dennis_zane 2011-06-09  
你说这里可能是压死骆驼的最后一根稻草,那还可以接受,你说这段代码会导致OOM,那就是信口开河。我可以写个并发程序来证明,调用N次也不会OOM。
12 楼 humaeks 2011-06-09  
zhang_xzhi_xjtu 写道
这个问题抛OOE说起来比较复杂。
楼上的例子之所以没有问题是因为。
1 单线程跑,没有并发。本次循环执行时内存不够回minor GC掉上一个循环申请的内存,
2 GC设置的是什么GC,一般大型系统用CMS GC,由于回收是并行做的,有可能无引用对象还是会占用内存的。
3 内存吃紧的时候,这种无谓的内存申请,有可能是压死骆驼的最后一根稻草。

这里的关键在于并发时,minor GC不掉其他线程的内存,并且提升至OLD,而OLD内存在major GC CMS回收时,是有一段时间处理不到同时并发的内存申请的。

CMS需要更多的内存空间,因为mark phase时程序还是在运行,程序可以申请更多的old空间。在mark phase中,CMS保证标识活对象,但是该过程中,活对象可能转变为垃圾,只能等待下一次GC才能回收。

总而言之:
内存是宝贵的,在可以用确定大小内存完成任务的情况下,采用多申请内存的方式会有OOE的风险。



前面会的帖子里面,提到来不及回收就很神奇。原因就在于,如果真的确定OOM是在这里引起的,那并发真的不知道要多少个了。
简单点的估算,需要的工作内存=并发数×每个并发需要的内存。由于这个map对每个并发需要的内存的增量是如此的小,但如果确实修改后就不出现oom的话,那么其并发数一定对比起jvm内存限制来说一定非常之大。实际需要调整的,并非这个增量把。
11 楼 ray_linn 2011-06-09  
连enum.values都ooe了...是不是楼主家的所有的代码都要加上staitc hashmap,因此最后导致内存空间不足。


明显在拼凑场景。
10 楼 zhang_xzhi_xjtu 2011-06-09  
这个问题抛OOE说起来比较复杂。
楼上的例子之所以没有问题是因为。
1 单线程跑,没有并发。本次循环执行时内存不够回minor GC掉上一个循环申请的内存,
2 GC设置的是什么GC,一般大型系统用CMS GC,由于回收是并行做的,有可能无引用对象还是会占用内存的。
3 内存吃紧的时候,这种无谓的内存申请,有可能是压死骆驼的最后一根稻草。

这里的关键在于并发时,minor GC不掉其他线程的内存,并且提升至OLD,而OLD内存在major GC CMS回收时,是有一段时间处理不到同时并发的内存申请的。

CMS需要更多的内存空间,因为mark phase时程序还是在运行,程序可以申请更多的old空间。在mark phase中,CMS保证标识活对象,但是该过程中,活对象可能转变为垃圾,只能等待下一次GC才能回收。

总而言之:
内存是宝贵的,在可以用确定大小内存完成任务的情况下,采用多申请内存的方式会有OOE的风险。

9 楼 dennis_zane 2011-06-09  
<div class="quote_title">坏孩子 写道</div>
<div class="quote_div">
<div class="quote_title">dennis_zane 写道</div>
<div class="quote_div">
<p>我很想请教下是怎么来不及回收。为了免的信口开河,我写了个简单测试,调用这个方法一亿次,观察gc情况。代码:</p>
<pre name="code" class="java">    /**
     * @param args
     */
    public static void main(String[] args) {
        long result = 0;
        for (int i = 0; i &lt; 100000000; i++)
            result += WorkingDay.findWorkingDayByDes("星期一").ordinal();
        System.out.println(result);

    }</pre>
<p> </p>
<p>jstat观察到的gc状况:</p>
<pre name="code" class="java">  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT  
25.00   0.00   0.00   0.36  12.75   2464    0.467     0    0.000    0.467
25.00   0.00   0.00   0.36  12.75   2582    0.488     0    0.000    0.488
  0.00  25.00   0.00   0.36  12.75   2707    0.510     0    0.000    0.510
  0.00  25.00   0.00   0.36  12.75   2787    0.528     0    0.000    0.528
  0.00  25.00  72.48   0.36  12.75   2866    0.550     0    0.000    0.550
25.00   0.00  18.12   0.36  12.75   2918    0.560     0    0.000    0.560
</pre>
 
<p>没有一次full gc,平均一次minor gc在1.8毫秒。</p>
</div>
<br>JDK版本,那个公司的jvm</div>
<p> </p>
<p>dennis@dennis-laptop:~$ /opt/jdk1.6.0_18/bin/java -version<br>java version "1.6.0_18"<br>Java(TM) SE Runtime Environment (build 1.6.0_18-b07)<br>Java HotSpot(TM) Server VM (build 16.0-b13, mixed mode)<br>dennis@dennis-laptop:~$ uname -a<br>Linux dennis-laptop 2.6.38-8-generic-pae #42-Ubuntu SMP Mon Apr 11 05:17:09 UTC 2011 i686 i686 i386 GNU/Linux<br></p>
8 楼 坏孩子 2011-06-08  
<div class="quote_title">dennis_zane 写道</div><div class="quote_div"><p>我很想请教下是怎么来不及回收。为了免的信口开河,我写了个简单测试,调用这个方法一亿次,观察gc情况。代码:</p>
<pre name="code" class="java">    /**
     * @param args
     */
    public static void main(String[] args) {
        long result = 0;
        for (int i = 0; i &lt; 100000000; i++)
            result += WorkingDay.findWorkingDayByDes("星期一").ordinal();
        System.out.println(result);

    }</pre>
<p> </p>
<p>jstat观察到的gc状况:</p>
<pre name="code" class="java">  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT  
25.00   0.00   0.00   0.36  12.75   2464    0.467     0    0.000    0.467
25.00   0.00   0.00   0.36  12.75   2582    0.488     0    0.000    0.488
  0.00  25.00   0.00   0.36  12.75   2707    0.510     0    0.000    0.510
  0.00  25.00   0.00   0.36  12.75   2787    0.528     0    0.000    0.528
  0.00  25.00  72.48   0.36  12.75   2866    0.550     0    0.000    0.550
25.00   0.00  18.12   0.36  12.75   2918    0.560     0    0.000    0.560
</pre>
 
<p>没有一次full gc,平均一次minor gc在1.8毫秒。</p></div><br/>JDK版本,那个公司的jvm
7 楼 dennis_zane 2011-06-08  
<p>我很想请教下是怎么来不及回收。为了免的信口开河,我写了个简单测试,调用这个方法一亿次,观察gc情况。代码:</p>
<pre name="code" class="java">    /**
     * @param args
     */
    public static void main(String[] args) {
        long result = 0;
        for (int i = 0; i &lt; 100000000; i++)
            result += WorkingDay.findWorkingDayByDes("星期一").ordinal();
        System.out.println(result);

    }</pre>
<p> </p>
<p>jstat观察到的gc状况:</p>
<pre name="code" class="java">  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT  
25.00   0.00   0.00   0.36  12.75   2464    0.467     0    0.000    0.467
25.00   0.00   0.00   0.36  12.75   2582    0.488     0    0.000    0.488
  0.00  25.00   0.00   0.36  12.75   2707    0.510     0    0.000    0.510
  0.00  25.00   0.00   0.36  12.75   2787    0.528     0    0.000    0.528
  0.00  25.00  72.48   0.36  12.75   2866    0.550     0    0.000    0.550
25.00   0.00  18.12   0.36  12.75   2918    0.560     0    0.000    0.560
</pre>
 
<p>没有一次full gc,平均一次minor gc在1.8毫秒。</p>
6 楼 liuzhiqiangruc 2011-06-08  
恩,很不错,这个static是必须的,随随便便写代码不行
5 楼 mtnt2008 2011-06-08  
humaeks 写道
GC来不及回收,这就很神奇。。。。。。。


很正常的事情。

其实,越是平常的事情,做好了并不容易。

楼主期待下一个
4 楼 kdlan 2011-06-08  
请教为什么GC会来不及回收呢?
map每次调用以后没有就没有任何变量引用map了
map最后都必然会被回收掉吧
当然效率会有点差
3 楼 Simon.Wang 2011-06-08  
这个问题都可以拿来做主题?
2 楼 Mic_X 2011-06-07  
这个很明显应该把map做成static
1 楼 humaeks 2011-06-07  
GC来不及回收,这就很神奇。。。。。。。

相关推荐

Global site tag (gtag.js) - Google Analytics