Java查漏系列(2)——java内存区域

前一节大致的介绍了一下JVM的体系结构,如下图:


其中,Runtime DataArea(运行时数据区)是整个JVM的重点,平时,由于我们编写java程序很少关心内存的释放问题,这个都是JVM来自动管理的,不过,也正是因为Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。这里就大致的介绍一下JVM的这一区域。

JVM中,所有的数据和程序都存放在运行时数据区,如上图,这个区域又包括几个子区域,它们各自有各自的用途和生命周期, MethodArea和Heap是基于JVM实例的,即JVM的每个实例都有一个它自己的方法域和一个堆;PC Register和Stack是基于线程的,即每个线程创建的时候,都会拥有自己的程序计数器和栈;Native Method Stack是为虚拟机用到的Native方法服务。下面分别介绍这几个区域:

1.Heap(堆)

一个JVM实例只存在一个堆内存,对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:

a)Permanent Space(永久存储区)

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,一般被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

b)Young Generation Space(新生区)

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0space)和1区(Survivor 1space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。

c)Tenure Generation Space(养老区)

养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。

三个区的示意图如下:


之所以将堆内存再进行分区,主要是基于这样一个事实:不同对象的生命周期是不一样的。在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,对堆进行分区管理是采用了分治的思想,把不同生命周期的对象放在不同区域,不同区域采用最适合它的垃圾回收方式进行回收。分区之后可以提高JVM垃圾收集的效率,进而优化内存管理。

无论对Java堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

2.Method Area(方法域)

方法域实际上就是堆中的永久存储区(Permanent Space),它还有个别名叫做Non-Heap(非堆),所以也可以将方法域看作堆的一个逻辑部分。方法域中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。这个区域除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久存储区不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。对类的卸载需要满足下面3个条件:
  1)该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
  2)加载该类的ClassLoader已经被GC;
  3)该类对应的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

3.Stack(栈)

栈的生命周期也是与线程相同。栈描述的是Java方法调用的内存模型:每个方法被执行的时候,都会同时创建一个栈帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个栈帧在栈中的入栈至出栈的过程。栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“后进先出”原则。栈帧中主要保存3类数据:本地变量(LocalVariables),包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack),记录出栈、入栈的操作;栈帧数据(Frame Data),包括类文件、方法等等。

栈中有两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果栈可以动态扩展,当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

4.PC Register(程序计数器)

每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令。对于非Native方法,这个区域记录的是正在执行的VM原语的地址,该地址指向方法域中的方法字节码,由执行引擎读取下一条指令。如果正在执行的是Natvie方法,这个区域则为空。

5.Native Method Stack(本地方法栈)

本地方法栈与VM栈所发挥作用是类似的,只不过VM栈为虚拟机运行VM原语服务,而本地方法栈是为虚拟机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun Hotspot虚拟机)直接就把本地方法栈和VM栈合二为一。和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。

这里对运行时数据区的几个逻辑组成部分做了个大致的介绍,其中,同样是存储数据的栈和堆有什么区别呢?这可能也是我们编码时容易忽略的地方。下一节来分析一下这个。


你可能感兴趣的:(java)