Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。    -----《深入理解Java虚拟机》

对于Java开发者来说,在虚拟机自动内存管理机制的帮助下,不在需要为每一个new操作去写配对的delete/free代码,极大的简化了开发流程。但把内存控制的权利交给虚拟机,一旦出现了内存溢出和泄露的问题,如果不了解虚拟机的工作原理,排查问题所在会十分困难。

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,根据《Java虚拟机规范(Java SE 7 版)》的规定,Java虚拟机所管理的内存划分为以下几个模块:

JDK1.6 JVM内存结构

1.程序计数器

定义:程序计数器是一块较小的内存空间,可以看作是当前所执行的字节码的行号指示器。

作用:1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

特点:1.这类内存区域为“线程私有”的内存

2.如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址

3.如果正在执行的是本地方法,计数器值则为空

4.是唯一一个不会出现OutOfMemoryError的内存区域

5.生命周期随着线程的创建而创建,随着线程的结束而死亡


2.Java虚拟机栈

定义:虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数组、动态链表、方法出口等信息

特点:1.局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且,局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外,在方法运行的过程中局部变量表的大小是不会发生改变的。

2.Java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError。当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。(StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。 )

3.Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。


3.本地方法栈

定义:与Java虚拟机栈的用途类似,本地方法栈为虚拟机所用到的本地方法服务

特点:方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会抛出StackOverFlowError和OutOfMemoryError异常。


4.Java堆

定义:Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

特点:1.Java堆是垃圾收集器管理的主要区域,因此有些时候被叫做“GC堆”

2.Java堆可以细分为新生代和老年代(新生代可以再细分为Eden空间、From Survivor空间、To Survivor空间等)

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

4.Java堆可以处于物理上不连续的空间内存空间中,只要逻辑上是连续的即可,就像磁盘空间一样


5.方法区

定义:与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据

特点:1.(HotSpot中的永久代)方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为老年代。

2.和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以先选择不实现垃圾回收

3.方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载。

JDK1.8中,JVM内存已经不存在方法区了,方法区直接被放在本地内存区域,此区域被称为元空间,方法区与堆不再相连。

6.运行时常量池

定义:运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

特点:当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也储存在运行时常量池中。

JDK1.6运行时常量池位于方法区中,由于方法区中内存空间固定,可能会导致内存溢出的问题,在JDK1.7中将常量池移到了堆中。

举个栗子来理解一下JDK1.6与JDK1.7运行常量池的不同:JVM创建String对象时首先会去常量池中查找,若存在,则直接返回返回值的引用;若不存在,则先在常量池先创建该String对象,然后在JDK1.6中会将该String对象拷贝一份到堆中,在JDK1.7中会将常量池中该String对象的引用拷贝一份到堆中。

借网上的几张图来理解JDK1.6,JDK1.7,JDK1.8的JVM内存结构的变化(主要是运行时常量池和方法区发生了变化):


7.直接内存

定义:直接内存不属于虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但也有可能被Java使用

作用:在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。

特点:在系统内存不足时也会抛出OutOfMemoryError异常


总结:

1.程序计数器、Java虚拟机栈、本地方法栈是线程私有的,并且他们的生命周期和所属的线程一样。堆、方法区是线程共享的,在Java虚拟机中只有一个堆、一个方法栈。并在JVM启动的时候就创建,JVM停止才销毁。

2.JVM垃圾回收的主要区域是堆