Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
程序计数器:当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。由于JVM的多线程时通过轮流切换并分配处理器执行时间的方式来实现的,为了在线程切换后能正常恢复到正确执行的位置,每条线程都需要有一个独立的程序计数器,独立存储,互不影响。所以程序计数器是线程私有的内存区域。如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程执行的是一个Native方法,计数器的值为空。此区域是Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈:Java虚拟机栈也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
程序员主要关注的就是Stack栈内存,就是虚拟机中局部变量表部分。局部变量表存放了编译期可知的各种数据类型(八大数据类型),对象引用和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译时期完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。Java虚拟机规范中对这个区域规定了两种异常情况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务(也就是字节码),而本地方法栈为虚拟机使用到的Native方法服务。Java虚拟机规范对本地方法栈使用的语言、使用方法与数据结构并没有强制规定,因此可以由虚拟机自由实现。同虚拟机栈相似,Java虚拟机规范也规定对这个区域两种异常情况:StackOverflowError异常和OutOfMemoryError异常。
Java堆:Java堆是被所有的线程共享的一块内存区域,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾回收器管理的主要区域,因此被称为GC堆。从内存回收的角度看,由于现在收集器基本痘采用分代收集算法,所以Java堆可以细化为:新生代、老生代;从内存分配的角度看,线程共享的Java堆可能划分为多个线程私有的分配缓冲区(TLAB);不论如何划分,都与存放的内容无关,无论哪个区域,存储的仍然是对象实例。Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。Java虚拟机规范规定,如果在堆上没有内存完成实例分配,并且堆上也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区:方法区和Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范方法堆方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。Java虚拟机规范规定,当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常。
运行时常量池:运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息就是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的另外一个重要特征就是哭呗动态性,Java语言并不要求常量一定要在编译期间才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用较多的时String类的intern()方法。
String.intern():String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此字符串的引用。
…………………………………………………………………………………………………………………………………
Java对象的访问:Object obj = new Object();
其中,“Object obj”这部分语义作为一个reference类型数据出现,将存储到JAVA栈的本地变量表中。new Object()将生成一个实体对象,存储在JAVA堆中。
由于reference类型在JAVA虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用该通过哪种方式去定位,以及访问到JAVA堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:句柄和直接指针。
JAVA堆中将会划分出来一块内存作为句柄池,reference中就是存储了对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。使用句柄访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要被修改。
相比较句柄的访问方式,JAVA堆中不会单独划分内存,reference中直接存储了对象地址,而对象中包含了对象类型数据的地址信息。使用直接指针的最大好处就是速度更快,节省了一次指针定位需要的时间开销,由于JAVA对象访问十分频繁,这类开销积小成多后也是一项非常可观的执行成本。
Java创建对象的步骤:
1. 虚拟机遇到一条new指令
2. 检查指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,若没有,则必须首先执行相应的类加载过程
3. 类加载完成,虚拟机为新生对象分配内存。分配内存相当于从Java堆中划分出一块内存大小确定的块,分两种情况
1. Java堆内存,属于绝对规整的那种。只需指针向空闲空间挪动一段举例即可
2. 不规整。空闲区和已分配区交错,需要一张“空闲列表”记录哪些区域是分配了的
4. 虚拟机对对象进行必要的设置。如,这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码。此步骤为止虚拟机步骤完成
5. Java程序开始:init方法还没有执行,所有字段仍然为0,new指令后,执行 init 方法。
OutOfMemoryError异常总结:
1. Java堆溢出:Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,对象数量达到最大堆容量限制,则发生溢出。这种异常比较常见,一般先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,确实是内存泄露还是内存溢出。
a) 内存泄露:查看泄露对象到GC Roots的引用链,定位泄露代码位置。
b) 内存溢出:如果不存在泄露,即内存中的对象确实都还必须活着,检查JVM堆参数(-Xmx与-Xms),调大参数,检查代码是否存在某些对象生命周期过长,持有状态过长的情况,减少程序运行期的内存消耗。
2. 虚拟机栈和本地方法栈溢
a) StackOverFlow:递归调用方法,定义大量的本地变量,增大此方法帧中本地变量表的长度。
b) OutOfMemoryError:多线程下的内存溢出,与栈空间是否足够大并不存在任何联系。为每个线程的栈分配的内存越大(参数-Xss),那么可以建立的线程数量就越少,建立线程时就越容易把剩下的内存耗尽,越容易内存溢出。在这种情况下,如果不能减少线程数目或者更换64位虚拟机时,减少最大堆和减少栈容量能够换区更多的线程。方法区和运行常量池溢出
3. 方法区和运行常量池溢出:
4. 本地直接内存溢出: