Java中的内存区域及对象的创建、内存布局与访问定位

来源:互联网 发布:在哪里购买域名 编辑:程序博客网 时间:2024/05/28 03:03

在读这篇博客之前,我很想知道你的目的,以及你目前的基础。如果你明白基本的堆栈信息以及对常量池和方法区有所了解。而且你知道static finai native 等关键字的意义而且有实际应用,并且你很想知道对象创建的内容到底是什么。那就读下去。很长,但是需要好好读。我花费了很久才弄懂一小部分内容。
友情提示: 按照顺序进行读,理解每一个模块都是为了理解后面模块的基础。由于编辑器的问题,有些地方需要画图,但是没有画,抱歉。

运行时数据区域

划分:根据《Java虚拟机规范》规定:运行时数据区划分为:
1.程序计数器
2.Java虚拟机栈
3.本地方法栈
4.堆
5.方法区

  1. 程序计数器
    本质:一片用来指引当前线程执行的字节码的行号指示器。字节码解释器就是通过改变计数器的值从来来确定下一跳要执行的字节码指令。

    空间大小:较小的内存空间
    线程共享OR私有:线程私有
    生命周期:与线程相同
    异常状况:唯一一个在Java虚拟机规范中没有规定OutOfMemoryError情况的区域


2.java虚拟机栈
理解:广义上的Java内存划分是不准确的。不能直接划分为堆和栈。

本质:java方法执行的内存模型。
栈帧:用来存储局部变量表,操作数栈,动态链接,方法出口信息的一种基础数据类型。方法的完成,对应栈帧在虚拟机栈中完成入栈出栈的过程。

普通意义上的栈:也就是局部变量表部分。

局部变量表的本质:存放编译期可知道的各种基本数据类型 + 对象引用 + returnAddress类型

局部变量表的内存空间:编译期间内存空间就分配成功,当一个方法进入的时候,这个方法需要在栈帧中分配的大小是完全确定的,方法运行期间并不会改变局部变量表的大小。

线程共享OR私有:线程私有
声明周期:与线程一致
异常状况: StackOverflowError(请求栈深度大于虚拟机允许的深度)
OutofMemoryError(扩展未申请到足够大的内存)


3.本地方法栈
作用:为执行Native方法服务
虚拟机规范:未指定本地方法栈中方法使用的语言、使用方式、数据结构进行规定
线程以及声明周期和异常状况与虚拟机栈一直。具体参考上文。


4.Java堆
本质:存放对象实例的一个较大的内存空间,垃圾收集器管理的主要空间。
存在的目的:为对象实例和数组分配内存空间。

划分的方式:与链表类似。可以处于物理上不连续,但是逻辑上连续即可。
Java堆的细分:

依据内存回收角度划分:新生代与老生代。新生代继续细分:Eden空间、From Survivor空间、To Survivor空间,其比例是8:1:1.
依据内存分配角度:多个线程私有的分配缓冲区(TLAB)

划分的意义:与存放内容无关,仍然是对象实例。目的都是为了加快JAVA虚拟机工作。
线程私有OR共享:线程共享。
异常状况:在堆内没有内存完成实例分配,堆也无法扩展时抛 OutOfMemoryError异常。


5.方法区
在这里我把方法区作为掌握的重点。因为我打一开始学习的时候就不太明白很多东西。
本质:存放已经被JVM加载的类信息、常量、静态变量等数据的内存区域。
地位:HotSpot团队选择吧GC分代收集扩展至方法区,或者可以说用永久代实现方法区。
线程私有OR共享:线程共享。
常量池:存放编译期间的各种符号引用和字面量。类加载后进入方法区的运行时常量池存放。
运行时常量池:动态性。体现:String类的intern()。
异常状况:常量池无法申请到内存时抛OutOfMemoryError异常。


6.方法区的仔细介绍

存储的内容:类信息 + 常量 + 静态变量 + 即时编译器编译后的代码等数据。
首先我们介绍类信息,这是一个庞大的体系。
类信息
1>.类型信息
类型信息:是由类加载器在类加载时从类文件中提取出来的
类型信息包括的内容:
<1>.这个类型的完整有效名:包名 + . + 类名。
<2>.这个类型的直接父类的完整有效名:除过interface + Object类。
<3>.这个类型的修饰符:四大访问权限+abstract +final。
<4>.这个类型直接接口的有序列表。

接下来的是重要的常量池。
2>.类型的常量池
JVM为每个已经加载的类都维护一个常量池。
本质:这个类型用到的常量的一个有序集合。
存储内容:实际常量(String,Integer,) + 对(类型,域,方法的)符号引用。
池中数据的排列方式:同数组,通过索引来进行访问。
作用:动态链接起到核心作用。

然后是域信息。
3>.域信息
域信息包括:域的相关信息以及域的声明顺序。
域的相关信息包括:域名+域类型+域修饰符(四大访问修饰符+static+final+volatile +transient的某个子集)。

最后是方法信息。
4>.方法信息
方法信息包括:与域信息一样包括方法的以下信息及声明顺序。
方法名+返回类型+有序的方法参数的数量和类型 +方法的修饰符。
特别注意的是:方法的修饰符包括:四大访问修饰符 +abstract + static + final+synchronized+native.除过abstract 和native方法外,其他的方法还要保存方法的字节码操作数栈+方法栈帧的局部变量表的大小。

常量
常量是什么,就是被final修饰的变量。在常量池中每个常量都有一个拷贝。
常量的分类:非static修饰的但被final修饰 + 类常量被final修饰的
非static修饰但被final修饰的:被存储在使用它的类信息内。
类常量被final修饰的:存储在声明它的类信息内。

静态变量
静态变量又被称作类变量,随类加载而存在,只与类相关。
类变量被类的所有实例所共享,即使没有实例你仍然可以继续访问它,用类名.进行访问。
因为只与类相关,所以在方法区中,他们成为类数据在逻辑上的一部分。
所以在JVM使用一个类之前,必须为static final类变量分配空间。

编译后的代码
这里不详细介绍。

对象的内存布局

基于HotSpot虚拟机。
对象在内存中的布局分为3部分:对象头+实例数据+对齐填充。
<1>.对象头:包括Mark Word + 类型指针
1>.Mark Word:用来存储对象自身的运行时数据的非固定的数据结构。包括:哈希码、GC年龄、锁状态标志、线程持有的锁等。
优势:在极小的空间存储更多的信息。包括根据对象的状态复用自己的空间。

2>.类型指针:对象指向类元数据的指针,可以通过它来确定这个对象是哪个类的实例。如果是数组,还应该包括一个用于记录数组长度的数据。

<2>.实例数据:对象真正存储的有效信息。包括:父类继承到的+子类自己定义的
存储顺序的影响因素:虚拟机分配策略参数。
分配策略:相同宽度的字段总是被分配到一起。在这个前提下,父类定义的变量会出现在子类之前。

<3>.对齐填充
自动内存管理系统:要求对象起始地址必须是8字节的证书也被,也就是对象的大小必须是8字节的整数倍。

对齐填充的作用:占位符,满足虚拟机的自动内存管理系统的要求。只有当实例数据部分没有对齐时,才需要对齐填充来进行补全。

对象的访问定位

前提:我们整天在说栈上面的引用变量指向了堆中的实例对象,然后堆中的实例对象指向了方法区中加载的类信息。然后就开始进行内存分析了。我相信大部分老师都是这样给学生讲的。同样,我的老师仍然这样给我讲,而且我在网上找视频看的时候也是这样讲的。

引用类型变量的作用:位于JAVA栈中局部变量表中的引用变量指向了堆中的对象。在虚拟机规范里面这样说:reference规定指向的是一个引用,并没有定义这个引用应该通过何种方式去定位去访问堆上的对象的具体位置。所以应该由具体的虚拟机自行实现。

目前流行的访问方式:使用句柄和直接指针。
<1>.使用句柄去访问:堆划分:句柄池和实例池。
句柄池放置两块内容:对象实例数据的地址+对象类型数据的地址。
实例池:放置对象实例数据
方法区:放置对象类型数据
描述:局部变量表中存储的引用变量指向了堆中的句柄池。然后句柄池依据存储的内容不同去访问不同地方的数据。句柄池中的对象实例数据的指针指向堆中另外一块实例池中的对象实例数据。而句柄池中的对象类型数据的指针指向的是方法区中的对象类型数据。
优点:引用变量存储的是句柄地址,所以当对象移动时只会改变句柄中的实例数据指针,而引用变量本身并不需要改变。

<2>.使用直接指针访问。
局部变量表:存储引用变量。
堆:存储实例对象。这个实例对象的对象头上面存储着对象类型数据的地址 + 对象实例数据。
方法区:放置对象类型数据。
描述:局部变量表中的引用变量指向堆中的实例对象,然后实例对象保存两部分内容:对象实例数据 + 对象头中的对象类型数据的指针。通过对象头中存储的指针可以轻易找到方法区中的对象类型数据。

优点:由于JAVA中访问对象过于频繁。这种访问方式速度足够快,节省了一次指针定位的开销。所以HotSpot就是采用这种方式来进行访问定位。

对象的创建

1.虚拟机遇到new指令后悔检查这个指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否进行了加载,解析初始化,也就是类加载过程。如果没有,先进行类加载。(类加载的内容过于庞大,后期专门写一篇)。

2.检查通过后为新生对象分配内存。对象所需内存大小在类加载后就可以确定。
考虑两大问题:如何划分内存 以及解决在为对象分配内存时由于创建对象过于频繁指针错位使用的情况。

<1>.如何划分内存:依据堆内存是否规整。而规整的原因在于所采用的垃圾收集器是否带有压缩整理功能。(垃圾回收同样是一部分大内容下一期博客进行讲解)

1>.依据堆内存规整的时候的划分:指针碰撞。
指针碰撞:由于内存规整,是否使用的内存之间是有清晰的划分的。所以在使用的内存与空闲的内存空间之间放置一个指针,作为分界点的指示器。分配内存就是将指针像空闲空间挪动一部分与实例对象等同大小的距离。
2>.依据堆内存不规整时候的划分:空闲列表
空闲列表:由于可用内存与已用内存之间纵横交错,不能进行简单的指针碰撞。所以此时选择维护一个列表,列表中记录的是哪些内存块是可用的。当需要分配的时候就从列表中找到一个足够大的划分给实例对象,并更新列表的内容。

<2>.指针错位使用。
由于对象的频繁创建,修改指针中的内容在并发情况下也不是线程安全的。
出现的问题:A正在分配内存,指针还未来得及修改,这时候B使用了原来的指针进行分配内存。
解决方案:分配内存空间的动作进行同步处理 + 依据线程的划分吧内存分配动作划分在不同空间内。
同步处理:虚拟机采用的CAS+失败重试保证更新操作的原子性。
划分不同空间:每个线程在堆中预先分配一小块内存,称为TLAB(本地线程分配缓冲)。那个线程要进行内存分配就在那个线程的TLAB上分配。只有TLAB用完并分配新的时候才需要同步锁定。

3.内存分配完成后就开始初始化零值。(不包括对象头。)保证了对象的实例字段在JAVA中不赋初值就可以直接进行使用,访问到的都是对应类型的零值或NULL。

4.对对象头进行设置。

5.虚拟机视角来查看对象已经创建成功,但是从程序视角来看并没有。因为我们手动初始化的东西还没有进行。所以此时会调用

阅读全文
0 0