JVM内存结构
JVM在执行.class文件的时候,需要在内存中的分配空间,我们称之为运行时数据区。
其中,虚拟机栈,本地方法栈,程序计数器是线程私有的,而方法区和堆区是线程共享的。
1.程序计数器
程序计数器,也叫PC寄存器。每个线程刚创建时,会创建一个程序计数器,生命周期跟随线程。
作用:可以看做是线程所执行的字节码的行号指示器,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的指令。(通过它JVM可以知道下一条要执行的指令)
2.虚拟机栈
虚拟机栈,也是线程私有的,随线程的创建而创建,它由一个个栈帧组成。
当每次线程调用一个方法时,都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
在Java虚拟机规范中,对这个区域规定了两种异常情况:
(1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
(2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现Out Of MemoryError
3.本地方法栈
本地方法栈是线程私有的,主要用来支持native方法,记录native方法调用的状态。可以把native 方法看成是 java 调用 非 java 代码的一个接口。主要用于允许Java 和其他语言,比如 C 语言进行交互。
4.方法区
方法区也被称作永久代,是线程共享的。
主要用来保存被JVM加载的类信息,静态变量,常量等。(其中开辟一块常量池区域用来存放常量)
很多人把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。并且在Java8中,永久代被元空间(metaspace)所代替。
5.堆区
堆区是JVM中最大的一块内存区域,是线程共享的,主要目的是用来存放数组和对象。所有的对象和数组都在堆区中进行分配空间,当申请不到空间时会抛出Out Of MemoryError。
JVM 堆区是内存溢出和垃圾回收的主要区域。现在的GC基本都采用分代收集算法,如果是分代的,那么堆也是分代的。
从下图中我们可以看到堆的分代划分
从上图中我们知道,JVM堆区可以划分为年轻代和老年代,其中年轻代又可划分为EdenSpace,两个Survivor区域
不同区域的存放的对象拥有不同的生命周期:
- 新建(New)或者短期的对象存放在Eden区域;
- 幸存的或者中期的对象将会从Eden区域拷贝到Survivor区域;
- 始终存在或者长期的对象将会从Survivor拷贝到Old Generation;
直接内存并不是虚拟机运行时数据区的一部分。
在NIO中,引入了一种基于通道和缓冲区的I/O方式,它可以使用native函数直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
总结:
堆和栈的区别
功能不同
- 栈内存用来存储局部变量和方法调用。
- 而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
共享性不同
- 栈内存是线程私有的。
- 堆内存是所有线程共有的。
异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
- 栈空间不足:java.lang.StackOverFlowError。
- 堆空间不足:java.lang.OutOfMemoryError。
空间大小
栈的空间大小远远小于堆的。