Java虚拟机内存区域分为五部分:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。其中程序计数器、Java虚拟机栈、本地方法栈属于线程私有内存区,其生命周期与线程相同,随线程的产生而产生,随线程的消亡而消亡。这几个区域的内存在方法或线程结束时,自然就跟着回收了。因此这三个区域的内存分配和回收具有确定性,不需要考虑内存回收的问题。而Java堆和方法区属于线程共享内存区,所有线程都可以访问。只有在程序运行期间才能知道创建哪些对象,因此这两个区域的内存分配和回收都是动态的,也是垃圾收集器收集的主要区域。
下面分别介绍这五个区域。
1) 程序计数器
程序计数器是一块较小的内存空间,可以当做线程执行的字节码行号指示器。字节码解释器工作的时候就是通过改变程序计数器的值,来选取下一条所要执行的字节码指令,分支、跳转、循环、异常处理、线程恢复等都需要依赖程序计数器来完成。程序计数器是Java虚拟机中唯一不会发生内存溢出(OutOfMemoryError)的区域。
Java虚拟机的多线程是通过线程间的轮流切换并分配处理器执行时间的方式来完成的,任意时刻一个处理器只会执行一条线程中的指令。为了使线程切换后能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各线程之间独立存储、互不影响,因此是线程私有内存区。
2) Java虚拟机栈
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行结束都伴随着一个栈帧在虚拟机栈中从入栈到出栈的过程。
Java虚拟机栈也是线程私有内存区,生命周期与线程相同。基于栈的内存区域都可能发生两种异常:栈溢出和内存溢出。如果线程请求栈的深度大于虚拟机栈所允许的最大深度,并且无法扩展时,将会发生栈溢出(StackOverflowError)异常;如果可以扩展,在扩展时无法申请到足够的内存,将会发生栈溢出(OutOfMemoryError)异常。
局部变量表用于存放方法参数和方法内部定义的局部变量,其数据类型是编译期可知的各种基本数据类型、对象引用类型(reference)和返回(returnAddress)类型(它指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,即在Java程序被编译成Class文件时,就确定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
3) 本地方法栈
本地方法栈与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务(即程序员自己写的方法),而本地方法栈则为使用到的本地操作系统(Native)方法服务。本地方法栈也是基于栈,因此也会发生栈溢出或内存溢出异常。
4) Java堆
Java堆是虚拟机所管理的最大一块内存,是所有线程共享的一块内存区域,在虚拟机启动时创建。Java堆用来存放对象实例,几乎所有的对象实例和数组都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。在实现时,既可以是固定大小的,也可以是可扩展的,目前主流虚拟机都是可扩展的。如果在堆中没有足够的内存可分配,且无法扩展时,将会抛出内存溢出异常。
从垃圾收集的角度来看,Java堆还可以细分为新生代和老年代。新生代包括Eden、Survivor from、Survivor to,详情请见下面下面一章。
5) 方法区
方法区也是线程共享的内存区域,用于存储被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样,方法区同样可以处于物理上不连续的内存空间中,只要逻辑上连续即可。在实现时,既可以是固定大小的,也可以是可扩展的。方法区可以不进行垃圾收集,相对而言,方法区的垃圾收集行为很少,主要是对常量池的回收和对类型的卸载。当方法区没有足够的内存完成分配时,会发生内存溢出异常。
平时所说的“永久代”(PermGen space)指的是方法区。
“运行时常量池”是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。运行时常量池具有动态性,常量既可以在编译期产生并进入常量池,也可以在运行期间进入常量池,常见的是String类的intern()方法。运行时常量池是方法区的一部分,因此也会发生内存溢出异常。
转载请注明出处 https://www.cnblogs.com/Y-oung/p/9762682.html
工作、学习、交流或有任何疑问,请联系邮箱:[email protected]
原文地址:https://www.cnblogs.com/Y-oung/p/9762682.html