这里以JVM1.8为主,介绍JVM的内存模型,如图所示。

JVM1.8内存模型

下面就各个组成部分进行说明。

堆heap

基本上Java对象实例都放在Java堆heap中,是垃圾收集器GC管理的主要区域。当我们new一个对象或者数组的时候都会在heap堆中申请内存空间,当内存空间不够的时候则会报OOM(OutOfMemoryError )异常。堆空间的内存是线程共享的。

堆heap中包含新生代老年代,新生代占1/3堆区域,老年代占2/3堆区域,新生代又分为Eden(伊甸园)Survivor(幸存者)两个区。如图所示:

JVM1.8 堆

其中,绿色部分为Eden(伊甸园),黄色部分为Survivor(幸存者)。

当我们new一个对象的时候首先被分配到Eden(伊甸园区),新生代回收(Minor GC)后,

如果对象还存活,则将它放到Survivor(幸存者区),之后每次执行Minor GC,

如果对象还存活,就将该对象的年龄加1,当对象的年龄超过15(默认值),则进入老年代。

更多GC内容请参考 JVM 中 MinorGC、MajorGC、FullGC 。

上面的图示中代表的是G1垃圾收集器之前的堆分布图,新生代和老年代的内存分布是连续的,而G1收集器的堆内存结构是不连续的,如下图所示。

G1收集器堆内存分布

详情请参考 JVM垃圾收集器中的G1收集器。

这里给出堆异常例子,帮助理解Java中堆的概念

List<byte[]> list = new ArrayList<>();
while (true) {
	list.add(new byte[1024 * 1024]); // 每次增加一个1M大小的数组对象
}

OOM异常

虚拟机栈

  • JVM在运行的时候,会为每个线程分配一个虚拟机栈,虚拟机栈中存在很多栈帧,栈帧就是线程在执行方法的时候,调用一个方法,将这个方法压入到栈帧中,方法执行结束之后弹出栈
  • 在栈帧中存在有:方法的局部变量表,方法的入口以及出口
  • 多个栈帧不断的进行入栈,出栈的操作
  • 当调用的方法过多的时候,会出现 StackOverFlow 的异常

虚拟机栈帧如图所示:

JVM栈帧

为了更好的理解虚拟机栈的概念,给出虚拟机栈异常例子。

public class StackError {

    private static int count = 1;

    public static void main(String[] args) {
        System.out.println(count++);
        main(args);
    }
}
这是一个不断调用自身方法的例子,当count=9831时,报StackOverFlowError异常,如图所示:

虚拟机栈异常

本地方法栈

本地方法栈和虚拟机栈作用一样,不一样的是,本地方法栈里面进栈的是本地方法,也就是 native 方法,虚拟机栈是开发人员开发的方法。

本地方法就是java代码里面写的native方法,它没有方法体。是为了调用C/C++代码而写的。在JNI程序里面使用。

程序计数器

程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

我们举一个简单的例子来说明什么是程序计数器。

public class HelloWorld {

    public static void main(String[] args) {
        int i=1;
        int j=2;
        int k = i+j;
        System.out.println(k);
    }
}

将上面的程序保存为HelloWorld.java,使用javac编译java文件

javac HelloWorld.java
生成class文件后,使用javap命令查看class文件格式,如图所示:

JVM1.8内存模型

可以看到红色标注部分的指令,它便是程序计数器,它会创建相应的程序计数器,0,1,2,3,4,5......

运行时常量池

Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。即运行期间产生的常量放在运行时常量池中。

类加载后,常量池中的数据会在运行时常量池中存放.
这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)

Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。

例子

String s1 = "yxjc123";//静态常量池
String s = "123";
String s2 = "yxjc"+s;//运行时确定的常量池
System.out.println(s1==s2);

输出 false,即s1不等于s2。

在上面的例子s1是存放在静态常量池中的,s2是运行时确定的,属于运行时常量池,所以s1不等于s2。

静态常量池

是指在编译期间产生的。