珠海华兴建设工程有限公司网站,正规代加工项目招商,苏州免费发布信息网站,seo快速优化软件虽然Java程序员不用像C/C程序员那样时刻关注内存的使用情况#xff0c;JVM会帮我们处理好这些#xff0c;但并不是说有了GC就可以高枕无忧#xff0c;内存泄露相关的问题一般在测试的时候很难发现#xff0c;一旦上线流量起来可能马上就是一个诡异的线上故障。1. 内存泄露的… 虽然Java程序员不用像C/C程序员那样时刻关注内存的使用情况JVM会帮我们处理好这些但并不是说有了GC就可以高枕无忧内存泄露相关的问题一般在测试的时候很难发现一旦上线流量起来可能马上就是一个诡异的线上故障。1. 内存泄露的定义如果GC无法回收内存中不再使用的对象则定义为内存有泄露2. 未关闭的资源类当我们在程序中打开一个新的流或者是新建一个网络连接的时候JVM都会为这些资源类分配内存做缓存常见的资源类有网络连接数据库连接以及IO流。值得注意的是如果在业务处理中异常则有可能导致程序不能执行关闭资源类的代码因此最好按照下面的做法处理资源类public void handleResource() {try {// open connection// handle business} catch (Throwable t) {// log stack} finally {// close connection}
}
3. 未正确实现equals()和hashCode()假如有下面的这个类public class Person {public String name;public Person(String name) {this.name name;}
}
并且如果在程序中有下面的操作Test
public void givenMapWhenEqualsAndHashCodeNotOverriddenThenMemoryLeak() {MapPerson, Integer map new HashMap();for(int i0; i100; i) {map.put(new Person(jon), 1);}Assert.assertFalse(map.size() 1);
}
可以预见这个单元测试并不能通过原因是Person类没有实现equals方法因此使用Object的equals方法直接比较实体对象的地址所以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;}
}
则上文中的单元测试就可以顺利通过了需要注意的是这个场景比较隐蔽一定要在平时的代码中注意。4. 非静态内部类要知道所有的非静态类别类都持有外部类的引用因此某些情况如果引用内部类可能延长外部类的生命周期甚至持续到进程结束都不能回收外部类的空间这类内存溢出一般在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();}}
}
5. 重写了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对此类对象的处理有特殊逻辑。6. 针对长字符串调用String.intern()如果提前在src/test/resources/large.txt中写入大量字符串并且在Java 1.6及以下的版本运行下面程序也将得到一个OOMTest
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此类问题也就无需再关注了7. ThreadLocal的误用ThreadLocal一定要列在Java内存泄露的榜首总能在不知不觉中将内存泄露掉一个常见的例子是Test
public void testThreadLocalMemoryLeaks() {ThreadLocalListInteger localCache new ThreadLocal();ListInteger cacheInstance new ArrayList(10000);localCache.set(cacheInstance);localCache new ThreadLocal();
}
当localCache的值被重置之后cacheInstance被ThreadLocalMap中的value引用无法被GC但是其key对ThreadLocal实例的引用是一个弱引用本来ThreadLocal的实例被localCache和ThreadLocalMap的key同时引用但是当localCache的引用被重置之后则ThreadLocal的实例只有ThreadLocalMap的key这样一个弱引用了此时这个实例在GC的时候能够被清理。img其实看过ThreadLocal源码的同学会知道ThreadLocal本身对于key为null的Entity有自清理的过程但是这个过程是依赖于后续对ThreadLocal的继续使用假如上面的这段代码是处于一个秒杀场景下会有一个瞬间的流量峰值这个流量峰值也会将集群的内存打到高位(或者运气不好的话直接将集群内存打满导致故障)后面由于峰值流量已过对ThreadLocal的调用也下降会使得ThreadLocal的自清理能力下降造成内存泄露。ThreadLocal的自清理实现是锦上添花千万不要指望它雪中送碳。8. 类的静态变量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 ThreadLocalMyCounter {
}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泄露它泄露的内存就非常可观了。参考文献https://www.baeldung.com/java-memory-leakshttps://cwiki.apache.org/confluence/display/tomcat/MemoryLeakProtection-END-