程序计数器
作用: 字节码解释器工作时通过修改计数器的值来选择下一条需要执行的字节码指令,因为虚拟机是通过多线程来切换并分配处理器执行时间的方式来执行的,因此 为了线程切换后能恢复到正确的位置,每一个线程需要有一个独立的程序计数器,各个计数器互不影响,在线程内独立存储。
虚拟机栈
和程序计数器一样,虚拟机栈也是线程私有的,其生命周期与线程一样每个方法在执行的同时都会创建一个栈帧,栈帧用于存储 局部变量表,操作数栈,动态链接 方法出口等等。每个方法从开始到完结 就对应了栈帧在虚拟机栈的入栈与出栈过程。
局部变量表所需要的内存空间在编译器完成分配,是完全确定的 ,运行时不会改变局部变量表的大小。
虚拟机中的两种异常:StackOverFlowError和OutOfMemoryError。
StackOverFlowError:如果线程请求栈帧的深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常。
OutOfMemoryError: 如果虚拟机栈客园扩展(大部分虚拟机都可以动态扩展),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
2种异常的理解:StackOverFlowError 就是递归的太深了 比如 自己调用自己
public class StackOverflowTest { public static void main(String[] args){ method(); } private static void method() { method(); } }
OutOfMemoryError:表示创建对象的速度快于JVM回收空间,就会发生这个异常,比如死循环创建对象
public class OutOfMemoryTest { public static void main(String[] args){ List<Object> list = new ArrayList<Object>(); while(true){ int[] index = new int[20_0000_0000]; list.add(index); } } }
本地方法栈
本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法区是本地方法运行的内存模型,本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表等等。
堆
堆是java虚拟机所管理内存最大的一块,是被所有线程共享的内存区域,在虚拟机启动的时候创建,用于存放对象实例和数组。
根据分代算法可以分为新生代和老年代,再细致一点 可以分为Eden、From Survior、To Survior 空间。
堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的(-Xmx,Xms来控制),因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError异常。
方法区
方法区与堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量 静态变量,即时编译器编译后的代码等。
方法区的信息一般是永久存在的 因此我们也称之为永久代,因为需要长期存在 所有GC在此区域回收效率较低,另外这里还允许不实现垃圾回收。
运行时常量池
方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码。其中常量存储在运行时常量池中,常量池是方法区的一部分。
当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。 当常量池无法申请到足够的内存时也会抛出OutOfMemoryError异常。
当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。
直接内存
直接内存是除Java虚拟机之外的内存,但也有可能被Java使用,在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率,直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OOM异常。