Java中那些内存泄漏的场景!

虽然Java程序员不用像C/C++程序员那样时刻关注内存的使用情况,JVM会帮我们处理好这些,但并不是说有了GC就可以高枕无忧,内存泄露相关的问题一般在测试的时候很难发现,一旦上线流量起来可能马上就是一个诡异的线上故障。

内存泄露定义

如果GC无法回收内存中不再使用的对象,则定义为内存有泄露。

1. 未关闭的资源类

当我们在程序中打开一个新的流或者是新建一个网络连接的时候,JVM都会为这些资源类分配内存做缓存,常见的资源类有网络连接,数据库连接以及IO流。值得注意的是,如果在业务处理中异常,则有可能导致程序不能执行关闭资源类的代码,因此最好按照下面的做法处理资源类

public void handleResource() {try {// open connection// handle business} catch (Throwable t) {// log stack} finally {// close connection}
}

2. 未正确实现equals()hashCode()

假如有下面的这个类

public class Person {public String name;public Person(String name) {this.name = name;}
}

并且如果在程序中有下面的操作

@Test
public void givenMapWhenEqualsAndHashCodeNotOverriddenThenMemoryLeak() {Map<Person, Integer> map = new HashMap<>();for(int i=0; i<100; i++) {map.put(new Person("jon"), 1);}Assert.assertFalse(map.size() == 1);
}

可以预见,这个单元测试并不能通过,原因是Person类没有实现equals方法,因此使用Objectequals方法,直接比较实体对象的地址,所以map.size() == 100

如果我们改写Person类的代码如下所示:

public class Person {public String name;public Person(String name) {this.name = name;}@Overridepublic boolean equals(Object o) {if (o == this) return true;if (!(o instanceof Person)) {return false;}Person person = (Person) o;return person.name.equals(name);}@Overridepublic int hashCode() {int result = 17;result = 31 * result + name.hashCode();return result;}
}

则上文中的单元测试就可以顺利通过了,需要注意的是这个场景比较隐蔽,一定要在平时的代码中注意。

3. 非静态内部类

要知道,所有的非静态类别类都持有外部类的引用,因此某些情况如果引用内部类可能延长外部类的生命周期,甚至持续到进程结束都不能回收外部类的空间,这类内存溢出一般在Android程序中比较多,只要MyAsyncTask处于运行状态MainActivity的内存就释放不了,很多时候安卓开发者这样做只是为了在内部类中拿到外部类的属性,殊不知,此时内存已经泄露了。

public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);new MyAsyncTask().execute();}private class MyAsyncTask extends AsyncTask {@Overrideprotected Object doInBackground(Object[] params) {return doSomeStuff();}private Object doSomeStuff() {//do something to get resultreturn new MyObject();}}
}

4. 重写了finalize()的类

如果运行下面的这个例子,则最终程序会因为OOM的原因崩溃

public class Finalizer {@Overrideprotected void finalize() throws Throwable {while (true) {Thread.yield();}}public static void main(String str[]) {while (true) {for (int i = 0; i < 100000; i++) {Finalizer force = new Finalizer();}}}
}

JVM对重写了finalize()的类的处理稍微不同,首先会针对这个类创建一个java.lang.ref.Finalizer类,并让java.lang.ref.Finalizer持有这个类的引用,在上文中的例子中,因为Finalizer类的引用被java.lang.ref.Finalizer持有,所以他的实例并不能被Young GC清理,反而会转入到老年代。在老年代中,JVM GC的时候会发现Finalizer类只被java.lang.ref.Finalizer引用,因此将其标记为可GC状态,并放入到java.lang.ref.Finalizer.ReferenceQueue这个队列中。等到所有的Finalizer类都加到队列之后,JVM会起一个后台线程去清理java.lang.ref.Finalizer.ReferenceQueue中的对象,之后这个后台线程就专门负责清理java.lang.ref.Finalizer.ReferenceQueue中的对象了。这个设计看起来是没什么问题的,但其实有个坑,那就是负责清理java.lang.ref.Finalizer.ReferenceQueue的后台线程优先级是比较低的,并且系统没有提供可以调节这个线程优先级的接口或者配置。因此当我们在使用使用重写finalize()方法的对象时,千万不要瞬间产生大量的对象,要时刻谨记,JVM对此类对象的处理有特殊逻辑。

5. 针对长字符串调用String.intern()

如果提前在src/test/resources/large.txt中写入大量字符串,并且在Java 1.6及以下的版本运行下面程序,也将得到一个OOM

@Test
public void givenLengthString_whenIntern_thenOutOfMemory()throws IOException, InterruptedException {String str = new Scanner(new File("src/test/resources/large.txt"), "UTF-8").useDelimiter("\\A").next();str.intern();System.gc(); Thread.sleep(15000);
}

原因是在Java 1.6及以下,字符串常量池是处于JVM的PermGen区的,并且在程序运行期间不会GC,因此产生了OOM。在Java 1.7以及之后字符串常量池转移到了HeapSpace此类问题也就无需再关注了

6. ThreadLocal的误用

ThreadLocal一定要列在Java内存泄露的榜首,总能在不知不觉中将内存泄露掉,一个常见的例子是:

@Test
public void testThreadLocalMemoryLeaks() {ThreadLocal<List<Integer>> localCache = new ThreadLocal<>();List<Integer> cacheInstance = new ArrayList<>(10000);localCache.set(cacheInstance);localCache = new ThreadLocal<>();
}

localCache的值被重置之后cacheInstanceThreadLocalMap中的value引用,无法被GC,但是其keyThreadLocal实例的引用是一个弱引用,本来ThreadLocal的实例被localCacheThreadLocalMapkey同时引用,但是当localCache的引用被重置之后,则ThreadLocal的实例只有ThreadLocalMapkey这样一个弱引用了,此时这个实例在GC的时候能够被清理。

img

其实看过ThreadLocal源码的同学会知道,ThreadLocal本身对于keynullEntity有自清理的过程,但是这个过程是依赖于后续对ThreadLocal的继续使用,假如上面的这段代码是处于一个秒杀场景下,会有一个瞬间的流量峰值,这个流量峰值也会将集群的内存打到高位(或者运气不好的话直接将集群内存打满导致故障),后面由于峰值流量已过,对ThreadLocal的调用也下降,会使得ThreadLocal的自清理能力下降,造成内存泄露。ThreadLocal的自清理实现是锦上添花,千万不要指望它雪中送碳。

7. 类的静态变量

Tomcat对在网络容器中使用ThreadLocal引起的内存泄露做了一个总结,详见:https://cwiki.apache.org/confluence/display/tomcat/MemoryLeakProtection,这里我们列举其中的一个例子。

熟悉Tomcat的同学知道,Tomcat中的web应用由webapp classloader这个类加载器的,并且webapp classloader是破坏双亲委派机制实现的,即所有的web应用先由webapp classloader加载,这样的好处就是可以让同一个容器中的web应用以及依赖隔离。

下面我们看具体的内存泄露的例子:

public class MyCounter {private int count = 0;public void increment() {count++;}public int getCount() {return count;}
}public class MyThreadLocal extends ThreadLocal<MyCounter> {
}public class LeakingServlet extends HttpServlet {private static MyThreadLocal myThreadLocal = new MyThreadLocal();protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {MyCounter counter = myThreadLocal.get();if (counter == null) {counter = new MyCounter();myThreadLocal.set(counter);}response.getWriter().println("The current thread served this servlet " + counter.getCount()+ " times");counter.increment();}
}

需要注意这个例子中的两个非常关键的点:

  • MyCounter以及MyThreadLocal必须放到web应用的路径中,保被webapp classloader加载

  • ThreadLocal类一定得是ThreadLocal的继承类,比如例子中的MyThreadLocal,因为ThreadLocal本来被common classloader加载,其生命周期与tomcat容器一致。ThreadLocal的继承类包括比较常见的NamedThreadLocal,注意不要踩坑。

假如LeakingServlet所在的web应用启动,MyThreadLocal类也会被webapp classloader加载,如果此时web应用下线,而线程的生命周期未结束(比如为LeakingServlet提供服务的线程是一个线程池中的线程),那会导致myThreadLocal的实例仍然被这个线程引用,而不能被GC,期初看来这个带来的问题也不大,因为myThreadLocal所引用的对象占用的内存空间不太多,问题在于myThreadLocal间接持有加载web应用的webapp classloader的引用(通过myThreadLocal.getClass().getClassLoader()可以引用到),而加载web应用的webapp classloader有持有它加载的所有类的引用,这就引起了classloader泄露,它泄露的内存就非常可观了。

参考文献:

  1. https://www.baeldung.com/java-memory-leaks

  2. https://cwiki.apache.org/confluence/display/tomcat/MemoryLeakProtection

-END-

ThreadLocal不好用?那是你没用对!


额!Java中用户线程和守护线程区别这么大?


Semaphore自白:限流器用我就对了!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544785.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

@html.ActionLink的几种参数格式

http://blog.csdn.net/jingmeifeng/article/details/7792151 一 Html.ActionLink("linkText","actionName") 该重载的第一个参数是该链接要显示的文字&#xff0c;第二个参数是对应的控制器的方法&#xff0c;默认控制器为当前页面的控制器&#xff0c;如…

Java ClassLoader findClass()方法与示例

ClassLoader类findClass()方法 (ClassLoader Class findClass() method) findClass() method is available in java.lang package. findClass()方法在java.lang包中可用。 findClass() method is used to find the class with the given binary class name. findClass()方法用于…

ThreadLocal内存溢出代码演示和原因分析!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;前言ThreadLocal 翻译成中文是线程本地变量的意思&#xff0c;也就是说它是线程中的私有变量&#xff0c;每个线程只能操作自…

C,C++宏中#与##的讲解

文中__FILE__与示例1可以参见《使用ANSI C and Microsoft C中常用的预定义宏》宏中的#的功能是将其后面的宏参数进行字符串化操作&#xff08;Stringizing operator&#xff09;&#xff0c;简单说就是在它引用的宏变量的左右各加上一个双引号。 如定义好#define STRING(x) #x之…

int?id与id??1 的意思

http://blog.csdn.net/jingmeifeng/article/details/24710143 int? id 表示id是可以为null的整型 跟Nullable<int> id 是一样的 id ?? 1等于 idnull?1:id;

彻夜怒肝!Spring Boot+Sentinel+Nacos高并发已撸完,快要裂开了!

很多人说程序员是最容易实现财富自由的职业&#xff0c;也确实&#xff0c;比如字节 28 岁的程序员郭宇不正是从普通开发一步步做起的吗&#xff1f;回归行业现状&#xff0c;当开发能力可以满足公司业务需求时&#xff0c;拿到超预期的 Offer 并不算难。最近我也一直在思考这个…

java中get接口示例_Java LocalDateTime类| 带示例的get()方法

java中get接口示例LocalDateTime类的get()方法 (LocalDateTime Class get() method) get() method is available in java.time package. get()方法在java.time包中可用。 get() method is used to get the value for the given field from this date-time object. get()方法用于…

湖南多校对抗5.24

据说A,B,C题都比较水这里就不放代码了 D:Facility Locations 然而D题是一个脑经急转弯的题&#xff1a;有m行&#xff0c;n列&#xff0c;每个位置有可能为0&#xff0c;也可能不为0&#xff0c;问最多选K行是不是可以使得每一列都至少有一个0&#xff0c;其中代价c有个约束条件…

PPT演讲计时器

下载 GitHub 源码地址 如果访问不到的话&#xff0c;可以从百度盘下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1bK4sug-eK85fmPgi9DzhcA 提取码&#xff1a;0vp3 文件&#xff1a;VB.Equal.Timer-VB计时器软件-绿色无残留 写在前面 转眼也工作了两年了&…

2万字!66道并发面试题及答案

我花了点时间整理了一些多线程&#xff0c;并发相关的面试题&#xff0c;虽然不是很多&#xff0c;但是偶尔看看还是很有用的哦&#xff01;话不多说&#xff0c;直接开整&#xff01;01 什么是线程&#xff1f;线程是操作系统能够进⾏运算调度的最⼩单位&#xff0c;它被包含在…

stl向量_如何在C ++ STL中将数组元素复制到向量?

stl向量Given an array and we have to copy its elements to a vector in C STL. 给定一个数组&#xff0c;我们必须将其元素复制到C STL中的向量。 将数组元素复制到向量 (Copying array elements to a vector) In C STL, we can copy array elements to a vector by using…

YoloV8的目标检测推理

YoloV8的目标检测推理 原始的YoloV8封装的层次太高&#xff0c;想要为我们所用可能需要阅读很多API&#xff0c;下面给出比较简单的使用方式 导入所需的库 os&#xff1a;用于操作文件系统。cv2 (OpenCV)&#xff1a;用于图像处理。numpy&#xff1a;提供数学运算&#xff0…

【翻译】从Store生成Checkbox Group

原文&#xff1a;Ext JS: Generating a Checkbox Group from a StoreExt JS的checkbox group可以用来将复选框组合成一个单一的逻辑字段。由于复选框时不时需要动态的从Store中生成&#xff0c;因而&#xff0c;如果将store绑定到扩展类&#xff0c;就最好不过了。以下是第一次…

25种代码坏味道总结+优化示例

前言什么样的代码是好代码呢&#xff1f;好的代码应该命名规范、可读性强、扩展性强、健壮性......而不好的代码又有哪些典型特征呢&#xff1f;这25种代码坏味道大家要注意啦1. Duplicated Code &#xff08;重复代码&#xff09;重复代码就是不同地点&#xff0c;有着相同的程…

滚动照片抽奖软件

CODE GitHub 源码 1、女友说很丑的一个软件 说个最近的事情&#xff0c;女友公司过年了要搞活动&#xff0c;需要个抽奖的环节&#xff0c;当时就问我能不能给做一个&#xff0c;正好我也没啥事儿&#xff0c;就在周末的时候用C#做了一个&#xff0c;虽然派上用场了&#xf…

Java即时类| 带示例的compareTo()方法

即时类compareTo()方法 (Instant Class compareTo() method) compareTo() method is available in java.time package. compareTo()方法在java.time包中可用。 compareTo() method is used to compare this Instant object to the given object. compareTo()方法用于将此Instan…

11个小技巧,玩转Spring!

前言最近有些读者私信我说希望后面多分享spring方面的文章&#xff0c;这样能够在实际工作中派上用场。正好我对spring源码有过一定的研究&#xff0c;并结合我这几年实际的工作经验&#xff0c;把spring中我认为不错的知识点总结一下&#xff0c;希望对您有所帮助。一 如何获取…

MFC中的几种播放声音的方法

一&#xff0e;播放声音文件的简单方法  在VC 中的多媒体动态连接库中提供了一组与音频设备有关的函数。利用这些函数可以方便地播放声音。最简单的播放声音方法就是直接调用VC中提供的声音播放函 数BOOL sndPlaySound ( LPCSTR lpszSound,UINT fuSound ); 或BOOL PlaySound(…

标志枚举的使用

标志枚举的使用大多是在标记多重状态&#xff0c;比如说文件的属性&#xff1a;只读&#xff0c;可写&#xff0c;隐藏&#xff0c;系统文件等相关属性&#xff0c;都对应相应的标志位&#xff0c;如果在C#中想实现自己的标志枚举&#xff0c;也是可以的&#xff0c;下文是亲身…

duration java_Java Duration类| 带示例的getUnits()方法

duration java持续时间类getUnits()方法 (Duration Class getUnits() method) getUnits() method is available in java.time package. getUnits()方法在java.time包中可用。 getUnits() method is used to get the List object that contains the units of seconds and Nanos …