JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。根据《Java虚拟机规范》,JVM包括下列几个运行时数据区域,如下图所示:
其中红色部分是线程私有的,即每个线程各自都有自己的一份。绿色部分是各个线程共享的。
1.PC寄存器(The pc Register)
(1)每一个Java线程都有一个PC寄存器。
(2)PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。
(3)此内存区域是唯一一个在JVM Spec中没有规定任何OutOfMemoryError情况的区域。
2.JVM栈(Java Virtual Machine Stacks)
(1)JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,与程PC寄存器一样,JVM栈的生命周期也是与线程相同。
(2)JVM栈中存放的为当前线程中局部基本类型的变量(Java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
(3)在JVM Spec中对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果JVM栈可以动态扩展(JVM Spec中允许固定长度的JVM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
(4)由于JVM栈是线程私有的,因此其在内存分配上非常高效,并且当线程运行完毕后,这些内存也就被自动回收。
3.本地方法栈(Native Method Stacks)
(1)本地方法栈与JVM栈所发挥作用是类似的,只不过JVM栈为虚拟机运行JVM原语服务,而本地方法栈是为虚拟机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun Hotspot虚拟机)直接就把本地方法栈和JVM栈合二为一。
(2)和JVM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。
4.方法区(Method Area)
(1)别名叫做Non-Heap(非堆)。
(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域。
(3)方法区域是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
(4)在Sun JDK中这块区域对应的为Permanet Generation,又称为永久代,默认为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其大小。
5.运行时常量池(Runtime Constant Pool)
(1)类似C中的符号表,存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
(2)Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。
(3)运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。
6.Java堆(Java Heap)
Java堆是被所有线程共享的,在虚拟机启动时创建。它是JVM用来存储对象实例以及数组值的区域,绝大部分的对象实例都在这里分配。在逃逸分析和标量替换优化技术出现后,并不是所有的对象实例都是在这里分配,但我们可以粗略地认为Java中所有通过new创建的对象的内存都在此分配。Heap中的对象的内存需要等待GC进行回收。
大小通过-Xms和-Xmx来控制,-Xms为JVM启动时申请的最小Heap内存(默认为物理内存的1/64但小于1G),-Xmx为JVM可申请的最大Heap内存(默认为物理内存的1/4)。默认当空余堆内存小于40%时,JVM会增大Heap的大小到-Xmx指定的大小,可通过-XX:MinHeapFreeRatio=来指定这个比例。 默认当空余堆内存大于70%时,JVM会将Heap的大小往-Xms指定的大小调整,可通过-XX:MaxHeapFreeRatio=来指定这个比例。但对于运行系统而言,为了避免频繁的Heap Size的调整,通常都会将-Xms和-Xmx的值设成一样,因此这两个用于调整比例的参数通常是没用的。
JVM将Heap分为New Generation和Old Generation(或Tenured Generation)两块来进行管理:
(1)New Generation
又称为新生代,程序中新建的对象都将分配到新生代中,新生代又由Eden Space和两块Survivor Space构成,可通过-Xmn参数来指定其大小。发生在新生代的垃圾收集动作称为Minor GC。
(2)Old Generation
又称为旧生代(老年代),用于存放程序中经过几次垃圾回收还存活的对象,例如缓存的对象等,旧生代所占用的内存大小即为-Xmx指定的大小减去-Xmn指定的大小。发生在老年代的垃圾收集动作成为Major GC/Full GC。
(3)内存分配策略
a. 对象优先在Eden分配。b. 大对象直接进入老年代。c. 长期存活的对象将进入老年代。
对堆的解释:
1)堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
2)鉴于上面的原因,Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
3)TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效,但这种方法同时也带来了两个问题,一是空间的浪费,二是对象内存的回收上仍然没法做到像Stack那么高效,同时也会增加回收时的资源的消耗,可通过在启动参数上增加-XX:+PrintTLAB来查看TLAB这块的使用情况。