Java内存区域(上)
0.引
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,墙里的人却想出来。------《深入理解Java虚拟机》
本章将对Java虚拟机内存的各个区域进行学习,文章总结自《深入理解Java虚拟机》一书,与黑马Jvm课程,与部分大学教材。
1.程序计数器
什么是程序计数器与其作用?
学过操作系统或计组的应该知道,程序计数器可以用来确定下一条指令的地址,所以通常又称为指令计数器。
程序开始执行前,会将它的起始地址,即程序的一条指令所在的内存单元地址送入PC,因此程序计数器(PC)的内容即是从内存提取的第一条指令的地址。
而当执行指令时,CPU将自动修改PC的内容,即每执行一条指令PC增加一个量,这个量等于指令所含的字节数,以便使其保持的总是将要执行的下一条指令的地址。
那么在JVM中也是同样,会通过这个程序计数器来选取下一条需要执行的字节码指令。
我们所知的分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。
特点
线程私有。
学过计组我们都知道。CPU会为每个线程分配时间片,当当前线程的时间片使用完以后,CPU就会去执行另一个线程。所以CPU会不停的切换线程,所以在切换时, 程序计数器会记录线程下一行指令的地址,以便于接着往下执行。 A线程的PC就记录了A线程执行到哪儿了,B线程的PC就记录了B线程执行到哪儿了。
Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的。
所以,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
不会内存溢出
程序计数器所需要存储的内容仅仅就是下一个需要待执行的指令的地址 ,所以怎么会内存溢出( ̄_ ̄)!!
如果线程执行的是一个Java方法,这个程序计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是本地(Native)方法,这个计数器的值则应为空(Undefined)。
为什么执行Native方法会为空,因为这个计数器是JVM中的概念。。。。。。( ̄_ ̄)!!
此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
2.Java虚拟机栈
定义与一些特点
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数帧、动态连接、方法出口等信息。
每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 。
“栈”通常情况下就是指的虚拟机栈,或者更多情况下只是指虚拟机栈中的局部变量表部分。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址)
这类数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。(此大小指Slot的数量)。
Java虚拟机栈容量可以是固定大小的,也可以是动态变化(动态扩展)的。
如果是固定的,线程请求的栈深度大于虚拟机所允许的深度,那么会抛出StackOverflowError异常。
如果是动态扩展的,当栈扩展无法申请到足够的内存会抛出OOM异常(java.lang.OutOfMemoryError)
HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机可以。所以在HotSopt虚拟机上是不会由于虚拟机栈无法扩展而导致OOM异常。
只要线程申请栈空间成功了就不会有OOM,但如果申请时就失败,才会出现OOM。 否则无论是栈帧太大,还是栈容量太小,都只会出现StackOveflowError
比如调小的栈内存容量后,递归调用方法,不断插入栈帧,导致超出所允许的栈深度,出现的是StackOverflowError。 再比如对某一个方法定义一大堆本地变量,导致栈帧中的局部变量表增长,也只会出现StackOverflowError。
对于一个操作系统而言,操作系统分配给每个进行的内存是有限制的,譬如32位的Win系统的单个进程的内存限制为2GB。HotSpot提供的参数(-Xms -Xmx)能够限制堆的内存大小,程序计数器的所占内存大小可以忽略不计,方法区(注意只有JDK7以前的永久代可以通过JVM参数做限制)的内存大小可以通过参数(-XX:PermSize 和 -XX:MaxPermSize)来做限制。那么2GB减去方法区的内存大小,减去堆内存大小,减去直接内存和JVM本身消费的内存之后,剩下的就是栈所能用的内存了。
如果每个线程分配到的栈内存越大,那么可以建立的线程数量自然就越少,就会出现线程申请栈空间失败的情况导致OOM
————————————————
版权声明:本文为CSDN博主「只肉不吃饭」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41155436/article/details/106466770
几个问题:
- 垃圾回收是否涉及栈内存?
- 不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
- 栈内存的分配越大越好吗?
- 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
- 方法内的局部变量是否是线程安全的?
- 如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
- 如果如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
3.本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常的相似。区别只是,本地方法栈是为虚拟机使用到的本地(Native)方法服务。
《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由的实现它,甚至有的Java虚拟机(比如Hot-Spot虚拟机)直接就将本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈也会在栈深度移除或栈扩展失败时,抛出StackOveflowError 和OutOfMemoryError异常。