作为一个面向对象的编程语言,Java提供了自动的内存管理机制,这也成为了很多面试官最喜欢问的问题。从内存管理的角度来谈谈Java语言的优势?
相比较于C语言通过手动去申请、使用、释放内存来讲,Java最大的优势就在于其对于内存的自动管理,并且依赖于GC机制,能够自动识别和清理不用的内存空间,减少了手动释放内存的过程,简化的开发人员的工作。
下面我们就来谈谈Java自动内存所带来的OOM的问题。
什么是OOM?Java中的OOM(Out of Memory)通常是指内存溢出异常,由于在Java程序运行的过程中,需要为每个对象都分配一定的内存空间。当一个应用程序因为某些原因需要使用更多的内存空间的时候,但是这个时候却无法分配到这些空间的时候,就会出现OOM的异常。
什么情况下会发生OOM?一般情况下我们遇到的都是堆内存的溢出,因为在创建对象初期,大多数的对象都是占用堆内存空间,当堆内存分配不足的时候就会出现OOM的异常。
java.lang.OutOfMemoryError: Java heap space
堆内存溢出具体的场景有如下一些
- 由于对象生命周期太长:如果在创建某个对象的时候其生命周期太长,而且它所占用的空间又非常大的时候,那么在不断的创建对象的过程中,就会导致堆内存耗尽,导致内存溢出。这种情况一般出现在以集合作为缓存,并且没有考虑到缓存的淘汰机制的情况。
- 无限递归操作:递归是在开发中经常会被使用到的技术。如果在一个递归中缺少了递归结束的条件,或者是递归的深度太大也有可能会造成内存溢出。
- 大数据集合:在处理大量数据的时候,如果我们不能正确的使用处理机制,就会导致大数据进入到内存中,最终导致内存溢出。
- JVM配比不当:如果在JVM启动的时候,参数配比不当也会造成内存溢出。
简单举个例子,下面这段代码,如果一直运行,就会导致内存溢出
List<Integer> list = new ArrayList<>();
while (true) {
list.add(1);
}
什么叫做内存泄露呢?
所谓的内存泄露,其实是与内存溢出不一样的一种特殊场景。其本质还是内存溢出,只不过是因为错误而导致的内存溢出,是指在程序运行的过程中,由于某些原因导致无法释放不再被使用的内存而导致内存占用不断增加而耗尽资源的一种情况。
常见的内存泄露的情况有如下几种
- 对象引用未被正确释放:若在使用完一个对象之后,忘记了将其引用设置为null或者是没有从使用的逻辑中删除,就会导致该对象会无法被垃圾回收机制识别最终就会导致内存溢出,例如对于ThreadLocal的使用。
- 长生命周期对象持有了短生命周期对象的索引:如果一个使用生命周期较长的对象,持有了一个短生命周期的对象,如果短生命周期的对象使用完成之后,由于长生命周期的对象依然存在,那么短生命周期的对象也将无法被垃圾回收机制识别,就会导致内存泄露
- 过度使用第三方的库依赖:由于某些第三方库其本身就是存在有一些内存泄露或者是内存溢出的的情况在里面,如果大量的使用这样的代码的话就会导致内存溢出。
- 集合类使用不当:跟上面描述的情况一样,如果在使用集合类的时候,没有对集合中的元素进行适当的处理,那么就会导致对象无法被释放,最终导致内存泄露。
- 使用资源未及时释放:例如在使用一些文件流、数据库连接、线程池、网络连接的时候,建议就是在不使用这些链接的时候要及时调用close方法将其进行关闭,避免发生内存泄露。
如下代码所示,就是一个典型的长周期对象持有了短周期对象的引用导致了内存泄露的问题。
List<Integer> list2 = new ArrayList<>();
@GetMapping("/headOOM2")
public String headOOM2() throws InterruptedException {
while (true) {
list2.add(1);
}
}
栈内存溢出
我们知道JVM的内存模型中,除了堆内存,还有一块栈内存,那么什么情况下会导致栈内存溢出呢?一般情况下发生栈内存溢出会抛出StackOverflowError的异常。
我们知道栈这种数据结构有个特点就是先进后出,那么基于这个特点,最有可能导致栈内存溢出的场景就是在调用递归方法的时候,递归的深度太深,导致方法栈内存不够,无法容纳方法的递归调用,就会导致栈内存溢出。
public class OOMExample {
public static void recursiveMethod() {
recursiveMethod();
}
public static void main(String[] args) {
recursiveMethod();
}
}
元空间耗尽
在Java8之前,JVM内存模型中还有一块内存叫做永久代。在Java8之后被元空间所取代了,而这块元空间主要的作用就是用来存储类的结构信息、方法信息、静态变量以及一些编译之后的代码等信息。
在Java8之前,对于一些大对象的内存分配,如果在堆内存中无法进行分配了,有一个概念就是内存管理会将这个对象的内存分配直接分配到永久代。如果说程序中出现了这种大量的大对象的分配,则会导致永久代的内存溢出。
显然这里元空间是取代了永久代的地位,它所完成的使命就是永久代的使命,所以当程序中加载了大量的类、以及动态生成的类、或者是使用了反射等操作生成了大量的大对象,最终就有可能导致元空间的内存被占满,导致内存溢出。
下面我们就来总结一下元空间被耗尽的情况。
- 类加载太多:如果一个应用程序大量的加载了很多的动态生成类,会导致元空间的使用量增加,最终如果没有办法卸载这些类的时候,就有可能会导致元空间耗尽。
- 字符常量太多:在Java中的字符常量通常会被存储在元空间中,如果在一个应用中使用了大量的字符常量,尤其是超长的字符串的存在,就会导致元空间被占满。
- 频繁使用反射:由于反射操作需要大量的类提供源数据,为了存储这些类信息,就会占用大量的元空间,如果在应用中频繁的使用了反射操作,就会导致元空间被占满。
- 大量的动态代理:动态代理操作其实还是反射的一种类型,它是使用了反射操作去创建对象。如果使用了太多的动态代理,就会生成大量的代理类,最终会占满整个的元空间。
- 没有正确的配置JVM元空间大小:在默认情况下,元空间的大小是不受限制的,它会自动的根据自己的需求来进行扩展,但是如果在使用了相关配置的情况下,没有对其进行合理的分配的时候就有可能会导致内存溢出。
根据上面所述,最终都是因为元空间是用来存储类信息的地方,如果类信息太多的话就会导致元空间内存溢出。如下的代码就是模拟了一个类加载太多导致元空间内存泄露的情况。
public class OOMExample {
public static void main(String[] args) {
while (true) {
ClassLoader classLoader = new CustomClassLoader();
classLoader.loadClass("com.example.LargeClass");
}
}
}
终极问题:如果一个Java线程处理请求的时候发生了内存溢出?那么整个进程还能处理请求么?
其实这个问题考察的内存还是挺多的。首先需要判断的就是OOM之后会不会导致整个的进程挂掉。其次呢?OOM是因为内存不够了?既然这样那么进程还能处理请求么?垃圾处理机制这个时候在干什么?难道不是因为垃圾处理之后发现无法清理了,没有空间了,最终的内存溢出是垃圾处理机制抛出的异常么?
带着这些问题我们来对其进行探究。
问题探究首先做一个简单的分析。我们知道当程序发生了内存溢出的时候,我们观察到的情况是程序还在运行着,也就是说好像有些功能还能用,真的是这样么?
我们在执行一个方法的时候,该方法对应的线程如果发生了OOM的异常,到底其他线程能不能再进程中执行其实是取决于这些线程执行逻辑所使用的内存是否会继续大量的占用内存空间。如果这个线程对应使用的内存空间较小,内存可以提供的时候,那么这个线程就会正常的得到执行,并且在内存不足的时候也会抛出OOM异常。那么如果这个线程执行所使用的内存空间太大的话,就会直接触发OOM的异常。
那么在不发生内存溢出的情况下,为什么频繁的创建对象会导致OOM,GC不会将这些对象进行回收么?
- 堆内存的限制:JVM对堆内存的大小其实是有所限制的,并不是无限的,所以如果频繁创建的对象没有得到有效的垃圾回收的时候,堆空间很快就会被占满耗尽,虽然GC还在不断的回收不在被使用的对象,但是如果这个时候创建对象的速度比垃圾回收的速度要高的话,那么就会导致堆内存溢出。有点像进水的速度比出水速度高的话水池依然会被灌满溢出。
- 垃圾回收对内存的开销:我们知道尽管垃圾回收是用来清理不使用的对象的,但是垃圾回收机制本身的运行也是需要占用资源的。如果频繁的面对的都是大对象的清理,那么对于垃圾回收器来讲也是一种性能资源上的挑战。并且Stop The World也会影响JVM的执行效率。
- 内存碎片化:随着对象的不断创建,不断的回收,会导致整个的堆空间产生大量的内存碎片。当内存的碎片程度太高的话,就会出现一种情况,总的内存使用依然足够,但是没有足够的大的连续的内存来分配给大的新对象的时候,这种情况下,即使是垃圾回收机制回收了一部分的空间,但是依然没有足够的空间来分配给对象的时候依然会出现OOM的异常。所以出现了这种情况,其实是最为致命的。
根据上面的分析讲解,相信大家对于问题也有了自己的认识,并且通过简单的分析我们得到了一个结论
- 在内存溢出的情况下,当GC的速度无法跟上对象创建速度的时候,会发生OOM的情况,并且将对应的OOM线程*掉。这种情况下其他线程一般还是可以处理请求的。
- 如果出现了内存泄露的情况,也就是由于这些内存空间本身是不能被回收的,这种情况发生了OOM,那么这个线程依然会被*掉,这个时候,如果有些线程的执行没有占用到内存的,那么它还是继续可以执行,但是如果出现了一个继续占用大量内存的线程那么它就无法执行了,最坏的情况就可能是导致整个的应用进程挂掉。