引言

JVM内存模型分为两部分私有内存和共享内存;如图所示,堆和方法区是所有线程共有的,而虚拟栈、本地方法栈、程序计数器则是线程私有的。接下来我们来一一分析不同区域的作用。

堆内存

堆内存是所有线程共有的,可分为两部分:年轻代和老年代。下图中的Perm代表的永久代,但是永久代并不属于堆内存中的一部分,同时JDK1.8之后永久代被移除了。

GC(垃圾回收器)对年轻中的对象进行回收被称为Minor GC,用通俗一点的话说年轻代就是用来存放年轻的对象,年轻对象是什么意思呢?年轻对象可以简单的理解为没有经历多次垃圾回收的对象,如果一个对象经历过一定次数的Minor GC,JVM一般就会将这个对象放入到年轻代,而JVM对年老代对象回收则成为Major GC。

如上图所示,年轻代中还可以细分为三个部分,我们需要重点关注这几点:

1、 大部分对象刚创建的时候,JVM将其分布到Eden区域。

2、当Eden区域中的对象达到一定的数目的时候,就会进行Minor GC,经历这次垃圾回收后所有存活的对象都会进入两个 Suvivor Place中的一个。

3、同一时刻两个Suvivor Place,即S0和S1中总有一个总是空的。

4、年轻代中的对象经历过了多次的垃圾回收就会转移到年老代中。

5、当申请不到空间时会抛出OutOfMemoryError.下面我们简单模拟一个内存溢出的情况:

1
2
3
4
5
6
7
8
9
10
11
12
public class HeapOOM {
static class OOMObject{

}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}

下面是执行的情况:

1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at jvm.HeapOOM.main(HeapOOM.java:17)

堆内存是我们平时在生产环境中进行性能调优中一个非常重要的部分,以下拓展补充几个常见的性能调优参数:

  • PretenureSizeThreshold:直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代中分配。
  • MaxTenuringThreshold:晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC之后,年龄会加1,当超过这个参数值时就进入老年代。
  • UseAdaptiveSizePolicy:动态调整Java堆中各个区域的大小以及进入年老代的年龄。
  • SurivorRatio:新生代Eden区域与Survivor区域的容量比例,默认为8,代表Eden: Suvivor = 8:1。
  • NewRatio:设置新生代(包括Eden和两个Survivor区)与老年代的比值(除去持久代),设置为3,则新生代与年老代所占比值为1:3,新生代占整个堆栈的1/4
  • Xmx:设置JVM堆最大内存
  • Xms:设置JVM堆初始化内存
  • Xmn:参数设置年轻代内存

方法区

方法区与Java堆一样,是所有线程共享的区域,它用于存储已被虚拟机加载的类信息、常量、静态常量,即时编译(JIT)后的代码等数据。

对于JDK1.8之前的HotSpot虚拟机而言,很多人经常将方法区称为我们上图中所描述的永久代,实际上两者并不等价,因为这仅仅是HotSpot的设计团队选择利用永久代来实现方法区而言。同时对于其他虚拟机比如IBM J9是不存在永久代概念的。

程序计数器

JVM中程序计数器和计算机组成原理中提到的程序计数器PC概念类似,线程私有,用来记录执行的字节码位置。CPU的占有时间是以分片的形式分配给每个不同线程的,从操作系统的角度讲,在不同线程之间切换的时候就是依赖程序计数器来记录上一次线程所执行到具体的代码的行数,在JVM就是字节码。

JAVA虚拟机栈

与程序计数器一样,JAVA虚拟机栈也是线程私有的,用通俗的话将它就是我们常常听到堆栈中的那个“栈”内存。虚拟机栈描述的是JAVA方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表(局部变量表需要的内存,在编译期间就确定所在方法运行期间不会改变大小),操作数栈、动态链接、方法出口等信息。每一个方法从调用至出栈的过程,就是栈帧在虚拟机中从入栈到出栈的过程。

本地方法栈

本地方法栈和JAVA虚拟机栈类似,只不过是为JVM执行Native方法服务。

参考资料

  1. 深入理解JAVA虚拟机