前言
头条上每天都会同步更新,喜欢看的也可以关注下头条,小疯子程序员
今天我们来说一个问题, 如果垃圾收集器不存在,会发生什么样的情况?我们不断地在创建对象,那么总会突破内存限制,对象再也方法不下了,造成溢出,Java垃圾收集器的存在,让我们程序员解脱了出来,不需要考虑去释放内存,但是,垃圾收集器并不是万能的,某些情况下还是会出现各种问题。
正文
在Java虚拟机规范的描述中,内存大概我们分了这几个点,程序计数器,虚拟机栈,本地方法,堆,方法区,程序计数器线程私有,主要存储的方法运行指令信息,一般不会发生OutOfMemoryError(OOM)异常,但是在其他区域,我们要考虑到OOM异常的出现。
1. Java堆溢出
Java堆主要存储的是对象实例,主要我们不断的创建对象,并且保证这些对象不被垃圾收集器回收,内存没办法空出来,那么一定就会造成内存溢出,对象是否可回收,一般是采用标记法或者GC roots对象可达性分析来判断的,具体两种方法,后面细说,先卖个关子。
Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。 当出现Java堆内存溢出时,异常堆栈信息
“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。我们要解决这方面问题,要具体定位到问题出在哪里,是不是我们创建的对象时必要的,也要分清楚是不是内存泄漏还是内存溢出,分清楚这些问题,才能具体问题具体分析。
内存泄漏:不再需要的对象得不到回收,内存没办法释放。典型的占个茅坑不拉屎。
内存溢出:内存里对象已经满了,你还要继续放对象,人家就要报错了。
如果是内存泄露,我们可以通过一些工具来查看泄露对象到GC Roots的引用链。 找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。 掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。
如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、 持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
2.虚拟机栈和本地方法
我们都知道对象实例是放在堆中的,所以垃圾收集器工作频繁的地方也是在堆中,上一节我分析了对象的创建,分配内存,所以应该很好理解了,但是虚拟机栈跟本地方法一般说的比较少,但是它们也存在内存溢出。
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
总结一句话:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
但是在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是*Error异常。
如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
3.方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,因此这两个区域的溢出就放在一起讨论。
方法区用于存放Class的相关信息,如类名、 访问修饰符、 常量池、 字段描述、 方法描述等,我们要想造成溢出就要在运行时产生大量的类去填满方法区。
其实在实际应用中,我们一定会遇到这样的情况,当前的很多主流框架,如Spring、 Hibernate,在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。 另外,JVM上的动态语言(例如Groovy等)通常都会持续创建类来实现语言的动态性,随着这类语言的流行,也越来越容易遇到溢出场景。
本机直接内存溢出
这个直接内存溢出一般是我们不会考虑的,虚拟机内存的讨论也不包含这个部分,但是今天拿出来说,是因为这是一个比较隐秘的内存块,我们还在学习NIO时,大家不知道还记不记得有这样一块内存,通过将硬盘上的信息先映射到一块内存上,然后通过ByteBuffer来与内存直接交换数据,这样可以提高读取信息的速度。
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。