初步理解java的内存区域

java与c++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。 ----《深入理解java虚拟机》

在java里面,不需要为每一个new操作还去使用delete/free操作,不那么容易出现内存泄漏和溢出的问题,这是因为java程序是由java虚拟机在执行,这些内存管理机制也是控制了内存,但是如果不了解这种机制,排错的工作是很艰难的。

一:运行时的数据区域


在这里插入图片描述

我们首先看看这张图片,相信很多人都很熟悉,但是里面的原理不见得都很理解,虽说一般的开发并不需要我们了解这些东西,但是我个人觉得了解这一些基础而又高深的机制是有必要的,毕竟java以后不知道怎么变,但是这些本质性的东西还是多了解了解,像我一样入个门也行。下面我来一一介绍这些东西。

(1):程序计数器

这个比较好理解,就把它当做一种计数器就行,但是专业术语把它看做当前线程所执行的字节码的行号指示器,在java虚拟机里面,字节码解释器就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令,各种基础功能都需要它来完成。它是一块较小的内存空间
之前讲过线程,一个处理器会为线程分配一定的时间片,一旦线程创建了,就相应的创建了一个特有的程序计数器,从专业术语来讲,所谓的“当前线程所执行的字节码行号指示器”,可以理解成记录线程的执行位置,这个通俗点讲,当你在写论文时,如果你对象突然叫你出去吃饭,当你回来时,你是不是还得接着原来的地方写,当然人是自己的意识知道自己写到哪,但是虚拟机不知道呀,比如线程A在执行,时间片到了,轮到线程B,当系统再分配给线程A时间片时,是不是就需要知道这个线程执行到了哪里了,这时候,虚拟机就会读取程序计数器,通过计数器的值来找到当前线程的字节码,进而执行线程A。这就是程序计数器的作用。
特点:
1:因为每个线程独有一个程序计数器,所以是线程安全的
2:这是线程私有的内存
3:如果线程执行的是一个java方法,这个计数器记录的就是正在执行的虚拟机字节码指令地址。
4:如果正在执行的是一个Native方法,计数器的值为空,因为调用Native(本地)方法时,调用的是本地的C/C++库,相当于这些库暴露给了java,但是不是java去实现,所以不能产生相应的字节码,自然的程序计数器也无法找到相应的字节码行号,所以为空。
5:程序计数器是一个极小的内存空间,计算内存时几乎忽略不计,也不会出现OutOfMemoryError情况。

(2)java虚拟机栈

与虚拟机程序计数器一样,java虚拟机栈也是线程私有的,所以线程也是安全的,而且生命周期与线程相同,它描述的是java方法执行的内存模型,每个方法执行时会创建一个栈帧,这个栈帧里面有局部变量表,操作数栈,动态链接,方法出口等信息。在编译程序代码的时候,栈帧的局部变量表的最大容量,操作数栈的最大深度等等,都已经确定了。一个线程可能同时有很多个方法需要执行,但是对于一个线程,只有处于栈顶的栈帧才可以被执行,
在这里插入图片描述
看这张图片就显而易见了,我这里只画了一个线程,多个线程也是这样,只是线程执行的情况可能不一样而已。一个线程可能执行的链路可能很长,当前栈帧相关联的方法调用另一个方法时,就调用了另一个栈帧。
一:局部变量表:
局部变量很熟悉吧,没错,这是一个存储局部变量和方法参数的空间,在java程序被编译为class文件时,就在方法的code属性的max_locals数据项中确定了局部变量的最大容量。局部变量存储了各种基本数据类型,对象引用,returnAdress类型。局部变量的单位叫做变量槽(slot),每个slot占32位内存空间,每个基本数据类型都占一个slot,而double,long因为是64位长度的,所以占有两个slot,但是读写的时候是连续读写两个slot,虚拟机通过索引位置的方式来定位局部变量表,比如索引0就是第0个slot,索引n就是第n个slot,但是如果索引的是64的数据类型,那么索引n就是要找到第n个和第n+1个slot了,这就是简单的slot。
还有要注意的一点就是局部变量一定要赋值,局部变量是没有准备阶段的,没有赋值是不能运行的。就像下面这样的代码。
public class a{
public static void main(String[]args){
int a;
System.out.println(a);
}
}
二:操作数栈
这个就不用多说了,数据结构里面写的很清楚,后入先出是吧,这个相信大家都很熟悉了,和局部变量表一样,最大的容量已经写入了code属性里面的max_stacks里面了,里面放入的可以是32位的,也可以是64位的,只是64位的数据占容量为2而已。
先举个平常的例子,如果一个方法是要计算1+1,那么方法编译执行时,会有各种字节码指令来向操作数栈中写入数据,写入1和1,然后通过字节码相加指令,将两个1先后拿出栈,算出2有入栈,得出计算结果。
这个东西很容易懂得,我也不多说,详细的可以去看看数据结构栈,一些算法讲的很详细。
三:动态链接
每一个帧栈都有代码支持动态链接,class文件描述当一个方法调用其他方法,或者通过符号引用访问成员变量时,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。
这个其实和c++里面的动态关联差不多。一个叫做静态关联,一个叫做动态关联,在java里面可以叫静态解析,动态链接(连接),静态解析就是在编译是就确定了是哪个类或者哪个方法了,比如函数重载的话,根据参数的个数就有可能会知道是要执行哪个方法了。而动态链接就是编译时无法确定具体调用哪个类哪个方法,编译时就只能做语法检查,这时候就要去运行时确定关联关系。所以,静态解析只要在编译时就可以将方法转为直接引用,而动态链接要在每次运行期间才可以。
符号引用:类和接口的完全限定名、字段的名称和描述符、方法的名称和描述符
四:方法返回地址
方法返回的方式有两种,第一种是在方法执行时遇到return这种正常的字节码指令来返回方法,有没有返回值也会根据你的定义和字节码来返回一个返回值给这个方法调用者,这种退出方法是正常的。第二种就是异常的了,在方法执行过程中遇到了异常,而且没有得到处理,这样就不管是java虚拟机内部异常,还是字节码的异常,都差不多,在这个方法异常表里面找不到对应的处理措施,方法就会终止退出,并且不会有任何返回值。
按理来说,方法退出以后,应该要返回上一层的调用者的位置,这时候线程的pc计数器应该会记录一些位置信息,但是异常退出的不会了。
注意:
虚拟机栈有两个异常情况:
1、线程请求的栈深度>虚拟机栈的允许最大深度,抛出StackOverflowError
2、如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存是,抛出OutOfMemoryError(当前大部分虚拟机都支持动态扩展,只不过虚拟机规范中也允许固定大小的虚拟机栈)

(3):本地方法栈

本地方法栈和虚拟机栈很相似,虚拟机栈只不过是为java方法服务的,而本地方法栈只不过为虚拟机用到的native方法服务的。什么是本地方法,你可以想象成一种用c语言编写的实现方法,在调用时,不会向虚拟机栈中压入方法,只是简单纯粹的调用而已,但是java虚拟机只能识别java编译后的代码的,这就涉及到底层操作系统之间的交互了,具体可以参考c与java交互,我们也不用知道怎么具体实现交互,反正本地方法就是给我们这样一个接口罢了,我们只需要通过这一个接口去引用本地方法,因为在某些情况下,java编写的方法是没有c语言的快的。但是注意,它和虚拟机栈也可以跑出OutOfMemoryError和StackOverflowError异常的。

(4):java堆

java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就可以了。java堆是java虚拟机管理内存中最大的一块了,而且是被线程共享的一块区域,一旦虚拟机启动了,相应的堆就创建了。它是用来存所有实例对象的,而且它是垃圾收集管理的主要区域,称作GC堆,java堆中可以分为新生代和老年代。

(5)方法区

方法区也叫做永久代,它本来是java堆中的逻辑部分,但是为了与java堆区别,他还有个别名叫做非堆,和java堆一样,是线程共享的区域,他可以存储各种已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。这是基于HotSpot的虚拟机才有的概念,它的设计团队把GC分代收集扩展到方法区,以便管理这部分内存,以后我会更新博客详细介绍的。

(6)运行时常量池

在这里插入图片描述

先给这个图片给大家看一下,这样就能够很清楚的明白方法区和运行时常量池之间的关系了。
先理清楚下面的概念:

  • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。我们可以理解为class文件中的资源仓库。

在这里插入图片描述

常量池中主要存放两大类常量:字面量和符号引用。
符号引用在前面的虚拟机栈中的动态链接已经叙述过了,字面量就是文本字符串、声明为final的常量值等

  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。听说JDK1.6之前字符串常量池是在方法区中,1.7之后就被移出去了。
  • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。
  • 当然在运行的时候也可能向运行时常量池中添加数据的。

二:HotSpot虚拟机对象


(1)对象的创建过程

1:虚拟机接受到创建对象的指令,来到方法区,找到方法区有没有对应的符号引用,其实就是一些类的信息啦。如果发现类已经被加载过了,就直接进入分配新的内存。否则需要虚拟机加载这个类。
2:为对象在堆中划分一个区域内存,这里就要遵守内存分配的原则了,内存分配原则以后再说啦,大家可以暂时查一查,然后分配方法有指针碰撞和空闲列表两种。
指针碰撞:这个是假设java堆中的内存是绝对规整的,然后将用过的内存放在一边,空闲的放在了一边,中间有指针作为分界线,分配内存移动指针就行了。
空闲列表:如果java堆中的内存不是绝对规整的,这样用过的和没用过的就乱七八糟,这样通过建立一个空闲列表来记录那些内存区域可以使用,分配内存是只要更新一下列表就行了。
在分配内存是,还有一个线程安全的问题,在并发时,有可能一个对象创建了,指针还没来得及改变,另一个对象就创建并占有了这个指针移动的内存区域,导致内存分配矛盾。这里通常两种解决的方法:
对分配内存采用同步处理,失败重试。
将分配内存的线程划分不同的空间中,为每一个线程划分内存,保证每个线程有个缓冲区;
3:内存分配后就要初始化了,虚拟机将内存空间都初始化为零,保证了实力对象的字段能不赋初值就能使用。
4:虚拟机对对象进行必要的设置,比如这个对象是哪个类的实例,这个对象的GC分代啥的。
5:对象创建刚刚开始,这时候要用到方法初始化,所有字段还为零。

(2)对象的内存布局

在HotSpot虚拟机中,对象在内存中的布局可以分为3块区域:对象头,实例数据,对齐填充。
在这里插入图片描述
对象头:对象头包括两个部分:
1:用于存储对象自身的运行时数据:比如上图的哈希吗,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳,等等,官方称它叫做“Mark Word”,考虑到虚拟机的空间效率,官方设计者把它设计成非固定的数据结构,以便在很小的空间里存储尽量多的信息,根据对象的状态复用自己的存储空间。
2:类型指针:这个是指向元数据的一个指针,用来描述这个对象属于哪一个类。
补充:如果为对象数组,则应该还有描述数组长度的int数据,java虚拟机从对象的元数据可以获得数组的大小,但是不能从数组的元数据获得数组大小。
实例数据:就是对象真正存储有效的内容,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数和字段在 Java 源码中定义顺序的影响。
对齐填充:这个部分没有多大意义,就起了一个占位符的作用,主要是虚拟机的对象起始地址必须满足8的整数倍,有时候不足就要补齐。

计算对象内存的大小:对象头+实例数据+对齐补充

对象头:如下图所示,对象头包括了一下几个部分。在32位系统中,Mark work的字节是4字节,class指针也是4字节,一搬情况下就是8字节,如果是64位系统中,就都变成了8字节,但是如果开启了指针压缩,class指针就变成了4字节,一共是8字节,然后对齐后就是16字节了。
在这里插入图片描述
实例数据:根据本来的数据类型大小计算所占字节。但是要根据指针是否压缩来对齐。

(3)对象的定位访问

1:句柄访问:

在这里插入图片描述
通过 栈帧中局部变量表所存储的对象引用来对堆内存中的对象实例进行访问或操作的,我们就只要想成栈帧里面对象引用有指针通过一些办法指向堆里面的实例对象。上面这张图,java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
优点:reference中存储的是稳定的句柄地址,在对象被移动时,只用修改句柄中的实例数据指针,而reference本身不需要修改。
缺点:通过两次引用指针,开销相对较大。

2:直接访问: 在这里插入图片描述

reference直接指向了对象类型数据,reference存储的就是对象地址。

优点:一次指针访问,开销相对较小

缺点:必须考虑如何放置访问类型数据的相关信息

以上就是一些对象创建,内存布局,访问等一些知识点,希望对大家有所帮助,当然还有很多细节的东西我还没弄清楚,这只是一个大概的轮廓,后面我会更新更正它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小满锅lock

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值