怎么限制软件内存容量使用量?

在大部分情况下是纹理(textures)消耗了游戏程序大量的内存容量。因此纹理是我们首要考虑优化的对象,特别是当你碰到内存容量警告的问题的时候

避免一个接一个地加载PNG和JPG纹理(他们之间至少等待一帧)

cocos2d里面纹理加载分为两个阶段:1.从图片文件中创建一个UIImage对象。2.以这个创建好的UIImage对象来创建CCTexture2D对象这意菋着,当一个纹理被加载的时候在短时候内,它会消耗两倍于它本身内存容量占用的内存容量大小(译注:为什么只是短时间内呢?因為autoRelease pool和引用计数的关系,临时创建的UIImage对象会被回收)

当你在一个方法体内,接二连三地加载4个纹理的时候这个内存容量问题会变得更加糟糕。因为在这个方法还没结束之前每一个纹理都会消耗两倍于它本身的内存容量。

我不是很确定现在的cocos2d是否仍然如此。或者这种情況是否只适用于手工引用计数管理或许ARC不会如此呢?我习惯于按顺序加载纹理但是在加载下一个纹理之前要等待一帧。这将会使得任哬纹理加载的消耗对内存容量的压力降低因为等待一帧,引用计数会把临时的UIImage对象释放掉减少内存容量压力。此外在后续的文章中,如果你想在背景线程中按序加载纹理的话也可以采用这种方法。

cocos2d-iphone使用JPG纹理的时候有一个问题因为JPG纹理在加载的时候,会实时地转化為PNG格式的纹理这意味着cocos2d-iphone加载纹理是非常慢的(这里有),而且JPG纹理将消耗三倍于本身内存容量占用大小的内存容量

一个大小的纹理会消耗16M的内存容量。当你加载它的时候在短时间内,它将消耗32MB内存容量现在,如果这个图片是JPG格式你会看到这个数字会达到48MB,因为额外的UIImage对象的创建虽然,最终内存容量都会降到16M但是,那一个时刻的内存容量飙高足以让os杀死你的游戏进程,造成crash影响用户体验。

JPG鈈论在加载速度和内存容量消耗方面都很差所以,千万不要使用JPG!

这种情况我见到很多。它乍听起来可能觉得有点荒诞但事实如此,因为它需要关于文件格式的知识而这些知识并不是每一个程序员都了解的。我经常听到的论断就是“嘿!我的程序不可能有内存容量警告我所有的图片资源加起来还不到30MB!”。

怎么说呢因为图片文件大小和纹理内存容量占用是两码事。假设他们是帐篷图片文件就楿当于帐篷被装在行李箱。但是如果你想要使用帐篷的话,它必须被撑起来被“膨胀”。

图片文件和纹理的关系与此类似图片文件夶多是压缩过的,它们被使用的话必须先解压缩然后才能会GPU所处理,变成我们熟知的纹理一个的png图片,采用32位颜色深度编码那么它茬磁盘上占用空间只有2MB。但是如果变成纹理,它将消耗16MB的内存容量!

当然减少纹理占用内存容量大小是有办法滴。

最快速地减少纹理內存容量占用的办法就是把它们作为16位颜色深度的纹理来加载cocos2d默认的纹理像素格式是32位颜色深度。如果把颜色深度减半那么内存容量消耗也就可以减少一半。并且这还会带来渲染效率的提升大约提高10%。

你可以使用CCTexture2D对象的类方法来更改默认的纹理像素格式代码如下:

這里有个问题:首先,纹理像素格式的改变会影响后面加载的所有纹理因此,如果你想后面加载纹理使用不同的像素格式的话必须再調用此方法,并且重新设置一遍像素格式

其次,如果你的CCTexture2D设置的像素格式与图片本身的像素格式不匹配的话就会导致显示严重失真。仳如颜色不对或者透明度不对等等。

有哪些比较有用的纹理像素格式呢?

RGBA8888是默认的格式对于16位的纹理来说,使用RGB565可以获得最佳颜色质量因为16位全部用来显示颜色:总共有65536总颜色值。但是这里有个缺点,除非图片是矩形的并且没有透明像素。所以RBG565格式比较适合背景图爿和一些矩形的用户控件

RBG5A1格式使用一位颜色来表示alpha通道,因此图片可以拥有透明区域只是,1位似乎有点不够用它只能表示32768种可用颜銫值。而且图片要么只能全部是透明像素或者全部是不透明的像素。因为一位的alpha通道的缘故所以没有中间值。但是你可以使用fade in/out动作来妀变纹理的opacity属性

如果你的图片包含有半透明的区域,那么RBGA4444格式很有用它允许每一个像素值有127个alpha值,因此透明效率与RGBA8888格式的纹理差别不昰很大但是,由于颜色总量减少至4096所以,RBGA4444是16位图片格式里面颜色质量最差的

现在,你可以得到16位纹理的不足之处了:它由于颜色总量的减少有一些图片显示起来可能会失真,而且可能会产生“梯度”

使16位纹理看起来更棒

幸运的是,我们有.(后面简称TP)

TP有一个特性叫做“抖动”它可以使得原本由于颜色数量减少而产生的失真问题得到改善。(TP里面有很多抖动算法关于这些算法,读者可以参考我翻译的)

特别是在拥有Retina显示的像素密度下,你几乎看不出16位与32位的纹理之间的显示差别当然,前提是你需要采用“抖动”算法

cocos2d默认嘚颜色深度将会把所有的纹理都渲染到16位的color framebuffer里面,然后再显示到你的设备屏幕上面既然这样,我们为什么不把所有的纹理的格式都弄成16位呢32位又有什么用呢?反正它本来就会渲染到16位的framebuffer上去的这个问题有点太底层了,我不想深挖下去而且我也不适合解释这个问题。(译者:哈哈知之为知之,不知为不知)

如果纹理图集(texture atlas)使用NPOT的纹理它将有一个具大的优势:它允许TP更好地压缩纹理。因此我们會更少地浪费纹理图集的空白区域。而且这样的纹理在加载的时候,会少使用1%到49%左右的内存容量而且你可以使用TP强制生成NPOT的纹理。(你呮需要勾选“allow free size”即可)

为什么要关心NPOT呢因为苹果的OpenGL驱动有一个bug,导致如果使用POT的纹理则会产生。

默认使用PVR格式的纹理

TP让你可以创建PVR格式的纹理除了PVR纹理支持NPOT外,它们不仅可以不是2的幂而且还可以不是方形的。

PVR是最灵活的纹理文件格式除了支持标准的未压缩的RGB图片格式外,支持有损压缩的pvrtc格式另外,未压缩的pvr格式的纹理的内存容量消耗非常地低不像png图片那样要消耗2倍于本身内存容量占用大小的內存容量,pvr格式只需要消耗纹理本身内存容量大小再加上一点点处理该图片格式的内存容量大小

pvr格式的一个缺点就是,你不能在Mac上面打開查看但是,如果你安装了TP的话就可以使用TP自带的pvr图片浏览器来浏览pvr格式的图片了。(强烈建议大家购买TP支持TP,不要再盗版了)

使鼡PVR格式的文件几乎没有缺点此外,它还可以极大地提高加载速度后面我会解释到。

在三种可选用的pvr文件格式中优先选择pvr.ccz格式。它是專门为cocos2d和TP设计的在TP里面,这是它生成的最小的pvr文件而且pvr.ccz格式比其它任何文件格式的加载速度。

当在cocos2d里面使用pvr格式的纹理时只使用pvr.ccz格式,不要使用其它格式!因为它加载速度超快而且加载的时候使用更少的内存容量!

当视觉察觉不出来的时候,可以考虑使用PVRTC压缩

PVR纹理支持PVRTC纹理压缩格式它主要是采用的。如果拿PVRTC图片与JPG图片作对比的话它只有JPG图片中等质量,但是最大的好处是可以不用在内存容量里媔解压缩纹理。

这里把32位的png图片(左边)与最佳质量的PVRTC4(4位)图片(点击图片查看完整的大小)作对比:

注意在一些高对比度的地方,奣显有一些瑕疵有颜色梯度的地方看起来还好一点。

PVRTC肯定不是大部分游戏想要采用的纹理格式但是,它们对于粒子效果来说非常适鼡。因为那些小的粒子在不停地移动、旋转、缩放所以你很难看出一些视觉瑕疵。

PVRTC压缩图片格式

TP提供的PVR格式不仅有上面两种还包括TC2和TC4這两种没有alpha通道的格式。

这里的alpha和16位纹理的alpha是一样的没有alpha通道意味着图片里面没有透明像素,但是更多的颜色位会用来表示颜色,那麼颜色质量看起来也会更好一些

有时候,PVRTC图片格式指的是使用4位或者2位颜色值 但是,并不完全是那样PVRTC图片格式可以编码更多的颜色徝。

就像标题所说尽你所能,一定要预先加载所有的纹理如果你的所有的纹理加起来不超过80MB内存容量消耗的话(指的是拥有Retina显示的设備,非Retina的减半考虑)你可以在第一个loading场景的时候就全部加载进来。

这样做最大的好处在于你的游戏体验会表现得非常平滑,而且你不需要再担心资源的加载和卸载问题了

这样也使得你可以让每一个纹理都使用合适的纹理像素格式,而且可以更方便地找出其它与纹理无關的内存容量问题因为如果与纹理有关,那么在第一次加载所有的纹理的时候这个问题就会暴露出来的。如果所有的纹理都加载完毕这时候再出现内存容量问题,那么肯定就与纹理无关了而是其它的问题了。

如果你知道问题与纹理无关的话那么你查找剩下的内存嫆量问题将会变得更加简单。而且你避免了前面说的这种情况:当的纹理加载的时候它本来只需要消耗16MB内存容量,但是短时间会冲到32MB内存容量后面会提出一种方法来解决“间歇性内存容量飙高”(“译者发明滴”)的方法。(译者:希望下次开发者的对话中“间歇性内存容量飙高”的说法会出现呵呵)

按照纹理size从大到小的顺序加载纹理

由于加载纹理时额外的内存容量消耗问题,所以采用按纹理size从大箌小的方式来加载纹理是一个最佳实践。

假设你有一个占内存容量16MB的纹理和四个占用内存容量4MB的纹理。如果你首先加载4MB的纹理这个程序将会使用16MB的内存容量,而当它加载第四张纹理的时候短时间内会飙到20MB。这时你要加载16MB的那个纹理了,内存容量会马上飙到48MB(4*4 + 16*2)然後再降到32MB(4*4 + 16)。

但是反过来,你先加载16MB的纹理然后短时候内飙到32MB。然后又降到16MB这时候,你再依次加载剩下的4个4MB的这时,最多会彪箌(4*3 + 4*2 + 16=36)MB

在这两种情况下,内存容量的峰值使用相差12MB要知道,可能就是这12MB会断送你的游戏进程的小命哦!

避免在收到内存容量警告消息嘚时候清除缓存

我有时候看到了一种奇怪的“自己开枪打自己的脚”的行为:纹理已经全部在Loading场景里面加载完毕了这时候,内存容量警告发生了然后cocos2d就会把没有使用的纹理从缓存中释放掉。

听起来不错没有使用到的纹理都被释放掉了,但是!。

你刚刚把所有的纹悝都加载进来,还没有进入任何一个场景中(此时所有的纹理都被当作“unused”)但是马上被全部从texture cache中移除出去。可是你又需要在其它场景中使用它们。怎么办你需要接着判断,如果有纹理没有加载就继续加载。但是一加载,由于“间歇性内存容量飙高”又马上收箌了内存容量警告,再释放再判断,再加载。。 我的天这是一个死循环啊!这也能解释为什么有些童鞋,在loading场景完了之后进入下┅个场景 的时候很卡的原因了

现在,当我收到内存容量警告的时候我的做法是----什么也不做。内存容量警告仍然在发生但是,它只是茬程序刚开始加载的时候我知道这是为什么,因为“间歇性内存容量飙高”嘛所以,我不去管它(但是,如果是游戏过程中再收到內存容量警告你就要注意了,因为这时候可能你有内存容量泄漏了!!!)

我有时候会想办法改善一下通过移除掉一些不使用的纹理囷一些只有在很特殊的场景才会使用的图片(比如settings界面,玩家是不经常访问的)然后,不管什么时候当我需要某张图片的时候,我会艏先检查一下该sprite frame是否在cache中如果没有就加载。你会在后面看到具体的做法

理解在什么时候、在哪里去清除缓存

不要随机清除缓存,也可鉯心想着释放一些内存容量而去移除没有使用的纹理那不是好的代码设计。有时候它甚至会增加加载次数,并多次引发“间歇内存容量飙高”分析你的程序的内存容量使用,看看内存容量里面到底有什么以及什么应该被清除,然后只清除该清除的

你可以使用方法來观察哪些纹理被缓存了:

这个方法的输出如下:(为了清楚起见,我把那些与-hd后缀有关的信息屏蔽掉了) 

 上面包含了非常多有用的信息纹理的大小、颜色深度(bpp)和每一个被缓存的纹理在内存容量中所占用大小等。这里的“rc”代表纹理的“引用计数”如果这个引用计數等于1或2的话,那么意味着这个纹理当前可能不会需要使用了,此时你可以放心地把它从纹理cache中移除出去。

你只移除你知道在当前场景下不太可能会被使用的纹理(即上面介绍的引用计数为1或2的情况)这是一个明智的做法。另外只移除那些占用内存容量大的纹理。洳果一个纹理只占几个kb的内存容量其它移不移除都没什么太大的影响。(译注:这就和程序优化一样不要做过多的细节优化,不要过早优化要找到性能的瓶颈,然后再重点优化以20%的时间换取80%的效率。过早和过多细节优化对于大多数程序而言是需要极力避免的)。

仩面提到的例子中纹理的引用计数可能有点让人看不懂。你会发现纹理集有很高的retain count,即使你知道这些纹理集中的纹理当前并没有被使鼡

你可能忽略了一件事:CCSprteFrame会retain它的纹理。因此如果你使用了纹理集,你要完全移除它不是那么容易因为,由这个纹理集产生的sprite frame还是保留在内存容量中所以,你必须调用CCSpriteFrameCache的方法能彻底清除纹理缓存中的纹理集。(译注:记住不是你调用对象的release方法了,对象的内存容量就会被释放掉而是引用计数为0了,内存容量才会被删除)

这样看起来有点无知(innocent):

但是要注意,CCSpriteFrameCache并不会去检查一个精灵帧是否已经被缓存起来了!这与CCTextureCache的动作方式有所不同它每次都会去加载spriteframes.

这个过程到底需要耗费多少时间呢,这取决于你提供的.plist文件中精灵帧的数量我注意到,只有14帧的plist加载与有280帧的plist加载有着很大的区别所以,对于精灵帧的加载你也需要谨慎。

所以你要避免一些不必要的addSpriteFrames*方法調用。因为那边导致场景切换时产生小的卡顿

你可以清除任何缓存(比如animation,sprite frames等),但是请不要轻易清除纹理缓存

cocos2d有许多缓存类比如纹理緩存、精灵帧缓存,动画缓存等但是,如果你想清理内存容量的话精灵帧缓存和动画缓存对内存容量的占有是非常少的,可以说是极尐的

当然,如果你想从内存容量中移除一个纹理你也必须移除与之相关的精灵帧(因为精灵帧会retain纹理)。说白了不要轻易去移除精灵幀和动画缓存,因为你有可能会使用到一个没有缓存的动画帧对象或者精灵帧对象那样会导致程序crash。

例外:检查声音文件的内存容量使鼡!

声音文件会被缓存起来然后可以重复播放而不会被中断。由于声音文件一般比较大特别是,我看到有一些开发者使用没有压缩的聲音文件作为游戏的背景音乐而这些背景音乐文件非常大,它们通常会造成大量的内存容量消耗

请使用MP3格式的声音文件。因为使用没囿压缩的声音文件既浪费内存容量又占用程序大小当你加载完一些游戏音效时,在不需要的时候记得要卸载掉。在第二篇文章中我會向大家介绍有于声音文件更多的知识。

如何避免缓存特定的纹理

如果你有一个纹理你确实不想缓存起来,那怎么办呢比如,在初始嘚加载场景中的图片或者那些用户很少会在意的图片--比如你的非常牛比的致谢场景的图片。

经常容易被误解的一点是一个纹理显示出來了,那么它就被缓存起来了如果你从缓存中移除此纹理,那么此时你再移除精灵就会程序崩溃这个理解不正确。

CCTextureCache只不过是对纹理再添加了一次retain函数的调用这样,当没有其它对象(比如sprite)持有纹理的引用的时候纹理仍然会存在内存容量之间。基于这一点我们可以竝马从缓存中移除出去,这样当纹理不存需要的时候,马上就会从内存容量中释放掉如下代码所示:

 你需要记住,当你从CCTextureCache中移除一个紋理的时候cocos2d下一次在调用spriteWithFile的时候,还是会再加载该纹理的--不管是否有没有一张名字一样的图片正在被其它精灵所使用因此,如果你不夠细心的话你有可能最后会在内存容量中加载两张重复的纹理。

有一个例子就是当你在循环中加载纹理,而这些纹理你并不想缓存起來这种情况下,你就需要在循环之外去移除此纹理的缓存否则可能会导致多个纹理被重复加载到内存容量之中:

 上面这个例子是我从highscore場景中抠出来的,一旦此场景退出就不应该持有CCLabelAtlas纹理的引用。因此我们需要把它从纹理缓存中移除出去。但是你必须防止重复加载紋理到内存容量中去。

通过这种方式我们可以非常方便地清除缓存中的纹理,而且最好是在创建纹理的时候清除而不要在其它地方,仳如dealloc或者索性让purge cache去做这个事

如果你不能预先加载所有的纹理的话,你可以使用一个loading场景同时显示一个动画来表明加载的进度。这样可鉯在进入下一个场景之前让前面一个场景销毁,同时释放它所占用的内存容量资源

 实现起来非常简单。这个loading场景调度一个selector然后每一幀(或者0.1秒也可以)执行一个函数,比如update除非你前面一个场景有内存容量泄漏,否则的话每一次update函数执行的时候,都会把一些引用计數为0的内存容量资源释放掉在这个update方法里面,你可以创建新的场景

这样极大地避免了“间歇性内存容量飙高”的问题,可以极大地减尛内存容量压力

CCTextureCache类还支持异步加载资源的功能,利用方法你可以很方面地给addImageAsync方法添加一个回调方法,这样当纹理异步加载结束的时候,可以得到通知

这一点非常重要:你必须等待一个资源加载完毕。否则的话由于“间歇性内存容量飙高”,可能会引发下列问题:

2) 紋理被加载两次!因为异步加载并不能保证加载顺序

在后台加载其它游戏资源

可是,我们并没有方法来异步加载sprite frames和其它资源但是,我們可以借助来实现类似的异步加载的功能:

 里面的selector方法只接收一个object参数(但是并没有使用)然后就可以在此这方法里面异步加载资源了,如下所示:

 这样做最大的好处在于你加载资源的同时,loading场景还可以播放动画可以添加精灵并运行一些action,这一切可以处理得很平滑這种优势甚至在单个CPU的机器上面也表现得不错,但是如果你的设备有多个cpu的话效果更佳

但是,你需要注意你不能在后台线程加载纹理,你必须使用addImageAsync方法这是因为纹理必须与公共的OpenGL context在相同的线程中加载。这样你就必须先异步加载纹理,然后再去后台加载sprite frames.你不能依靠CCSpriteFrameCache在後台线程中加载纹理

下面的代码,是我采用的异步加载纹理和精灵帧的方法(在另外一个线程中加载:)

 当这个方法运行到第一个case语句嘚时候为了避免同样的图片被加载多次,我们把loadingAsset标记设置为yes当纹理加载完后,我们就添加increaseAssetLoadCount(这个数量可以用来显示进度条加载百分比)后面的case语句还可以加载更多的其它东西,比如声音、字体文件、粒子效果、物理配置文件、关卡信息等不管加载多少东西,最后的default語句会执行然后就可以进入MainMenuScene了。

这个方法的通用之处是你可以通过case与assetLoadCount来异步加载多个纹理,同时又能避免“间歇性内存容量飙高”的問题因为每帧调用一次方法的时候,前面纹理加载多出来的临时内存容量已经被释放掉了因为当前线程栈顶的autoRelease pool会在每一帧渲染之前被清空。

把纹理的颜色位深度减少到16位不仅可以减少内存容量压力,还可以有效地减少程序的体积但是,我们还有其它方法可以更进一步地减少程序的大小

如果你有某些原因,让你坚持要使用PNG文件格式而不是我之前极力向你推荐的pvr.ccz文件格式那么TexturePacker有一个选项,叫做“Png Opt Level”(Png優化级别)可以帮助我们减少png文件的大小(注意:这样并不会影响图片加载时间)

就我目前的理解来看,最大的优化级别可以生成最小的攵件大小但是,它有一个缺点就是非常耗时。对于2009年出的27寸的iMac来说处理尺寸稍大的纹理,需要耗费10-20的时间来处理由于该优化过程采用了多线程的方式,所以如果你有机器是四核的,那么速度应该会快一些

当然,你只有在真正发布应用的时候才需要利用这个优化特性现在的问题是,它到底可以减少多少文件体积呢

我最大的一张png图片从2.4MB减少到了2.2MB.小一些的纹理从180kb减至130kb。可能单个文件减少的量并不昰很多可是当你的png图片的总大小有18MB时,它可以使之减少至16MB

注意,在xcode里面有一项设置你可能会把它忽略掉。你需要关闭"Compress PNG files"开关因为这個选项有可能会使你的png图片膨胀。你可以在xcode的build settings里面设置如下所示:

如果激活此png压缩选项,xcode会在png文件打包进程序的时候运行自带的png优化程序所以,有可能会使我们先前使用TP优化过的png图片再次膨胀因此,再次确保这个选项已关闭!

不过即使你没有禁用此选项你的程序大尛还是会有所减小。因为你有可能使用一些没有被TP优化过的png图片。

检查你的程序在App Store 里面的大小

在开发游戏的过程中你会经常添加、移除和替换游戏资源。所以你可能会因为某些原因,忘记移除一些不用的图片资源所以,你需要额外注意把它们都从项目中移除出去臸少要从程序的target中出去。

尤其是你使用多个target的时候(比如你同时维护ipad和mac版本),你就极有可能会在一个target里面添加一些错误的资源

当然,在移除资源之后你一定要充分测试你的游戏。切记!一定要充分测试

有时候,我们也会忽视这个问题如果你不考虑声音文件的格式,不管是就内存容量的使用还是程序的大小而言都是一种极大的浪费。下面是一些方法可以用来减少声音文件的大小我推荐大家使鼡一款免费的声音。

立体声道变单声道 – 你的mp3文件可以采用立体声但是,这样做值得吗如果你听不出来差别的话,建议还是采用单一聲道这样可以把文件大小和内存容量使用都减少一半。

MP3 比特率 –在iOS设备上面任何比特率大于192kbps的声音都是浪费。你可以尽量采用低的比特率来获得最好的音质效果这是一个折中。一般来说96到128kbps对于mp3文件来说够用了。

采样率 – 大部分的声音文件使用1122,44或者48kHz采样率。采樣率越低声音文件越小。但是这样声音质量也会越低。44kHz已经达到了CD的音质了而48kHz会更好(这个差别只有调音师才可以听出来)

在大部汾情况下,44kHz或者更高的比特率都有点浪费所以,可以尝试下减小采样率(在Audacity里面:Tarck->Resample)不要只是修改采样率,因为这样会改变声音文件嘚音高

mp3文件的播放,首先是加载到内存容量中然后解码为未压缩的声音buffer,最后再播放

就我目前所知,CocosDenshion的SimpleAudioEngine的playBackgoundMusic是流式播放mp3文件的流试處理有两个优点:1.更小的内存容量足迹。2.解码mp3文件采用ios硬件而不是cpu。但是硬件一次只能解码一个文件,如果同时播放多个那么只有┅个采用的是硬件解码,其它的都是软件解码

许多开发者没有注意到,tilemap大小太大会消耗大量内存容量假设你有一个的tilemap,这个大概要消耗1M的内存容量--如果每一个tile消耗一个字节的内存容量的话然而,如果每一个tile大概消耗64个字节的话那么这个tilemap就会消耗60MB内存容量。我的天啊!

除了写一个更优的tilemap渲染器以外我们唯一可以做的就是减少tilemap的大小了,也可以把地图一分为二

在mac环境下功能会存在问题linux下可鉯使用:但是for i in range(10000)的值必须是10000或者更大的数值才有用。没有搞清楚为什么

   引起内存容量溢出的原因有很多種常见的有以下几种:
  1.内存容量中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2.集合类中有对对象的引用使用完後未清空,使得JVM不能回收;
  3.代码中存在死循环或循环产生过多重复的对象实体;
  4.使用的第三方软件中的BUG;
  5.启动参数内存容量徝设定的过小;

  1.检查对数据库查询中是否有一次获得全部数据的查询。一般来说如果一次取十万条记录到内存容量,就可能引起內存容量溢出这个问题比较隐蔽,在上线前数据库中数据较少,不容易出问题上线后,数据库中数据多了一次查询就有可能引起內存容量溢出。因此对于数据库查询尽量采用分页的方式查询
  2.检查代码中是否有死循环或递归调用。 

  4.检查对数据库查询中是否有一次获得全部数据的查询。一般来说如果一次取十万条记录到内存容量,就可能引起内存容量溢出这个问题比较隐蔽,在上线前数据库中   数据较少,不容易出问题上线后,数据库中数据多了一次查询就有可能引起内存容量溢出。因此对于数据库查询尽量采用汾页的方式查询 

  作为有个java程序员,我想大家对下面出现的这几个场景并不陌生倍感亲切,深恶痛绝抓心挠肝,一定会回过头来問为什么为什么为什么会这样嘿嘿,让我们看一下我们日常在开发过程中接触内存容量溢出的异常:  

  是不是有大家很熟悉的遇见这样的问题解决起来可能不简单,但是如果现在让大家写个程序故意让程序出现下面的异常,估计能很快写出来的也不是很多这僦要求开发人员对于java内存容量区域以及jvm规范有比较深的了解。

  既然抛出了异常首先我们肯定这些都是内存容量异常,只是内存容量異常中的不同种类我们就试着了解一下为什么会出现以上的异常,可以看出有两种异常状况::

  其中OutOfMemoryError是在程序无法申请到足够的内存容量的时候抛出的异常StackOverflowError是线程申请的栈深度大于虚拟机所允许的深度所抛出的异常。 可是从上面列出的异常内容也可以看出在OutOfMemoryError类型的┅场中也存在这很多异常的可能这是为什么?以为是在内存容量的不同结构中出现的错误所以抛出的异常也就形形色色,说道这我们鈈得不介绍一下java的内存容量结构请看下图(从网上摘的):

Regster(程序计数器)。从图中看出方法区和堆用黄色标记和其他三个区域的不同点僦是,方法区和堆是线程共享的所有的运行在jvm上的程序都能访问这两个区域,堆方法区和虚拟机的生命周期一样,随着虚拟机的启动洏存在而栈和程序计数器是依赖用户线程的启动和结束而建立和销毁。

  Program Counter Regster(程序计数器):每一个用户线程对应一个程序计数器用來指示当前线程所执行字节码的行号。由程序计数器给文字码解释器提供吓一条要执行的字节码的的位置根据jvm规范,在这个区域中不会拋出OutOfMemoryError的内存容量异常

虚拟机栈):这个区域是最容易出现内存容量异常的区域,每一个线程对应生成一个线程栈线程每执行一个方法嘚时候,都会创建一个栈帧用来存放方法的局部变量表,操作树栈动态连接,方法入口这和C#是不一样的,在C#CLR中没有栈帧的概念都昰在线程栈中通过压栈和出栈的方式进行数据的保存。jvm规范对这个区域定义了两种内存容量异常OutOfMemoryError,StackOverflowError

  Native MethodStack(本地方法栈):和虚拟机栈┅样,不同的是处理的对象不一样虚拟机栈处理java的字节码,而本地栈则是处理的Native方法其他方面一致。

  Heap(堆):前面说了堆是所有线程嘟能访问的随着虚拟机的启动而存在,这块区域很大因为所有的线程都在这个区域保存实例化的对象,因为每一个类型中每个接口實现类需要的内存容量不一样,一个方法内的多个分支需要的内存容量也不尽相同我们只有在运行的时候才能知道要创建多少对象,需偠分配多大的地址空间GC关注的正是这样的部分内容,所以很多时候也将堆称为GC堆堆中肯定不会抛出StackOverflowError类型的异常,所以只有OutOfMemoryError相关类型的異常

  Method Area(方法区):用于存放已被虚拟机加载的类信息,常量静态方法,即使编译后的代码同样只能抛出OutOfMemoryError相关类型的异常。

  介绍完jvm内存容量结构中的常见区域下面该是和我们主题呼应的时候了,在什么情况下在那个区域,如何才能复现开始提到的异常信息从第一个开始,异常信息的内容为:  

  可想而知是在堆中出现的问题如何重现,由于是在堆中出现这个异常那么就要处理好,不能被垃圾回收器给回收了设置一下jvm中堆的最大值(这样才能够更快的出现错误),设置jvm值的方法是通过-Xms(堆的最小值)-Xmx(堆的最大值)。丅面动手试一下:

然后在运行的时候设置jvm参数如下:

 运行一下看看结果:  

成功在java虚拟机堆中溢出。下面看第二个关于栈的异常內容如下:  

 因为是与栈相关的话,那么我们在重现异常的时候就要相应的将栈内存容量容量设置的小一些设置栈大小的方法是设置-Xss参数,看如下实现:  

  运行时jvm参数的设置如下:

  运行结果如下:  

  第三个异常是关于perm的异常内容我们需要的是设置方法区的大小,实现方式是通过设置-XX:PermSize和-XX:MaxPermSize参数内容如下:  

  那么程序就不会执行成功,执行的时候出现如下异常:  

  第四个異常估计遇到的人就不多了是DirectMemory内存容量相关的,内容如下:  

  DirectMemoruSize可以通过设置 -XX:MaxDirectMemorySize参数指定容量大小如果不指定的话,那么就跟堆的朂大值一致,下面是代码实现:  

  运行时设置的jvm参数如下:

  很容易就复线了异常信息:  

关于JAVA中内存容量溢出的解决办法

J2ee应用系统是运行在J2EE应用服务器上的而j2ee应用服务器又是运行在JVM上的,

生成环境中JVM参数的优化和设置对于J2EE应用系统性能有着决定性的作用要优囮系统,则需要对JVM参数进行合理的设置所以我们需要了解究竟在什么地方进行设置、有哪些参数以及各参数的意义分别是什么,并且我們还得了解JVM的内存容量管理机制究竟是个什么玩意儿其实我们在网上搜索引擎上,一搜就有可以获取到一大把相关信息关键是我们如哬深入的理解它们。那么下面我们就简单的介绍一下究竟什么是JVM的内存容量管理机制吧~!

JVM的早期版本并没有进行分区管理;这样的后果是JVM進行垃圾回收时不得不扫描JVM所管理的整片内存容量,所以搜集垃圾是很耗费资源的事情也是早起JAVA程序的性能低下的主要原因。随着JVM的發展JVM引进了分区管理的机制。 

JVM所管理的所有内存容量资源分为2个大的部分永久存储区(Permanent Space) 和堆空间(The Heap Space)。其中对空间又分为新生区和養老区新生区又分为伊甸园,幸存者0区、幸存1区如下图:

关于个分区的用途,大家可以参考其他相关文档本教程所要处理的问题是洳何解决内存容量溢出的问题。接下来以tomcat服务器为例:

下面分别对各参数进行介绍和解释:

-server 启用能够执行优化的编译器, 显著提高服务器的性能但使用能够执行优化的编译器时,服务器的预备时间将会较长生产环境的服务器强烈推荐设置此参数。

-Xss 单个线程堆栈大小值;JDK5.0 以後每个线程堆栈大小为1M以前每个线程堆栈大小为256K。在相同物理内存容量下减小这个值能生成更多的线程。但是操作系统对一个进程内嘚线程数还是有限制的不能无限生成,经验值在左右

-XX:+UseParNewGC 可用来设置年轻代为并发收集【多CPU】,如果你的服务器有多个CPU你可以开启此参數;开启此参数,多个CPU 可并发进行垃圾回收可提高垃圾回收的速度。此参数和+UseParallelGC-XX:ParallelGCThreads搭配使用。

+UseParallelGC 选择垃圾收集器为并行收集器此配置仅对姩轻代有效。即上述配置下年轻代使用并发收集,而年老代仍旧使用串行收集可提高系统的吞吐量。

-XX:ParallelGCThreads 年轻代并行垃圾收集的前提下(對并发也有效果)的线程数增加并行度,即:同时多少个线程一起进行垃圾回收此值最好配置与处理器数目相等。永久存储区相关参數:参数名参数说明

-XX:PermSize 应用服务器启动时永久存储区的初始内存容量大

-XX:MaxPermSize 应用运行中,永久存储区的极限值为了不消耗扩大JVM 永久存储区分配的开销,将此参数和-XX:PermSize这个两个值设为相等堆空间相关参数参数名参数说明

-Xms 启动应用时,JVM 堆空间的初始大小值

-Xmx 应用运行中,JVM 堆空间的極限值为了不消耗扩大JVM 堆控件分配的开销,将此参数和-Xms 这个两个值设为相等考虑到需要开线程,讲此值设置为总内存容量的80%.

-Xmn 此参数硬性规定堆空间的新生代空间大小推荐设为堆空间大小的1/4。

上面所列的JVM 参数关系到系统的性能而其中-XX:PermSize,

-XX:MaxPermSize-Xms,-Xmx 和-Xmn 这5 个参数更是直接关系到系统的性能系统是否会出现内存容量溢出。

-XX:PermSize 和-XX:MaxPermSize 分别设置应用服务器启动时永久存储区的初始大小和极限大小;在生成环境中强烈推荐將这个两个值设置为相同的值,以避免分配永久存储区的开销具体的值可取系统“疲劳测试”获取到的永久存储区的极限值;如果不进荇设置-XX:MaxPermSize 默认值为64M,一般来说系统的类定义文件大小都会超过这个默认值。

-Xms 和-Xmx 分别是服务器启动时堆空间的初始大小和极限值。-Xms的默认值是粅理内存容量的1/64 但小于1G-Xmx 的默认值是物理内存容量的1/4 但小于1G.在生产环境中这些默认值是肯定不能满足我们的需要的。也就是你的服务器有8g 嘚内存容量不对JVM 参数进行设置优化,应用服务器启动时还是按默认值来分配和约束JVM 对内存容量资源的使用不会充分的利用所有的内存嫆量资源。

堆空间不足此时只需要调整-Xms 和-Xmx 这两个参数即可。

到此我们知道了当系统出现内存容量溢出时,是哪些参数设置不合理需要調整但我们怎么知道服务器启动时,到底JVM 内存容量相关参数的值是多少呢

这个问题其实Sun公司早已经意料到了,所以给我们开发了内存嫆量使用监控工具jvmstat.

大家可以到ORACLE官网进行下载用它可以很方便的看到我们的服务器内存容量使用情况。

将下载的jvmstat包解压到如“C:\ProgramFiles\Java\jvmstat”(这是我夲地java路径大家可以根据自己所安装的java环境的路径进行解压)。启动完之后我们就可以使用visualgc命令了cmd进入命令符窗口,输入tasklist(windows下查看进程任务PID)查找到你要检测进程PID.然后直接输入visuglgc PID 就会弹出三个可见视图

内存容量溢出与数据库锁表的问题,可以说是开发人员的噩梦一般的程序异常,总是可以知道在什么时候或是在什么操作步骤上出现了异常而且根据堆栈信息也很容易定位到程序中是某处出现了问题。内存容量溢出与锁表则不然一般现象是操作一般时间后系统越来越慢,直到死机但并不能明确是在什么操作上出现的,发生的时间点也沒有规律查看日志或查看数据库也不能定位出问题的代码。

更严重的是内存容量溢出与数据库锁表在系统开发和单元测试阶段并不容易被发现当系统正式上线一般时间后,操作的并发量上来了数据也积累了一些,系统就容易出现内存容量溢出或是锁表的现象而此时系统又不能随意停机或重启,为修正BUG带来很大的困难

本文以笔者开发和支持的多个项目为例,与大家分享在开发过程中遇到的Java内存容量溢出和数据库锁表的检测和处理解决过程

内存容量溢出是指应用系统中存在无法回收的内存容量或使用的内存容量过多,最终使得程序運行要用到的内存容量大于虚拟机能提供的最大内存容量为了解决Java中内存容量溢出问题,我们首先必须了解Java是如何管理内存容量的Java的內存容量管理就是对象的分配和释放问题。在Java中内存容量的分配是由程序完成的,而内存容量的释放是由垃圾收集器(Garbage CollectionGC)完成的,程序员鈈需要通过调用GC函数来释放内存容量因为不同的JVM实现者可能使用不同的算法管理GC,有的是内存容量使用到达一定程度时GC才开始工作,吔有定时执行的有的是中断式执行GC。但GC只能回收无用并且不再被其它对象引用的那些对象所占用的空间Java的内存容量垃圾回收机制是从程序的主要运行对象开始检查引用链,当遍历一遍后发现没有被引用的孤立对象就作为垃圾回收

引起内存容量溢出的原因有很多种,常見的有以下几种:

内存容量溢出虽然很棘手但也有相应的解决办法,可以按照从易到难一步步的解决。

第一步就是修改JVM启动参数,矗接增加内存容量这一点看上去似乎很简单,但很容易被忽略JVM默认可以使用的内存容量为64M,Tomcat默认可以使用的内存容量为128MB对于稍复杂┅点的系统就会不够用。在某项目中就因为启动参数使用的默认值,经常报“OutOfMemory”错误因此,-Xms-Xmx参数一定不要忘记加。

第二步检查错誤日志,查看“OutOfMemory”错误前是否有其它异常或错误在一个项目中,使用两个数据库连接其中专用于发送短信的数据库连接使用DBCP连接池管悝,用户为不将短信发出有意将数据库连接用户名改错,使得日志中有许多数据库连接异常的日志一段时间后,就出现“OutOfMemory”错误经汾析,这是由于DBCP连接池BUG引起的数据库连接不上后,没有将连接释放最终使得DBCP报“OutOfMemory”错误。经过修改正确数据库连接参数后就没有再絀现内存容量溢出的错误。

查看日志对于分析内存容量溢出是非常重要的通过仔细查看日志,分析内存容量溢出前做过哪些操作可以夶致定位有问题的模块。

第三步安排有经验的编程人员对代码进行走查和分析,找出可能发生内存容量溢出的位置重点排查以下几点:

检查对数据库查询中,是否有一次获得全部数据的查询一般来说,如果一次取十万条记录到内存容量就可能引起内存容量溢出。这個问题比较隐蔽在上线前,数据库中数据较少不容易出问题,上线后数据库中数据多了,一次查询就有可能引起内存容量溢出因此对于数据库查询尽量采用分页的方式查询。

第四步使用内存容量查看工具动态查看内存容量使用情况。某个项目上线后每次系统启動两天后,就会出现内存容量溢出的错误这种情况一般是代码中出现了缓慢的内存容量泄漏,用上面三个步骤解决不了这就需要使用內存容量查看工具了。

Profiler、JinSight和Java1.5的Jconsole等它们的基本工作原理大同小异,都是监测Java程序运行时所有对象的申请、释放等动作将内存容量管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存容量泄漏问题一般来说,一个正常的系统在其启动唍成后其内存容量的占用量是基本稳定的而不应该是无限制的增长的。持续地观察系统运行时使用的内存容量的大小可以看到在内存嫆量使用监控窗口中是基本规则的锯齿形的图线,如果内存容量的大小持续地增长则说明系统存在内存容量泄漏问题。通过间隔一段时間取一次内存容量快照然后对内存容量快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏

通过以上四個步骤的分析与处理,基本能处理内存容量溢出的问题当然,在这些过程中也需要相当的经验与敏感度需要在实际的开发与调试过程Φ不断积累。

总体上来说产生内存容量溢出是由于代码写的不好造成的,因此提高代码的质量是最根本的解决办法有的人认为先把功能实现,有BUG时再在测试阶段进行修正这种想法是错误的。正如一件产品的质量是在生产制造的过程中决定的而不是质量检测时决定的,软件的质量在设计与编码阶段就已经决定了测试只是对软件质量的一个验证,因为测试不可能找出软件中所有的BUG

1.数据量过于庞大;迉循环 ;静态变量和静态方法过多;递归;无法确定是否被引用的对象;

2.虚拟机不回收内存容量(内存容量泄漏);

    说白了就是程序运行偠用到的内存容量大于虚拟机能提供的最大内存容量就发生内存容量溢出了。 内存容量溢出的问题要看业务和系统大小而定对于某些系統可能内存容量溢出不常见,但某些系统还是很常见的解决的方法

一个是优化程序代码,如果业务庞大逻辑复杂,尽量减少全局变量嘚引用让程序使用完变量的时候释放该引用能够让垃圾回收器回收,释放资源

JVM 管理两种类型的内存容量,堆和非堆堆是给开发人员鼡的上面说的就是,是在 JVM 启动时创建;非堆是留给 JVM 自己用的用来存放类的信息的。它和堆不同运行期内 GC 不会释放空间。如果 web app 用了大量嘚第三方 jar 或者应用有太多的 class 文件而恰好 MaxPermSize 设置较小超出了也会导致这块内存容量的占用过多造成溢出,或者 tomcat 热部署时侯不会清理前面加载嘚环境只会将 context 更改为新部署的,非堆存的内容就会越来越多

第一种情况是个补充,主要存在问题就是出现在这个情况中其默认空间 ( 即 -Xms) 是物理内存容量的 1/64 ,最大空间 (-Xmx) 是物理内存容量的 1/4 如果内存容量剩余不到 40 %, JVM 就会增大堆到 Xmx 设置的值内存容量剩余超过 70 %, JVM 就会减小堆到 Xms 设置的值所以服务器的 Xmx 和 Xms 设置一般应该设置相同避免每次 GC 后都要调整虚拟机堆的大小。假设物理内存容量无限大那么 JVM 内存容量的朂大值跟操作系统有关,一般 32 位机是 1.5g 到 3g 之间而 64 位的就不会有限制了。

注意:如果 Xms 超过了 Xmx 值或者堆最大值和非堆最大值的总和超过了物悝内存容量或者操作系统的最大限制都会引起服务器启动不起来。

垃圾回收 GC 的角色

JVM 调用 GC 的频度还是很高的主要两种情况下进行垃圾回收:

当应用程序线程空闲;另一个是 java 内存容量堆不足时,会不断调用 GC 若连续回收都解决不了内存容量堆不足的问题时,就会报 out of memory 错误因为這个异常根据系统运行环境决定,所以无法预期它何时出现

根据 GC 的机制,程序的运行会引起系统运行环境的变化增加 GC 的触发机会。

为叻避免这些问题程序的设计和编写就应避免垃圾对象的内存容量占用和 GC 的开销。显示调用 System.GC() 只能建议 JVM 需要在内存容量中对垃圾对象进行回收但不是必须马上回收,

一个是并不能解决内存容量资源耗空的局面另外也会增加 GC 的消耗。

简单的说 java中的堆和栈

java把内存容量分两种:┅种是栈内存容量另一种是堆内存容量

1。在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存容量中分配;

2堆内存容量鼡来存放由 new创建的对象和数组

在函数(代码块)中定义一个变量时, java就在栈中为这个变量分配内存容量空间当超过变量的作用域后, java会洎动释放掉为该变量所分配的内存容量空间;在堆中分配的内存容量由 java虚拟机的自动垃圾回收器来管理

堆的优势是可以动态分配内存容量夶小生存期也不必事先告诉编译器,因为它是在运行时动态分配内存容量的缺点就是要在运行时动态分配内存容量,存取速度较慢;

棧的优势是存取速度比堆要快缺点是存在栈中的数据大小与生存期必须是确定的无灵活 性。

新创建的对象被分配到 New 区当该区被填满时會被 GC 辅助线程移到 Old 区,当 Old 区也填满了会触发 GC 主线程遍历堆内存容量里的所有对象 Old 区的大小等于 Xmx 减去 -Xmn

每个线程都有他自己的 Stack

三、 JVM如何设置虛拟内存容量 提示:在 JVM中如果 98%的时间是用于 GC且可用的 Heap size 不足 2%的时候将抛出此异常信息。

提示: JVM初始分配的内存容量由 -Xms指定默认是物理內存容量的 1/64; JVM最大分配的内存容量由 -Xmx指定,默认是物理内存容量的 1/4

默认空余堆内存容量小于 40%时, JVM就会增大堆直到 -Xmx的最大限制;空余堆内存容量大于 70%时 JVM会减少堆直到 -Xms的最小限制。因此服务器一般设置 -Xms、 -Xmx相等以避免在每次 GC 后调整堆的大小

提示:假设物理内存容量无限大的話, JVM内存容量的最大值跟操作系统有很大的关系

简单的说就 32位处理器虽然可控内存容量空间有 4GB,但是具体的操作系统会给一个限制,

提示:注意:如果 Xms超过了 Xmx值或者堆最大值和非堆最大值的总和超过了物理内 存或者操作系统的最大限制都会引起服务器启动不起来。

提示:設置 NewSize、 MaxNewSize相等 “new”的大小最好不要大于 “old”的一半,原因是 old区如果不够大会频繁的触发 “主 ” GC 大大降低了性能

由 XX:MaxPermSize设置最大非堆内存容量嘚大小,默认是物理内存容量的 1/4

四、性能检查工具使用 

JProfiler 工具主要用于检查和跟踪系统(限于 Java 开发的)的性能。 JProfiler 可以通过时时的监控系统嘚内存容量使用情况随时监视垃圾回收,线程运行状况等手段从而很好的监视 JVM 运行情况及其性能。


1. 应用服务器内存容量长期不合理占鼡内存容量经常处于高位占用,很难回收到低位;

2. 应用服务器极为不稳定几乎每两天重新启动一次,有时甚至每天重新启动一次;

3. 应鼡服务器经常做 Full GC(Garbage Collection)而且时间很长,大约需要 30-40秒应用服务器在做 Full GC的时候是不响应客户的交易请求的,非常影响系统性能

因为开发环境和產品环境会有不同,导致该问题发生有时会在产品环境中发生 通常可以使用工具跟踪系统的内存容量使用情况,在有些个别情况下或许某个时刻确实 是使用了大量内存容量导致 out of memory这时应继续跟踪看接下来是否会有下降,

如果一直居高不下这肯定就因为程序的原因导致内存嫆量泄漏

五、不健壮代码的特征及解决办法 
1 、尽早释放无用对象的引用。好的办法是使用临时变量的时候让引用变量在退出活动域后,自动设置为 null 暗示垃圾收集器来收集该对象,防止发生内存容量泄露

对于仍然有指针指向的实例, jvm 就不会回收该资源 , 因为垃圾回收会將值为 null 的对象作为垃圾提高 GC 回收机制效率;

2 、我们的程序里不可避免大量使用字符串处理,避免使用 String 应大量使用 StringBuffer ,每一个 String 对象都得独竝占用内存容量一块区域;

3 、尽量少用静态变量因为静态变量是全局的, GC 不会回收的;

4 、避免集中创建对象尤其是大对象 JVM 会突然需要夶量内存容量,这时必然会触发 GC 优化系统内存容量环境;显示的声明数组空间而且申请数量还极大。

这是一个案例想定供大家警戒:

使用jspsmartUpload莋文件上传,现在运行过程中经常出现java.outofMemoryError的错误用top命令看看进程使用情况,发现内存容量不足2M花了很长时间,发现是jspsmartupload的问题把jspsmartupload组件的源碼文件(class文件)反编译成Java文件,如梦方醒:

变量m_totalBytes表示用户上传的文件的总长度这是一个很大的数。如果用这样大的数去声明一个byte数组並给数组的每个元素分配内存容量空间,而且m_binArray数组不能马上被释放JVM的垃圾回收确实有问题,导致的结果就是内存容量溢出

jspsmartUpload为什末要这樣作,有他的原因根据RFC1867的http上传标准,得到一个文件流并不知道文件流的长度。设计者如果想文件的长度只有操作servletinputstream一次才知道,因为任何流都不知道大小只有知道文件长度了,才可以限制用户上传文件的长度为了省去这个麻烦,jspsmartUpload设计者直接在内存容量中打开文件判断长度是否符合标准,符合就写到服务器的硬盘这样产生内存容量溢出,这只是我的一个猜测而已

所以编程的时候,不要在内存容量中申请大的空间因为web服务器的内存容量有限,并且尽可能的使用流操作例如

5 、尽量运用对象池技术以提高系统性能;生命周期长的對象拥有生命周期短的对象时容易引发内存容量泄漏,例如大集合对象拥有大数据量的业务对象的时候可以考虑分块进行处理,然后解決一块释放一块的策略

6 、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象可以适当的使用 hashtable , vector 创建一组对象容器然後从容器中去取那些对象,而不用每次 new 之后又丢弃

7 、一般都是发生在开启大型文件或跟数据库一次拿了太多的数据造成 Out Of Memory Error 的状况,这时就夶概要计算一下数据量的最大值是多少并且设定所需最小及最大的内存容量空间值。

我要回帖

更多关于 内存容量 的文章

 

随机推荐