jvm之jvm运行时数据区

在这里插入图片描述
由上图可知,jvm运行时数据区由堆、虚拟机栈(方法栈),本地方法栈,程序计数器,方法区(1.8之前由永久带实现,1.8之后由元空间来实现)5部分组成,其中,堆和方法区是所有线程共享的数据区,而虚拟机栈,本地方法栈,程序技术器是线程私有的数据区域

Java堆

堆是Java虚拟机管理的内存中最大的一块,Java堆是所有线程共享的内存区域,Java堆在Java虚拟机启动时创建。Java堆用来存放对象实例,几乎所有对象都在堆中分配内存,Java虚拟机规范的描述是:所有对象实例和数组在堆(Heap)中分配内存。
同时,Java堆是垃圾收集器管理的主要区域,很多时候也叫“GC堆”。从内存回收的角度来看,现在的垃圾回收器基本都采用分代回收,因此Java堆可以细分为新生代和老年代,新生代又可以细分为Eden,From Survivor,To Survivor3块区域。从内存分配的角度来看,堆又可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。但是无论如何划分,都与存放内容无关,无论是那块区域,存放的都是对象实例,之所以进一步划分,是为了更好地进行内存回收,或者更快分配内存。
根据Java虚拟机规范的定义,Java堆可以是物理内存不连续,只要逻辑内存连续即可。其实现可以是固定大小,也可以是扩展的(通过-Xms和-Xmx来控制),当堆空间没有内存分配,堆也无法进行扩展时,会抛出OOM异常。

方法区

方法区和堆一样,是线程共享的数据区域,方法区用来存放Java虚拟机加载的类信息、常量、静态变量、即使编译器编译的代码等。方法区在jdk1.8以前由永久代来实现,其大小受到-XX:MaxPermSize参数的限制,1.8开始,jdk1.8已经不存在永久代,因此-XX:MaxPermSize参数是无效的,取而代之,使用元空间来实现永久代,元空间使用直接内存,大小只受机器内存大小限制。在1.8之前,频繁的使用string.intern()方法非常使永久代抛出OOM,因为基本类型常量池和string常量池,以及常量都在永久代分配内存,而string.intern()方法首先在方法区检查是否存在该字符串,如果存在,直接返回方法区的引用,否则将堆中的字符串对象拷贝到方法区的字符串常量池,然后返回引用,这是1.7以前的做法,1.7以后,先检查是否在字符串常量池中存在,存在则返回引用,不存在,则将堆中对象的引用拷贝到字符串常量池,注意,是堆中字符串对象的引用,不是对象。当方法区无法分配内存时,抛出OOM异常

程序计数器

程序计数器是线程私有的数据,可以看作是线程执行的指令的行号。由于Java虚拟机的多线程是通过轮流切换并分配处理器时间来执行的,在任意一个时刻,一个处理只会执行一个线程的指令,当发生切换时,为了记住线程执行的指令位置,每个线程都需要保存一个程序计数器。当执行的是Java方法时,程序计数器指向的是字节码指令的地址,执行的是native方法时,程序计数器为空。程序计数器是唯一一个在Java虚拟机规范中没有定义任何OOM异常的区域。

虚拟机栈(方法栈)

虚拟机栈也是java线程私有的,其生命周期和线程的生命周期一样。虚拟机栈描述的是java方法执行的内存模型,每个方法的执行都会在虚拟机栈中创建一个栈帧用来存储局部变量表、操作数栈、动态链接以及方法出口信息,一个方法从调用到执行完成,对应着一个栈帧在虚拟机栈中入栈和出栈。Java虚拟机规范中对虚拟机栈只定义了2种异常,如果线程申请的栈深度大于虚拟机的限制,那么会抛出StackOverflowError;如果虚拟机栈深度可以动态扩展,如果扩展时无法申请到足够的内存时,将抛出OOM
局部变量表存储了编译器可知的8大基本数据类型(boolean、byte、short、int、long、float、double、char)以及对象引用还有returnAddress(指向字节码指令的地址)。long和double占用2个局部变量表空间(slot),其他类型占用一个局部变量空间。虚拟机通过索引定位的方式访问局部变量表,索引从0开始至最大的局部变量slot数量。如果访问的是32位数据类型,索引n就代表了使用了n个局部变量slot,如果访问的是64位数据类型,则说明同时使用n和n+1两个slot。如果是非静态方法,0索引用来传递方法所属对象实例的引用,在方法里可以使用this来访问这个隐含的参数,其余参数在按照参数表顺序排列,占用从1开始的局部变量slot,参数表分配完毕后,再根据方法内部的变量和作用域来分配其余的slot。
为了节省栈帧空间,slot是可以复用的,方法体定义的变量,其作用域不一定是整个方法体,如果当前字节码计数器的值已经超过某个变量的作用域,那么这个变量所占用的slot可以给其他变量使用。这样的设计除了节省栈帧空间,也会影响垃圾回收,下面用代码来说明。

public static void main(String[] args){
	byte[] bs = new byte[1024 * 1024 * 64]
	System.gc()
}

上述代码向堆中填充了64M数据,通过分析垃圾回收日志,我们发现这64M数据并没有被回收,这很容理解,在执行System.gc()时,bs变量还在作用域中,下面我们将上面的代码修改一下

public static void main(String[] args){
	{
		byte[] bs = new byte[1024 * 1024 * 64];
	}
	System.gc();
}

我们将bs变量的作用域限制在花括号内,按理说,这次64M数据应该是会被回收的,但是通过分析垃圾回收日志,64M数据还是没能被回收掉,这是为什么呢?bs数组能否被回收,取决于局部变量的slot中是否还存在对bs数组的引用,虽然我们将变量bs的作用域限制在花括号内,但是局部变量的slot还是存在对bs数组的引用,因此无法被回收。下面我们再来修改一下代码

public static void main(String[] args){
	{
		byte[] bs = new byte[1024 * 1024 * 64];
	}
	int a = 1;
	System.gc();
}

我们仅仅加了int a = 1,这看起来并没有什么作用,但是通过分析垃圾回收日志,64M数据确实被回收了,至于原因,就是我们上面讲到的slot复用,在给a分配slot时,发现bs变量已经不在作用域内了,因此把a分配在bs的slot上,此时,局部变量表已经没有slot指向bs数组的引用了,bs数组可以被回收了。其实这里的代码可以这样写:

public static void main(String[] args){
	byte[] bs = new byte[1024 * 1024 * 64];
	bs = null;
	System.gc();
}

效果是一样的,也会被回收。其实在这里有个help gc的小妙招,当执行一个方法时,如果某个变量已经超出了作用域,或者后续不会再使用到,但是后面又有比较耗时的操作,此时将变量设置为null就显得有意义了。

本地方法栈

本地方法栈其实和虚拟机栈一样,不过是用来执行native方法的。

运行时常量池

运行时常量池是方法区的一部分。class文件中除了类版本、字段、方法、接口信息外,还有一项就是常量池,用不存储编译生成的各种字面量和符号引用。在类加载阶段会把class文件的常量池加载到运行时常量池中。运行时常量池也可以动态产生常量,比如string.intern()方法。

直接内存

直接内存不属于java虚拟机运行时数据区域,也不是java虚拟机规范定义的内存,直接内存是堆外内存。直接内存随着jdk使用nio而引入,其目的是为了io时减少堆内内存到堆外内存的复制(至于为什么Java在使用io时,会发生堆内内存到堆外内存的复制,因为直接交给cpu去拷贝的话,Java的垃圾回收机制会造成内存压缩,这时候,内存发生了偏移,而cpu是不知道的),提高Java io的性能。nio可以直接使用堆外内存,也就是说直接在堆外分配内存,然后在堆内维护这块内存的引用,当写数据时,直接写到堆外内存。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值