深入理解java虚拟机(1)-java内存区域


该篇博客将从概念上讲解Java虚拟机内存的各个区域,讲解这些区域的作用,服务对象以及可能产生的问题,这也是我们为我们后面了解Java内存管理做的一些准备。


Java虚拟机在执行Java程序的过程中会吧它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间,话不多说,直接看下图,就是这些区域。可能之前你们只听说过栈,堆和常量区,其实这些区域只不过是我们常用的几个区而已,下面将对每个区做简单介绍。
Java虚拟机运行时数据区

1.程序计数器

大家对这块区域应该挺陌生的可能从来就没听说过,程序计数器是一块较小的内存空间,他的作用就可以看做是当前线程所执行字节码的行号指示器,用来记录我们程序执行到哪里了,举个栗子:我们知道很多时候我们的程序都是多线程程序,在整个应用的运行中,线程是不断切换的,线程A执行到方法A执行到一半,线程B又开始执行方法B,那么当线程A又获得资源开始执行自己的方法时,他是怎么知道自己要从哪里开始执行呢,所以也就引入了程序计数器,它记录了线程A之前执行字节码的位置,同样也会有相应的计数器记录线程B执行的位置。通过这个例子你应该知道程序计数器是干嘛的了,而且也能看出,每个线程都需要有一个独立的程序计数器,各条线程的计数器互不影响,独立存储,我们称这类区域为“线程私有”的内存。
如果线程正在执行的是一个java方法,这个计数器记录的正在执行的虚拟机字节码指令的地址,如果正在执行的是Native方法(中文名:本地方法,解释一下,因为后面还有个本地方法栈,本地方法是指那些调用非Java代码实现的接口,它是一个接口,它的真正实现是靠其他语言实现的,java虚拟机中就有很多方法并不是用java语言实现的,因为有些方法可能用其他语言更高效,我们称这类方法叫做本地方法)这个计数器值则为空。
(敲黑板了)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,后面你讲看到的所有区都是可以出现OutOfMemoryError的。

2.Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型(Java方法执行的时候密切相关):每个方法被执行的时候都会同时创建栈帧,用于存储操作数栈,动态链接,方法出口,局部变量表(这也就是我们平常常说的栈,想不到吧,原来只是Java虚拟机栈中的一个局部变量表)等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程,所以通过这句话,你应该要认识到,该区域也应该是线程私有的,因为方法时某个线程执行的。
必须要知道的是局部变量表存放了编译器可知的各种基本数据类型,和各类对象的引用。其中64位长度long和double类型的数据会占用2个局部变量空间,其余的数据类型会占用1个局部变量空间。
然后我们就来讨论讨论关于内存溢出的问题,这里先提一下,后面还是会提到的,在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;但是一般虚拟机都是可以动态扩展的,但是在扩展的时候如果无法申请到足够的内存时就会抛出OutOfMemoryError异常。

3.本地方法栈

前面我已经简单的介绍了什么是本地方法了,本地方法栈和虚拟机栈所发挥的作是非常相似的,其区别不过是虚拟机栈是为虚拟机执行Java方法服务,而本地方法则是为虚拟机使用到的Native方法服务,本地方法栈区域也会抛出StackOverFlowError和OutOfMemoryError异常。肯定也是线程私有的。

4.Java堆

Java堆是Java虚拟机所管理的内存中最大的一块,是在虚拟机启动时就创建的,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存的,后面会介绍到Java内存回收算法采用的是分代收集算法,所以将Java堆还可以细分为:新生代和老年代,在细分就是Eden空间,From Survivor空间和To Survivor空间(知道有这回事就行,后面会详细介绍里面的每个空间)如果堆中没有内存完成实例分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常。很显然这是哥哥线程共享的内存区域,因为各个现在在操作对象时都是需要在堆上进行的。

5.方法区

方法区和Java堆一样,是各个线程共享的内存区域,它用于存储已被徐弩机加载的类信息、常量、静态变量、即及时编译器编译后的代码等数据。这里只要记住方发渠里面有啥就行了,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

基本上图上的5个区都介绍完了,这里只需要知道每个区都是存放什么的就行,后面再虚拟机内存回收与分配时会详细的设计到每个区的知识点。后面还有2个区域是我们以后可能会遇到的,这里也就提一下。
(1)运行时常量池
运行时常量池是方法区的一部分,主要就是用来存放编译器存放的字面量和符号引用。
运行时常量池相对于Class文件常量池的另一个重要特征就是具备动态性,Java语言不要求常量一定只能在编译器产生,也就是并非预植入Class文件中常量池的内容才能进入方法区运行时,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的便是String类的intern()方法(该方法,在执行的时候会判断String常量池中是否有该对象,如果已经存在则直接返回该对象,如果没有则创建,这里jdk6和jdk7还有点不一样,有兴趣的可以去了解下这个方法)
(2)直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。
在jdk1.4引入了NIO(New Input/output)类,引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中里面的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著的提高性能,因为避免了在Java堆和Native堆中来回复制数据。
服务器管理员配置虚拟机参数时,一般会根据世界的内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的的总和大于物理内存限制,从而导致扩展时出现OutOfMemoryError异常。

6.举个栗子复习下我们的内存区域(对象访问)

Object obj = new Object();
假设这段代码在方法体中,当线程执行到该代码时,内存中是怎样的一个分配形式呢,首先 Object obj将会反映到Java栈的本地变量表中,作为一个reference类型数据出现,而 new Object()这部分语义将会反映到Java堆中,形成一块存储了Objcet类型所有实例数据值的结构化内存,另外,在Java堆中还必须包含能查到此对象类型数据(如对象类型,父类,实现的接口,方法等)的地址信息,而这些类型数据是存储在方法区中的。

前面提到了,通过引用去指向对象,但是没有提到引用通过什么方式去指向对象,现在主流的方式有两种,一种是句柄式,一种是直接指针。
1.句柄式:句柄式就相当于找个中介句柄(位置在堆中的句柄池中)而已,引用存储的是句柄的地址,句柄再存储对象实例数据和对象类型数据的地址。这样引用就可以找到对象所有的信息了。
2.直接指针访问方式,引用直接存储对象地址(实例数据地址),然后实例数据里面再存放类型数据的指针,指向类型数据。这样也完成了引用找到对象的所有信息。
两种方式各有优势,使用句柄访问的方式最大的好处就是reference中存储的是稳定的句柄地址,在对象移动的时候只需要改变句柄中的实例数据指针,而本身的reference不需要改变。直接指针的好处就是速度过,因为避免的中间环节嘛。

前面在介绍各个区的时候,也把每个区可能产生内存溢出的原因说了下,所以如果有人问道为什么会产生内存溢出,应该知道怎么答了,我的理解就是先大概说就是,虚拟机在执行程序的过程中请求不到足够的内存会导致内存溢出,再细化的话就是每个区里面是如何导致内存溢出的了。

此篇也是为下一篇介绍java虚拟机的另一个重要领域“垃圾收集器与内存分配策略打下基础”,所以想了解下一章节必须要先了解好Java的内存区域划分。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值