导语
最近一段时间需要对项目的内存进行优化,因为项目比较老,代码经过很多手,导致应用在使用过程中有较为严重的内存泄漏,在某些情况下还会出现OOM,简直是不能忍,所以简单记录一下从入门到放弃的过程,就当做是学习和总结。
JAVA运行时内存区域
Java虚拟机有一套内存自动管理的机制,所以程序员不需要也不能手动的alloc内存,这在很大程度上避免了内存泄漏的发生,但是不能百分百的避免。
在执行程序的时候会对它管理的内存进行划分,在不同的区域分配不同的工作,下图中展示了运行时各区域的基本结构,主要分为线程私有和共享两类:
1.1程序计数器
程序计数器是线程私有的,且是内存分区里头唯一一个不会造成OOM异常的一个内存区域。
它的作用是虚拟机字节码指令执行过程的管理者,通过计数器的值就可以控制下一条需要执行的指令字节码,它记录的是字节码指令的地址,由于Java虚拟机可以执行多线程,而每个处理器在每个时刻都只会执行一条线程,为了线程切换过程中保证字节码指令的稳定,每一条线程都会持有一个独立的计数器。需要说明的是,以上我们针对的是Java方法,如果虚拟机执行的是Native方法的话,计数器的值会为0。
1.2虚拟机栈
Java虚拟机栈是线程私有的,它负责Java方法执行过程中的入栈、弹栈,它可能会造成两种异常,StackOverFlowError与OutOfMemoryError,就是栈溢出与内存溢出。
我们经常会提到栈内存,这个栈内存指的就是虚拟机栈,虚拟机栈是的构成元素是栈帧,每个栈帧包含了局部变量表,操作数栈,动态连接,方法返回地址和一些额外信息,一个方法对应一个栈帧,方法从开始调用到执行结束的过程,就是一个栈帧在虚拟机栈中入栈到弹栈的过程。
大概讲一下图中的栈帧各组成结构作用
1.2.1局部变量表
局部变量表用于存放方法中的参数和方法内部定义的局部变量,最小组成单位是Slot,虚拟机规范并没有明确规定Slot占用的内存空间大小为32位,但是需要每一个Slot能够保证存放一个boolean、byte、char、short、int、float、reference(即对象类型)、returnAddress类型(指向一条字节码指令的地址),即能够存放32位以下的数据,所以根据不同虚拟机的具体实现把Slot设定为64位也是有可能的。
Java程序员都知道在方法内部调用"this"关键字可以引用方法所在的对象的实例引用,其实这个隐藏的参数是编译器在此处怼进去的,如果是实例方法(非static,static方法里面引用不了"this"关键字也是这里造成的),会在局部变量表的首位,即第0索引处保存该方法所属对象的实例引用。
1.2.2操作数栈
操作数栈用于存取方法执行过程中字节码指令操作的内容,算术操作和参数的传递都是在操作栈中进行的。
顺便提一下,Java虚拟机和Android 的Dalvik虚拟机一个重要区别就在这个地方,Java虚拟机是基于栈的执行引擎,这个栈就是操作数栈,它必须使用指令来载入和操作栈上数据,而Dalvik虚拟机是基于寄存器的。
1.2.3动态连接
我们都知道Java有三大特性,继承、封装、多态,为了实现多态,Java中方法调用就不能直接指向方法的地址,因为这样的话方法就会唯一确定,而多态是在运行时动态的选择方法(比如子类实现父类方法A,只有在虚拟机运行的时候才知道到底要执行父类还是子类的方法A),为了支持方法调用过程中的动态连接,栈帧中就会保留指向常量池中的符号引用,在真正方法调用时会以常量池中的符号来获取直接引用(实际的方法地址),为了虚拟机的性能着想,一部分的方法调用会在编译期间就会把符号引用转化成直接引用,这里会分为两种情况:
1.符合"编译期可知,运行期不可变"的方法调用会在代码写好和编译完成之后就会把这些方法的符号引用转化为直接引用,主要包括静态方法和私有方法,这两种方法不可能被继承,运行期间是不可能发生改变的
2.其它的方法会把符号引用到直接引用的转化延迟到运行期间才会进行,也称为分派调用,具体的调用逻辑可以参考书籍《深入理解Java虚拟机》
1.2.4方法返回地址
方法执行完成后,需要回到方法调用位置,这样才能保证程序继续执行,一般情况下此处会保留调用者的计数器值,而在方法异常退出的情况下,返回地址是通过异常处理表来处理的。
1.2.5附加信息
可以把这里当做Java虚拟机规范为了虚拟机的具体实现者预留的扩展位置,方便虚拟机具体实现者保存一些Java虚拟机规范中没有明确要求的信息。
虚拟机栈的深度是有规定要求的,在超过其最大值会抛StackOverFlowError异常(比如无限递归调用本方法),在一些虚拟机栈可以动态扩展的虚拟机上,在栈深度不够用时则会扩展,一直到内存不够用的时候抛出OutOfMemoryError异常。
1.3本地方法栈
本地方法栈与虚拟机栈功能基本一样,是线程私有的,两者的主要区别在于它负责Native方法执行过程中的入栈、弹栈,而虚拟机栈负责Java方法,它也可能会造成两种异常,StackOverFlowError与OutOfMemoryError,就是栈溢出与内存溢出。
1.4堆
平时程序猿所讨论的堆栈,栈指的是虚拟机栈,而这个堆就是这里讲到的Java堆,它是线程共享的,基本上所有的对象和数组都是在堆上分配内存的。它可能会造成OutOfMemoryError异常。
OOM异常一般是堆中内存不足造成的,由于对象不能或者未及时释放,导致对象一直占用堆中的内存,这会造成内存泄漏,当积累到一定程度,即堆内存不够用的时候虚拟机就会抛出OOM异常。
Java堆也称为"GC堆",对象的创建可以说是程序中最为频繁的现象,Java虚拟机会自动管理Java堆中的内存,在内存不够用的时候虚拟机会启动垃圾收集器进行垃圾回收操作释放一部分没有被引用的对象。后面有时间的话可以讲一下垃圾回收相关的知识。
1.5方法区
方法区跟堆一样,属于线程共享,当内存不足时会抛OutOfMemoryError异常。方法区逻辑上是堆的一部分,但是又会和堆区分开来,所以又称为非堆。
在我们的开发中,有时会把方法区称为永久代,用于存储一些类信息、常量、静态变量等,从永久代这个称谓中就可以看出在方法区中的变量存活时间比较久,因为这个区域很少会发生垃圾收集的行为,但是并非数据进入方法区后就会永久存在,方法区中的常量池是存在被垃圾回收的可能的。
1.5.1运行时常量池
运行时常量池是方法区的一部分,在讲到虚拟机栈-栈帧-动态连接的时候,提到过动态连接会保留指向运行时常量池的方法字符引用,从这里可以看出运行时常量池用于存放Class文件编译期生成的各种字面量和符号引用,对于静态方法和私有方法,运行时常量池在编译期也会存储这些方法的直接引用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。