从字面意思上来看,内存溢出与逃逸分析,貌似有些联系,一个是溢出,一个是逃逸,意思差不多。但是,这是两个完全不同的概念,千万不要混淆了。

内存溢出的原因是内存不足,在JMV上没有办法为新创建的对象申请到内存资源,就出现了内存溢出错误。当我们看到OutOfMemoryError异常时,就是发生了内存溢出。

即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术。逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据。

关于内存溢出,你需要了解这些

1、堆溢出。java.lang.OutOfMemoryError: Java heap space 出现这个错误,就发生了堆溢出。通过new创建的对象,是在堆上分配的,如果堆的空间不足,就会发生这个异常。可以通过调整-Xmx和-Xms增加堆空间,以避免出现这个错误。避免使用集合类作为单例对象的成员变量。缓存的功能,一定要用WeekHashMap。

2、栈溢出。java.lang.StackOverflowError 出现这个错误,就发生了栈溢出。是由于函数的调用深度太深造成的,一般会出现在递归调用的时候,要注意检查代码,调用深度不宜过深。也可以通过调整-Xss参数,避免这个错误。

3、永久带溢出。java.lang.OutOfMemoryError: PermGen space 出现这个错误,就是发生了永久带溢出,可能的原因包括:用了过多的第三方包、定义了大量常量、通过intern注入了常量、动态加载了太多的类等。可以通过-XX:PermSize和-XX:MaxPermSize增加永久代空间,以避免这个错误。

4、线程内存溢出。java.lang.OutOfMemoryError: unable to create new native thread 出现这个错误,就发生了线程的内存溢出。操作系统对线程数量有一定的限制,同时,线程上下文要占用一定的资源。过多的线程导致频繁的上下文切换,导致程序效率降低。从程序的角度,要有控制线程数量的预期,推荐使用线程池。

5、直接内存溢出。采用Netty、NIO等这样的框架,缓冲区是直接在操作系统的内存区分配的,而不是堆上分配。Jvm垃圾回收的时候,并不会回收这部分的内存。自己写代码的时候,在用完缓冲区,一定要调用clear函数回收内存,这点跟使用数据库连接是一样的,使用完要记得关闭。程序没有错误的情况下,可以通过调整参数-XX:MaxDirectMemorySize增加直接内存

还有一个容易发生内存溢出的场景,那就是JNI(Java Native Interface)。被调用的C/C++的代码,一定要做好内存的管理,这部分内存Jvm也是管理不到的。

关于逃逸分析,你需要了解这些:

逃逸分析并不是直接的优化手段,而是一个代码分析,分析对象的作用域,看是否发生对象逃逸。发生逃逸行为的情况有两种:方法逃逸和线程逃逸。

  • 方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中。
  • 线程逃逸:如类变量或实例变量,可能被其它线程访问到。

如果没有发生逃逸,就可以做优化,优化的方法如下

同步消除

线程同步本身比较耗,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,则可以消除对该对象的同步锁。

标量替换

标量是指不可分割的量,如java中基本数据类型和reference类型;相对的一个数据可以继续分解,称为聚合量。把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替。如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是在栈上创建若干个成员变量。

栈上分配

顾名思义,就是栈上分配对象。一般情况下,创建的对象是在堆上分配的,没有发生方法逃逸的对象,在栈上分配效率更高。执行完毕后,释放栈的资源,对象被消除,就不用等到垃圾回收了。然而真正的实现策略却是:并没有在栈上创建对象,而是利用了标量替换的原理。