程序计数器
线程执行到的字节码的行号指示器。每个线程都有一个(线程私有),原因是多线程工作的时候CPU需要在线程间切换,为了恢复线程原来执行到的位置,需要每个线程都有一个独立的计数器。
如果执行的是JAVA方法,计数器记录的是字节码的位置;如果是Native方法(即本地的、别的语言的方法),这个计数器为空。
此区域没有规定OutOfMemoryError。
JAVA虚拟机栈
线程私有,和线程共存亡。线程中一个方法被执行的时候则创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等。一个方法调用到执行完毕退出意味着一个栈帧在虚拟机中从入栈到出栈。
局部变量表
数据栈帧中的一部分。存储了编译期就已经确定了的基本数据类型、对象引用(引用类型,不等同于对象本身,可能是一个指向对象的地址的指针)和returnAddress(动态返回地址,简单说就是方法执行结束后,要返回的调用本方法的位置)类型。这些数据类型在局部变量表中用变量槽来表示。64位的long和double占两个变量槽,其余数据类型占一个。
因为局部变量表中存储的是编译期就已经确定的数据类型,所以局部变量表的空间大小在编译期完成后就决定了,不会在方法的运行中再改变。这里的改变是指局部变量表的变量槽数量不会再被改变。
两种异常:JAVA栈大于虚拟机栈允许的深度——StackOverflowError;如果栈容量能够动态扩展,而又申请不能申请到足够内存进行扩展——OutofMemoryError
本地方法栈
和JAVA栈类似,本地方法栈用于为本地方法服务。而JAVA栈为JAVA方法(.class字节码)服务。
JAVA堆
JAVA内存结构中最大的一块,所有线程共享的内存区域。唯一的作用就是存放对象实例。被垃圾收集器(GC)管理,用于对对象实例进行回收、释放内存。
从分配内存的角度看,JAVA堆被划分出多个线程私有的分配缓冲区(TLAB)用于提升对象分配时的效率。无论如何堆就是存储对象的实例,无论怎么划分只是为了更好地分配和回收内存。
目前主流的虚拟机都会实现可扩展堆,当无法申请到足够的内存进行扩展,OutofMemoryError异常。
问题:为什么要分开堆、栈两种不同的内存空间?
- 函数的调用方式:程序以main函数为入口(作为栈底)不断向其中添加指令,先调用的函数先执行,然后不断返回。栈正是最符合这种逻辑的数据结构,且地址上连续,访问速度够快。
总结:栈符合函数调用的逻辑结构。 - 变量的访问频率:人们发现变量主要是两种形式,一种内容短小(比如一个int整数),需要频繁访问,但是生命周期很短,通常只在一个方法内存活,而另一种内容可能很多(比如很长一个字符串),可能不需要太频繁的访问,但生命周期较长,通常很多个方法中可能都要用到,那么自然将这两类变量分开存储就显得比较理性,一类存储在栈区,通常是局部变量、操作符栈、函数参数传递和返回值,另一类存储在堆区,通常是较大的结构体(或者OOP中的对象)、需要反复访问的全局变量。
总结:有的变量生存周期短而访问频率高,有的生存周期长而访问频率低。有的变量需要局部多次快速访问,有的变量需要多线程共享。因此将他们分开存储比较好。 - 变量的内存分配方式:某些变量其生存周期可能很长,则会一直占用大量栈空间(第2点)。如果此时又要为一个大变量分配空间,则可能没有连续地址可用。所以需要堆。因为堆上的内存分配并不需要地址连续,可以采用空闲列表法在不同的地址上为其分配内存空间。其次,栈空间基本上在编译上就确定了每个变量的占用空间,而这对于动态增长的集合类型对象来说是不可取的。
总结:栈内存内存分配地址连续,适合快速访问。但不适合大变量的存储(连续空间不够),且不适合存储动态增长的类型。
方法区
线程共享。被加载过的类的信息,就被保存在方法区里。可以看成是将类的元数据(描述信息)存放在此。如果多个线程需要使用某个类但该类未加载,则让一个线程去加载,别的线程等待。
类的描述信息包括:类的完整名称、类的直接父类的完整名称、类实现的接口的有序列表、类的修饰符。
运行时常量池
属于方法区的一部分。一个.class文件除了类的各种描述信息,还有一项“常量池表”,用于存放编译期就已经生成的各种字面量和符号引用。在类加载后,.class文件里的常量池表会被复制到方法区的运行时常量池中。
当常量池无法申请到内存,OutOfMemoryError。
参考
《深入理解Java虚拟机》
https://blog.csdn.net/JustKian/article/details/104559654
https://my.oschina.net/ssdlinux/blog/2998513