深入理解JAVA虚拟机 自动内存管理机制

运行时数据区域

深入理解JAVA虚拟机 自动内存管理机制

其中右侧三个一起的部分是每个线程一份,左侧两个是所有线程共享的。

程序计数器(Program Counter Register)

英文名称叫Program Counter Register。如果翻译为程序寄存器更加合理。

因为这块内存区域很小,功能也类似于寄存器。所以还是翻译寄存器比较靠谱。

在虚拟机的概念模型中(仅仅是概念模型,虚拟机可以通过一些更高效的方式来实现),字节码解释器会通过改变程序计数器的值来选取下一条需要执行的字节码指令。

因为CPU是要轮转的,在切换回来之后,Java能够找到下一条要执行的指令。

每一个线程会有一个独立的程序计数器。

线程执行Nativan方法时,计数器记录为空(Undefined)

唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况区域

线程私有,生命周期跟线程一样。

Java虚拟机栈(Java Virtual Machine Stacks)

线程私有,生命周期跟线程一样。

用于描述Java方法执行的内存模型:

一个栈帧代表一个方法的执行内存模型:入栈和出栈分别代表方法被调用和方法执行完成。

一个栈帧包含:局部变量,操作数栈,动态链接,方法出口等。

重点说局部变量:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(Object reference)和字节码指令地址(returnAddress类型)。

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

因为所有类型占用的内存大小都是可知的,所以局部变量的内存空间是在编译器完成分配的,运行期不会改变局部变量表的大小。

如果线程请求的栈深度(个人理解:方法调用的深度)大于虚拟机所允许的深度,将抛出*Error异常;

如果虚拟机栈可以动态扩展(个人理解:虽然局部变量表的空间在编译器就固定了,但是其他几个可能会动态扩展),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)

本地方法栈(Native Method Stacks)与Java虚拟机栈所发挥的作用非常类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

同样可能会抛出*Error和OutOfMemoryError异常。

Java堆(Java Heap)

Java堆(java heap)是Java虚拟机所管理的内存中最大的一块

它是被所有线程共享的一块内存区域,在虚拟机启动时创建

几乎所有的对象实例都存放在这里。之所以说几乎,是因为:在java虚拟机规范中描述:所有的对象实例都要在堆上分配。但是由于JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术将会导致"所有的对象实例都要在堆上分配"不那么绝对了。

Java堆是垃圾收集管理的主要战场。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的 磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。(通过-Xmx和-Xms控制)

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区(Method Area)

方法区存储的是类的一些基本信息,如类的常量池、静态变量,即时编译器编译后的代码等。【标准说法是:类及其父类的全限定名(java.lang.Object没有父类)、类的类型(Class or Interface)、访问修饰符(public, abstract, final)、实现的接口的全限定名的列表、常量池、字段信息、方法信息、静态变量、ClassLoader引用、Class引用】

可以这样理解,如果定义了一个类,其中有一个静态变量,那么在new这个类的时候,静态变量会放在方法区,实例的其它部分会放在Java堆;如果又new了这个类,那么方法区的静态变量不变,在Java堆里面会有一块新的内存放实例。

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池(Runtime Constant Pool)

运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用(如编译器生成的static final常量字面量,方法表中的方法名索引 描述符索引 符号引用等),这部分内容将在类加载后存放到常量池中。

运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)

我的理解:运行时动态扩展常量池可以把一些不经常变化的数据缓存起来,和String的缓存池一样,不知道String的缓存是不是存放在这里。

直接内存(Direct Memory)

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

对象访问

对象访问会涉及到Java栈、Java堆、方法区这三个内存区域。

如下面这句代码:

Object objectRef = new Object();

假设这句代码出现在方法体中,"Object objectRef" 这部分将会反映到Java栈的本地变量中,作为一个reference类型数据出现。而"new Object()"这部分将会反映到Java堆中,形成一块存储Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定。另外,在java堆中还必须包括能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些数据类型存储在方法区中。

reference类型在java虚拟机规范里面只规定了一个指向对象的引用地址,并没有定义这个引用应该通过那种方式去定位,访问到java堆中的对象位置,因此不同的虚拟机实现的访问方式可能不同,主流的方式有两种:使用句柄和直接指针。

句柄访问方式:java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

深入理解JAVA虚拟机 自动内存管理机制

指针访问方式:reference变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据。

深入理解JAVA虚拟机 自动内存管理机制

这两种访问对象的方式各有优势,使用句柄访问方式最大好处就是reference中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。使用指针访问方式最大好处就是速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是第二种方式(直接指针访问)。

java堆溢出

实验:

实验代码:

  1. public class OutOfMemoryTest {
  2.  
  3.    static class OOMObject{
  4.  
  5.    }
  6.    public static void main(String[] args) {
  7.       // TODO Auto-generated method stub
  8.       List list = new ArrayList();
  9.       while(true)
  10.       {
  11.          list.add(new OOMObject());
  12.       }
  13.    }
  14.  
  15. }

设置JVM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

深入理解JAVA虚拟机 自动内存管理机制

错误信息:

java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid568.hprof ...

Heap dump file created [27885730 bytes in 0.090 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at java.util.Arrays.copyOf(Unknown Source)

at java.util.Arrays.copyOf(Unknown Source)

at java.util.ArrayList.grow(Unknown Source)

at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)

at java.util.ArrayList.ensureCapacityInternal(Unknown Source)

at java.util.ArrayList.add(Unknown Source)

at jvm.OutOfMemoryTest.main(OutOfMemoryTest.java:16)

使用MemoryAnalyzer查看dump:

深入理解JAVA虚拟机 自动内存管理机制

OOM(OutOfMemory)是最长江的内存溢出。

java.lang.OutOfMemoryError: Java heap space代表堆内存溢出。

这种情况,一般要确认是内存泄漏(Memory Leak)还是内存溢出(Memory OverFlow)。

内存泄漏:对象的使用导致没有被GC。

内存溢出:内存不足。需要调整JVM参数。

虚拟机栈和本地方法溢出

在Hotspot中不区分虚拟机栈和本地方法栈,所以调整他们两个的参数都是-Xss。

经过我的实验,我发现,当由于方法嵌套循环调用导致的溢出,只会跟-Xss的配置有关,没有线程请求的栈深度(个人理解:方法调用的深度)大于虚拟机所允许的深度这种说法。至少我将-Xss调整到100M也没有发现。

测试代码:

  1. package jvm;
  2.  
  3. public class *Test {
  4.  
  5.    private long stackLenth = 0;
  6.    public static void main(String[] args) {
  7.       // TODO Auto-generated method stub
  8.       *Test s = new *Test();
  9.       try {
  10.          s.testStack();
  11.       } catch (Throwable e) {
  12.          System.out.println(s.stackLenth);
  13.          e.printStackTrace();
  14.       }
  15.  
  16.    }
  17.    private void testStack()
  18.    {
  19.       long l = 999999999999999L;
  20.       stackLenth ++;
  21.       testStack();
  22.    }
  23.  
  24. }

在10M的时候,报错信息:

java.lang.*Error

at jvm.*Test.testStack(*Test.java:21)

在100M的时候,报错信息:

java.lang.*Error

at jvm.*Test.testStack(*Test.java:21)

ps:即使我把内存调整到2G,stackLenth还在增加。证明不是因为达到了"线程请求的栈深度"而导致的*Error。

作者的结论:

在单线程模式下,不论是栈帧太长,还是虚拟机栈容量太小,都只能抛出*Error。

在多线程模式下,通过不断地建立线程到时可以抛出OutOfMemoryError异常。

运行时内存池异常和方法区内存异常

运行时常量池存储在方法区中。所以他们的异常都是一样的。方法去的jvm参数如:-XX:PermSize=10M -XX:MaxPermSize=10M 注意,和上面不一样,这里是等号。

方法区的内存溢出报错为:java.lang.OutOfMemoryError: PermGen space

上一篇:在VMware Workstation上安装CentOS6.5系统步


下一篇:深入理解Java虚拟机二:垃圾收集与内存分配