oom

Android的虚拟机是基于寄存器的Dalvik,它的最大堆大小一般是16M,有的机器为24M。我们平常看到的OutOfMemory的错误,通常是堆内存溢出。移动开发和web开发的最大的区别是设备资源受限,对一般手机应用,这个资源是相当有限的,堆内存的上限值只有16M。Android的缺省值是16M(某些机型是24M),而对于普通应用这是不能改的,当应用程序处理大资源的资源,如图片或视频等媒体资源时 ,数量一多,时间一长,这个16M是很容易耗尽的,OOM是很容易出现的。
   *Android内存泄露*
   虽然JAVA有垃圾回收机制,但也存在内存泄露。如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成了内存泄露。如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了。当然java的,内存泄漏和C/C++是不一样的。如果java程序完全结束后,它所有的对象就都不可达了,系统就可以对他们进行垃圾回收,它的内存泄露仅仅限于它本身,而不会影响整个系统的。C/C++的内存泄露就比较糟糕了,它的内存泄露是系统级,即使该C/C++程序退出,它的泄露的内存也无法被系统回收,永远不可用了,除非重启机器。
  Android的一个应用程序的内存泄露对别的应用程序影响不大。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启),这是,我们的应用程序就会崩溃,我们就会看到OOM。
一般而言,android中常见的原因主要有以下几个:
1.数据库的cursor没有关闭。
2.构造adapter没有使用缓存contentview。
3.调用registerReceiver()后未调用unregisterReceiver().
4.未关闭InputStream/OutputStream。
5.Bitmap使用后未调用recycle()。
6.Context泄漏。
7.static关键字等。
下面我们就来逐一说明这些吧:
*1、首先,我们先来说明static,这个是万恶之源*
   static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。   不少程序员喜欢用static这个关键字修饰变量,因为他使得变量的生命周期大大延长啦,并且访问的时候,也极其的方便,用类名就能直接访问,各个资源间传值也极其的方便,所以,它经常被我们使用。但如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。
1public class ClassName {  
2      private static Context mContext;  
3      //省略  
4}


以上的代码是很危险的,如果将Activity赋值到么mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放,并且,如果该activity里面再持有一些资源,那就糟糕了。
    上面是直接的引用泄露,我们再看google文档中的一个例子。
01private static Drawable sBackground;  
02 
03 
04 @Override  
05 protected void onCreate(Bundle state) {  
06   super.onCreate(state);  
07 
08 
09   TextView label = new TextView(this);  
10   label.setText("Leaks are bad");  
11 
12 
13   if (sBackground == null) {  
14     sBackground = getDrawable(R.drawable.large_bitmap);  
15   }  
16   label.setBackgroundDrawable(sBackground);  
17 
18 
19   setContentView(label);  
20 }

    sBackground, 是一个静态的变量,但是我们发现,我们并没有显式的保存Contex的引用,但是,当Drawable与View连接之后,Drawable就将View设置为一个回调,由于View中是包含Context的引用的,所以,实际上我们依然保存了Context的引用。这个引用链如下:
    Drawable->TextView->Context
所以,最终该Context也没有得到释放,也发生了内存泄露。
那我们如何的避免这种泄露的发生呢?
   第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。
   第二、Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。
   第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef;
该部分的详细内容也可以参考Android文档中Article部分。
*2、 Context泄漏*
   第一条说的static泄露中,已经概括了大部分的context泄露,出了这种static的泄露context的方式外,还有一种就是内部类持有外部对象造成的内存泄露,常见是内部线程造成的。
01public class BasicActivity extends Activity {  
02   @Override  
03   public void onCreate(Bundle savedInstanceState) {  
04       super.onCreate(savedInstanceState);  
05       setContentView(R.layout.main);  
06       new MyThread().start();  
07   }  
08 
09 
10   private class OneThread extends Thread{  
11       @Override  
12       public void run() {  
13           super.run();  
14           //do somthing  
15       }  
16   }  
17}  

   这段代码很平常也很简单,是我们经常使用的形式。我们思考一个问题:假设OneThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。

    由于我们的线程是Activity的内部类,所以OneThread中保存了Activity的一个引用,当OneThread的run函数没有结束时,OneThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。
   有些人喜欢用Android提供的AsyncTask,但事实上AsyncTask的问题更加严重,Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了ThreadPoolExcutor,该类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题,故一般不建议将AsyncTask作为内部类使用。
   那么上述内存泄露问题应该如何解决呢?
   第一、将线程的内部类,改为静态内部类。并且注意第二条。
   第二、在线程内部采用弱引用保存Context引用。
*3、bitmap内存泄露*
   可以说出现OutOfMemory问题的绝大多数人,都是因为Bitmap的问题。因为Bitmap占用的内存实在是太多了,它是一个“超级大胖子”,特别是分辨率大的图片,如果要显示多张那问题就更显著了。
   如何解决Bitmap带给我们的内存问题?
   第一、及时的销毁。
虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”,  还有就是, 虽然recycle()从源码上看,调用它应该能立即释放Bitmap的主要内存,但是测试表明它并没能立即释放内存。故我们还需手动设置为NULL这样还能大大的加速Bitmap的主要内存的释放。。
如下:

1if(!bitmap.isRecycled()){
2          bitmap.recycle()
3}
       

   第二、设置一定的采样率。
   有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:
01    private ImageView preview;  
02    BitmapFactory.Options options = new BitmapFactory.Options();  
03    options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一  
04    Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);  
05    preview.setImageBitmap(bitmap);  
06    第三、巧妙的运用软引用(SoftRefrence)
07    有些时候,我们使用Bitmap后没有保留对它的引用,因此就无法调用Recycle函数。这时候巧妙的运用软引用,可以使Bitmap在内存快不足时得到有效的释放。如下例:
08private class MyAdapter extends BaseAdapter {  
09    
10    private ArrayList> mBitmapRefs = new ArrayList>();  
11    public View getView(int i, View view, ViewGroup viewGroup) {  
12        View newView = null;  
13        if(view != null) {  
14            newView = view;  
15        } else {  
16            newView =(View)mInflater.inflate(R.layout.image_view, false);  
17        }  
18    
19        Bitmap bitmap = BitmapFactory.decodeFile(mValues.get(i).fileName);  
20        mBitmapRefs.add(new SoftReference(bitmap));     //此处加入ArrayList  
21        ((ImageView)newView).setImageBitmap(bitmap);  
22    
23        return newView;  
24    }  
25}

   开源社区上有一个SoftHashMap工具类,就很好的采用了这种思想,所有,我们可以采用该容器来保存这些大内存资源。
*4.未关闭InputStream/OutputStream* 
   这个就不多说了,我们操作完输入输出流都要关闭流
*5、调用registerReceiver()后未调用unregisterReceiver().*
    广播接收者(BroadcastReceiver)经常在应用中用到,可以在多线程任务完成后发送广播通知UI更新,也可以接收系统广播实现一些功能 
    可以通过代码的方式注册: 
1IntentFilter postFilter = new IntentFilter(); 
2postFilter.addAction(getPackageName() + ".background.job"); 
3this.registerReceiver(receiver, postFilter);


    当我们Activity中使用了registerReceiver()方法注册了BroadcastReceiver,一定要在Activity的生命周期内调用unregisterReceiver()方法取消注册 
    也就是说registerReceiver()和unregisterReceiver()方法一定要成对出现,通常我们可以重写Activity的onDestory().
*6、 构造adapter没有使用缓存contentview*
   当一个listview的子项有成千上万个时,如果我们没有采用一定的策略来重用这些资源,那应用的那点对内存,是远远不够使用的。
   在继承BaseAdapter时会让我们重写getView(int position, View   convertView, ViewGroup parent)方法, 
   第二个参数convertView就是我们要用到的重用的对象。
   这里只讲使用方法,具体性能测试文章请见: 
    ListView中getView的原理+如何在ListView中放置多个item 
    http://www.cnblogs.com/xiaowenji/archive/2010/12/08/1900579.html 
    Android开发之ListView适配器(Adapter)优化 
    http://shinfocom.iteye.com/blog/1231511
*7、数据库的cursor没有关闭*
   Cursor是Android查询数据后得到的一个管理数据集合的类,正常情况下,如果查询得到的数据量较小时不会有内存问题,而且虚拟机能够保证Cusor最终会被释放掉。
   然而如果Cursor的数据量特表大,特别是如果里面有Blob信息时,应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。
   所以我们使用Cursor的方式一般如下:

01Cursor cursor = null;  
02 try {  
03   cursor = mContext.getContentResolver().query(uri,null, null,null,null);  
04   if(cursor != null) {  
05       cursor.moveToFirst();  
06       //do something  
07   }  
08 } catch (Exception e) {  
09   e.printStackTrace();    
10 } finally {  
11   if (cursor != null) {  
12      cursor.close();  
13   }  
14}


   有一种情况下,我们不能直接将Cursor关闭掉,这就是在CursorAdapter中应用的情况,但是注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭。
1@Override  
2protected void onDestroy() {        
3   if (mAdapter != null && mAdapter.getCurosr() != null) {  
4       mAdapter.getCursor().close();  
5   }  
6   super.onDestroy();   
7}

   CursorAdapter中的changeCursor函数,会将原来的Cursor释放掉,并替换为新的Cursor,所以你不用担心原来的Cursor没有被关闭。
   你可能会想到使用Activity的managedQuery来生成Cursor,这样Cursor就会与Acitivity的生命周期一致了,多么完美的解决方法!然而事实上managedQuery也有很大的局限性。
   managedQuery生成的Cursor必须确保不会被替换,因为可能很多程序事实上查询条件都是不确定的,因此我们经常会用新查询的Cursor来替换掉原先的Cursor。因此这种方法适用范围也是很小。
*总结*
   要减小内存的使用,其实还有很多方法和要求。比如不要使用整张整张的图,尽量使用9path图片。Adapter要使用convertView等等,好多细节都可以节省内存。
<think>我们正在讨论单机多线程环境下的OOM问题。OOM(OutOfMemory)错误是指程序在申请内存时,没有足够的内存空间供其使用,进而抛出错误。在多线程环境下,由于多个线程同时运行,可能会并发地申请内存资源,更容易导致内存耗尽。下面我将从原因和解决方案两个方面进行详细说明。###一、多线程环境下OOM的主要原因1.**线程过多导致栈内存溢出(StackOverflowError或OutOfMemoryError)**-每个线程都会为其栈分配内存(默认1MB,可通过`-Xss`调整)[^2]。当线程数过多时,总栈内存可能超过JVM限制。-**计算公式**:$\text{总栈内存}=\text{线程数}\times\text{每个线程栈大小}$-例如:默认栈大小1MB,创建1000个线程需要约1GB栈内存,若JVM总内存不足则发生OOM。2.**线程池配置不当**-**无界任务队列**:如使用`LinkedBlockingQueue`且未设置容量,任务激增时队列无限增长导致堆内存溢出[^3]。-**线程数失控**:`newCachedThreadPool()`理论上可无限创建线程(见案例):```javaExecutorServicepool=Executors.newCachedThreadPool();while(true){pool.execute(()->{try{Thread.sleep(1000000);}catch(InterruptedExceptione){}});}```最终因线程数过多触发OOM。3.**共享资源竞争引发内存泄漏**-**场景**:多线程频繁操作静态集合(如`Map`/`List`)且未正确清理。-**案例**:Web应用中将用户会话数据存入静态Map但未移除:```javapublicclassSessionManager{privatestaticMap<Long,Session>sessions=newConcurrentHashMap<>();//添加后未实现超时移除逻辑publicstaticvoidaddSession(Longid,Sessionsession){sessions.put(id,session);}}```随着时间推移,`sessions`持续增长直至OOM。4.**死锁或阻塞导致资源无法释放**-线程死锁使得部分对象无法被垃圾回收(即使逻辑上应回收)。-线程长时间阻塞(如I/O等待)导致关联对象滞留内存。###二、解决方案与实践优化####1.控制线程数量与生命周期-**限制最大线程数**:根据系统资源设置合理阈值(公式:$\text{maxThreads}=\frac{\text{总内存-堆预留}}{\text{线程栈大小}}$)。-**使用线程池替代裸线程**:通过`ThreadPoolExecutor`自定义参数:```javanewThreadPoolExecutor(corePoolSize,//核心线程数maxPoolSize,//最大线程数keepAliveTime,//空闲线程存活时间TimeUnit.MILLISECONDS,newLinkedBlockingQueue<>(1000)//有界队列);```####2.优化任务队列策略|队列类型|适用场景|防OOM措施||--------------------|-------------------------|------------------------||有界队列|可控任务量的系统|设置合理容量并配合拒绝策略||同步移交队列|高吞吐低延迟场景|需严格限制最大线程数||优先级队列|任务需分级处理|避免无限制增长|####3.内存泄漏防护-**代码规范**:-对长生命周期集合(如静态Map)使用`WeakReference`或定时清理。-避免在循环中创建大对象。-**工具检测**:-使用`jmap`生成堆转储文件,通过MAT分析对象引用链。-Arthas在线监控内存使用:`watchcom.example.Service*'{params,returnObj}'`####4.JVM参数调优-**调整栈大小**:根据线程数需求设置`-Xss256k`(减小默认栈大小)[^2]。-**合理分配堆内存**:年轻代大小推荐为堆的3/8(`-Xmn`参数)[^2]。-**示例配置**:```-Xmx4g-Xms4g//堆固定4G避免动态扩容开销-Xmn1.5g//年轻代1.5G(占3/8)-Xss256k//线程栈降为256KB```###三、典型案例分析>**案例1:日志服务异步队列OOM**>**现象**:日志服务使用无界队列接收日志,突发流量时队列堆积至2GB触发OOM。>**解决方案**:>1.将`LinkedBlockingQueue`改为有界队列(容量10000)>2.添加拒绝策略:丢弃超出容量的日志并告警>3.引入`GuavaRateLimiter`限流>**案例2:线程数失控**>**现象**:`newCachedThreadPool()`在并发连接激增时创建8000+线程,栈内存耗尽。>**优化**:改用有界线程池,参数计算:>$$\text{maxThreads}=\frac{\text{可用内存1GB}}{\text{线程栈512KB}}=2000\text{线程}$$>实际设置`maxPoolSize=500`(预留内存给堆使用)。###总结:多线程OOM防护体系```mermaidgraphTDA[预防]-->B[线程池参数优化]A-->C[有界队列+拒绝策略]A-->D[代码内存泄漏检查]E[诊断]-->F[jmap+MAT分析]E-->G[Arthas在线监控]H[治理]-->I[JVM调优]H-->J[限流降级]```>**关键原则**:>-线程是稀缺资源,需池化管理和约束上限>-内存分配需考虑线程开销(栈空间)>-多线程共享数据结构应具备自我清理能力§§相关问题§§1.如何通过JVM参数优化多线程应用的内存使用?2.哪些工具可以检测多线程环境下的内存泄漏?3.线程池的拒绝策略如何选择才能避免OOM?4.为什么说`newCachedThreadPool()`在突发流量下可能导致OOM?5.如何设计一个防OOM的异步任务处理系统?</think>### OOM问题在单机多线程环境下的原因及解决方案 #### 一、OOM的核心原因分析 ```mermaid graph TD A[多线程OOM原因] --> B[线程栈溢出] A --> C[堆内存泄漏] A --> D[元空间溢出] A --> E[直接内存溢出] ``` 1. **线程栈溢出(StackOverflowError/OOM)** - **原因**: - 线程数过多超出栈内存容量(默认1MB/线程)[^2] $$ \text{最大线程数} \approx \frac{\text{最大内存 - 堆内存}}{\text{线程栈大小}} $$ - 递归调用过深(非多线程特有但会被多线程放大) - **特征**:`java.lang.StackOverflowError`或`OutOfMemoryError: unable to create new native thread` 2. **堆内存泄漏(Heap OOM)** - **多线程特有场景**: - 静态集合(如`ConcurrentHashMap`)持续增长未清理 ```java // 危险代码示例 public static Map<Long, Object> cache = new ConcurrentHashMap<>(); void addData(Long id) { cache.put(id, new byte[1024*1024]); // 每个对象1MB } ``` - 线程池使用无界队列(如`LinkedBlockingQueue`)导致任务堆积[^3] - 死锁导致对象无法释放(两个线程互相持有对方需要的锁) 3. **元空间溢出(Metaspace OOM)** - **多线程影响**:高并发下类加载激增(如动态代理类生成) - **错误信息**:`OutOfMemoryError: Metaspace` 4. **直接内存溢出(Direct Buffer OOM)** - **场景**:多线程频繁操作`ByteBuffer.allocateDirect()` - **特征**:`OutOfMemoryError: Direct buffer memory` #### 二、解决方案与实践优化 ##### 1. 线程栈溢出应对 | 措施 | 实现方式 | 效果 | |-----------------------|-------------------------------------------------------------------------|--------------------------| | 限制最大线程数 | `ThreadPoolExecutor(maxPoolSize=50)` | 防止线程数失控[^3] | | 减小线程栈大小 | JVM参数:`-Xss256k`(默认1MB) | 同等内存支持更多线程[^2] | | 异步非阻塞模型 | 使用Netty/Vert.x替代线程池 | 1线程处理万级连接 | | 监控线程创建 | `jstack`或Arthas的`thread`命令 | 实时发现线程泄漏 | ##### 2. 堆内存泄漏治理 **代码层面:** ```java // 安全方案1:使用弱引用 Map<Long, WeakReference<Object>> cache = new ConcurrentHashMap<>(); // 安全方案2:定时清理 ScheduledExecutorService cleaner = Executors.newScheduledThreadPool(1); cleaner.scheduleAtFixedRate(() -> cache.clear(), 1, 1, TimeUnit.HOURS); ``` **配置优化:** - 线程池必须使用有界队列: ```java new ThreadPoolExecutor(..., new ArrayBlockingQueue<>(1000)); ``` - 设置合理的GC策略:`-XX:+UseG1GC -XX:MaxGCPauseMillis=200` **工具检测:** 1. `jmap -dump:format=b,file=heap.bin <pid>` 生成堆转储 2. MAT工具分析支配树(Dominator Tree),定位泄漏对象 ##### 3. 元空间与直接内存优化 | 内存区域 | 优化措施 | 参数示例 | |----------|--------------------------------------------------------------------------|------------------------------| | 元空间 | 增大元空间大小<br>关闭不需要的类加载 | `-XX:MaxMetaspaceSize=256m`<br>`-XX:+DisableAttachMechanism` | | 直接内存 | 限制直接内存用量<br>使用内存池(如Netty的PooledByteBufAllocator) | `-XX:MaxDirectMemorySize=128m` | #### 三、防御性编程实践 1. **资源约束策略** - 线程池必须设置: ```java new ThreadPoolExecutor( 10, // 核心线程数 100, // 最大线程数 60s, // 空闲超时 TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), // 有界队列 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 ); ``` 2. **内存监控接入** ```java // 示例:监控堆内存使用率 MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); if(memoryBean.getHeapMemoryUsage().getUsed() > maxHeap*0.8) { alert("内存使用超过80%!"); } ``` 3. **压力测试模型** ```mermaid graph LR A[模拟并发请求] --> B{监控指标} B --> C[JVM内存] B --> D[GC次数] B --> E[线程状态] C --> F[调整-Xmx] D --> G[优化GC参数] E --> H[调整线程池] ``` #### 四、典型案例分析 > **案例:电商系统大促OOM** > **现象**: > - 高峰期每秒5000请求 > - 线程池使用`newCachedThreadPool()`(无线程数限制) > - 2分钟内创建2000+线程触发`OOM: unable to create native thread` > > **解决方案**: > 1. 改用有界线程池:`maxPoolSize=500 + ArrayBlockingQueue(5000)` > 2. 添加拒绝策略:记录日志并返回友好错误 > 3. 增加JVM参数:`-Xss256k -XX:MaxMetaspaceSize=256m` > > **效果**: > | 指标 | 优化前 | 优化后 | > |-------------|-------------|-------------| > | 最大线程数 | 无限制 | 500 | > | 内存占用 | 8GB | 4GB | > | 错误率 | 23% | 0.5% | #### 总结:OOM防御体系 1. **资源隔离**:线程池约束 + 有界队列 2. **容量规划**:通过`(总内存 - 堆内存)/Xss`计算最大线程数[^2] 3. **监控预警**:JVM内存 + 线程状态实时监控 4. **优雅降级**:拒绝策略保核心业务(如`CallerRunsPolicy`) > **关键原则**: > - 多线程环境下,**所有资源必须设置明确上限** > - 线程不是越多越好,需满足:$ \text{线程数} \leq \frac{\text{可用内存}}{\text{线程栈}} $ > - 无界数据结构是OOM的主要诱因
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值