agoodjavaboy

C和C++工程师掌握创建和销毁内存空间的权力,并维护内存中每一个对象从始至终的生命。但Java工程师可以不再繁琐的进行内存控制,并且更不容易出现内存泄露和溢出的问题,但如果不了解Java是如何自动对内存进行控制的,在出现问题后更难定位。

JVM内存运行时数据区域

JVM运行时会将所管辖的内存划分为不同区域做不同功能,有的随虚拟机进程而生死,有的因线程而生死,虚拟机规范规定内存中要包含以下几个区域:

image

  • 程序计数器:较小空间的内存,用于记录当前线程所执行的字节码文件所在行号。通过改变这里的行号来实现指令中的分支、循环、跳转等操作。因为Java的多线程是多个线程轮流切换运行的,在执行另一线程时要让来源线程记录好自己所处的行号,所以每个线程都有自己独立的程序计数器,相互之间不会干扰,这种每个线程独有的空间就叫线程私有空间。当执行的是Natvie方法时,此空间记录为空,并且此空间是唯一不存在内存溢出错误的空间。

  • 栈:线程私有且生命周期与线程相同,在执行每个方法时都会创建栈帧压入到栈里,方法的调用开始就是栈帧入栈,完成就是栈帧出栈。栈帧中记录着方法的局部变量表、操作栈、动态链接和方法出口等信息。局部变量表存放了编译期间可知的基本类型和引用类型,还有就是returnAddress类型,表示一条字节码指令的地址。其中64位长度的变量数据将占用两个变量空间,其余占用一个变量空间。局部变量表会在编译期间完成分配,当进入一个方法开始指向的时候,这个方法所需要的局部内存就已经确定了,不会再改变了。当方法调用深度过深,比如出现了自调用情况时,就会出现栈溢出异常,当然一些虚拟机栈会动态扩大栈深度,当无法再获取内存资源来扩展时会出现内存溢出异常。

  • 本地方法栈:跟虚拟机栈很类似,采用同样的存储方式也会有同样的异常,但是用来运行Native方法的,没有规定其中方法所使用的语言,所以可以*的实现它。有的虚拟机例如HotSpot,就把虚拟机栈和本地方法栈合二为一了。

  • 堆:堆是JVM中最大的空间,是所有线程都会共享的空间,将在虚拟机启动时创建,也就是与进程同生死。对里存放对象的实例,所有的对象实例都将在这里进行分配。垃圾回收也将主要在堆中执行,为了更快的回收还会把堆中的对象进行各种分类。堆是物理不连续的内存空间,只要逻辑上是连续的就可以,这类似磁盘空间。当堆没法再去获得空间将出现内存溢出异常。

  • 方法区:存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等,是线程共享的。虽然Java规范将其形容为堆的一个区域,但只是一个逻辑分区。HotSpot虚拟机把方法区在垃圾回收中设置为永生代,但其他的虚拟机并不存在永生代的说法,之所以这么叫是因为其中的内容很少会被清理,清理起来也是比较的麻烦,Java官方Bug清单中就出现了很多因为此处空间导致的内存泄露。它像堆一样是不连续的物理内存,可以固定或扩展大小,可以选择不实现垃圾回收,当无法获取内存时也会出现内存溢出异常。

  • 运行时常量池:它是方法区的一部分,Class文件中除了类的版本、字段、方法、接口等描述信息外还有就是常量池,用于存放编译期间生成的字面量和符号引用,在类加载后放入此处空间。JVM对Class文件的每个部分都做了严格的规定,每个字节存储哪种数据都必须规范才会被JVM认可、装载和执行,但对于常量池JVM规范并没有特殊要求,不同的提供商可以自己实现这块区域,所以通常除了存放字面量和符号引用外,直接引用也会存在这里。Class文件常量池必须具备动态性,也就是并不一定在编译期间产生,运行时也可以放入新的常量。因为是方法区的一部分,所以当空间无法扩展也会出现内存溢出异常。

  • 直接内存:并不是JVM运行时内存的一部分,并且也并未在JVM规范中定义,但这块内存也被频繁使用并且也会出现内存溢出异常。JDK1.4中引入了NIO,非阻塞的IO方式,可以让Native函数直接分配堆外的内存,然后通过堆里的DirectByteBuffer对象作为引用指向这款内存,从而避免在堆和Native堆中切换复制数据,从而提高性能。堆外内存实在本机内存中的,虽然不会收到JVM的影响但空间和寻址空间的限制。当内存区域大于物理内存限制的时候会导致内存溢出异常。

分类:

JVM

技术点:

相关文章: