虚拟机在程序执行期间会将内存划分为不同的数据区域,他们都有各自的用途。有的伴随虚拟机进程启动一直存在,有的则和用户进程同生共死;
1.程序计数器
计数器即当前线程执行字节码(编译出的class文件)的行号指示器,对于编译产生的字节码机器是不能直接执行的,还需要字节码解释器来进行和底层的沟通。对于分支,循环,跳转,异常处理等基础功能都是字节码解释器,通过改变计数器的值来选取下一条字节码指令来完成的。
由于虚拟机多线程是通过在线程之间不停切换实现的,即对于一个确定时间只会有一个线程在被执行,为了让线程被切换后可以恢复到正确的位置,每个线程都有独立的计数器,各个线程的计数器被独立储存,这种内存被称为“线程私有”的内存。
- Java方法计数器记录的是正在执行虚拟机字节码指令的地址。
- 如果正在执行的是 Native 方法,则这个计数器值为空(Undefined)。(Native方法是java通过jni调用本地C/C++库来实现,非java字节码实现,所以无法统计)
- 程序计数器是java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域
2.Java虚拟机栈
栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法正常完成还是异常完成都算作方法结束。
栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表(局部变量表)、操作数栈和指向当前方法所属的类的运行时常量池的引用。
栈帧是线程本地的私有数据,不可能在一个栈帧中引用另外一个线程的栈帧。
虚拟机栈也是线程私有的,生命周期和线程相同。当一个方法被执行时,虚拟机就会同步创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。方法的调用到执行完毕对应着栈帧在虚拟机栈的入栈(创建)到出栈(销毁)。
对于一个虚拟机栈可以存着多个栈帧,因为方法可以嵌套调用的等。对于栈帧在虚拟机栈里排列顺序有一定的顺序,可以参考数据结构中栈就是先进后出。对于我调用的方法就在我的上方。
一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。对于JVM执行引擎来说,在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法称为当前方法, 定义这个方法的类叫做当前类。
执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。
调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧成为了当前栈帧。方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧(返回给方法调用者),随后虚拟机将会丢弃此栈帧。
局部变量表(用于存放方法参数和方法内定义的局部变量)
存放编译期可知的
- 各种基本数据类型
- 对象引用(reference类型,不等同于对象本身,可以是指向对象起始地址的引用指针,代表对象的句柄(即把new出来赋值给等号左边起的别名)或者其他与对象相关的位置)
- returnAddress类型(指向一条字节码指令的地址)
关于对象的引用,在方法里声明的数组,其引用放在局部变量表,但实体放在堆里。如果方法里声明了final或者static类型,那么相应的存储在常量池和堆里。
他们在局部变量表的存储空间是局部变量槽(Slot),64位的long和double占两个槽,其他占一个。表的空间大小(大小指槽的个数,而槽是32,64bit或者更多由虚拟机决定)在编译时完成分配,进入方法在栈帧中分配的空间是确定的,因为其要储存的东西在编译时大 小就已经确定,在方法运行期间不会改变局部变量表的大小。
局部变量必须显示赋值。
构造方法和实例方法在索引为0 的地方保存this。
对于出了作用域的变量,销毁后slot会被下面接着声明的变量重复使用。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量-1
异常:
- StackOverflowError异常
当如果线程请求的栈深度大于虚拟机允许的深度
- OutOfMemoryError异常
如果虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存
栈帧中其他信息介绍:
zhuanlan.zhihu.com/p/147341385
动态链接
静态方法,final方法,私有方法,实例构造器,父类方法时非虚方法。
栈顶缓存技术:将栈顶元素缓存到CPU寄存器中,降低对内存的读写。
3.本地方法栈
与虚拟机栈非常相似,只是虚拟机栈对应java方法,本地方法栈对应本地(Native)方法。
4.Java堆
Java堆是被所有线程共享的内存区域,在虚拟机启动时创建,他的唯一目的就是存储对象,规范中描述过:“所有的对象实例以及数组都应该在堆上分配”。(数组也是对象)堆是垃圾收集器管理的区域。
所有线程共享的堆可以划分出多个线程私有的分配缓存区,以提高对象分配时的效率。不论如何划分都不会改变堆的共性:
无论在哪个区域,存储的只能是对象的实例,将堆细分只是为了更好的回收内存,或者更快的分配内存。
堆可以是物理上不连续的内存空间,但逻辑上应被视为连续的。对于大对象(数组对象),为了简单高效还是可能要求连续的内存空间。
堆可以被固定大小,也可以是可扩展的。如果堆中没有内存完成实例分配,并且也无法扩展时,会抛出OutOfMemoryError异常。
默认比例 伊甸园:s1:s2=8:1:1 新生代:老年代=1:2
From to 区不会主动GC,只会在伊甸园区满的时候,一起GC
5.方法区
方法区也是线程共享的区域,用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。(域,方法)
在JDK7的HotSpot,已经把放在永久代的字符串常量池(保证创建的大量字符串能及时被回收),静态变量等移到堆中。在JDK8时,废弃永久代的概念,用在本地内存中实现的元空间来替代,把JDK7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
方法区和堆一样不需要连续的空间和可以选择固定空间或可扩展,甚至可以选择不实现垃圾收集。该区域的内存回收主要针对常量池的回收和对类型的卸载,但回收效果难令人满意。
当方法区无法满足新的内存分配需求时,会抛出OutOfMemoryError异常。
6.运行时常量池
运行时常量池是方法区的一部分(方法区存储的就包含常量)。Class文件除了有类的版本,字段,方法,接口等描述信息外,还有一个就是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分将在类加载后存放到方法区的运行时常量池。(运行时常量池即根据常量池表构建出来的)
运行时常量池是具备动态性的,即运行期间也可以将新的常量放入池中,如:
String.intern()
检查字符串常量池中是否存在String并返回池里的字符串引用;若池中不存在,则将其加入池中,并返回其引用。 这样做主要是为了避免在堆中不断地创建新的字符串对象
String a = "ddddd";
String b = new String("ddddd");
String c = b.intern();
String d = b;
System.out.println(b == a);
System.out.println(b==c);
System.out.println(a==c);
System.out.println(d==a);
System.out.println(d==b);
a不等于b,因为他俩一个直接指向常量池,一个间接。b不等于c因为intern返回的是直接对常量池的引用。所以a会等于c。而直接把b赋值给d只是给了b中存的引用,因此d不等于a,但d等于b。
当运行时常量池无法申请到足够内存时,会抛出OutOfMemoryError异常。
7.直接内存
直接内存并不是虚拟机运行时数据区的一部分。在JDK1.4中加入了NOI(New Input/Output)类,引入了基于通道和缓冲区的I/O方式,它可以通过Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆中来回复制数据。
直接内存不受Java堆的限制,但受本机总内存及处理器寻址空间的限制。当配置虚拟机参数时忽略直接内存,会因为动态扩展出现OutOfMemoryError异常。