公司网站销售怎么做的,qq空间做网站,西安微动免费做网站,建设网站套餐JVM系列 | 对象的生命周期1 对象的创建与存储 文章目录 前言对象的创建过程内存空间的分配方式方式1 | 指针碰撞方式2 | 空闲列表 线程安全问题 | 避免空间冲突的方式方式1 | 同步处理#xff08;加锁)方式2 | 本地线程分配缓存 对象的内存布局Part1 | 对象头Mark Word类型指针…JVM系列 | 对象的生命周期1 对象的创建与存储 文章目录 前言对象的创建过程内存空间的分配方式方式1 | 指针碰撞方式2 | 空闲列表 线程安全问题 | 避免空间冲突的方式方式1 | 同步处理加锁)方式2 | 本地线程分配缓存 对象的内存布局Part1 | 对象头Mark Word类型指针 Part2 | 实例数据Part3 | 对齐填充* 示例 | 代码与图 对象的访问定位方式1 | 句柄方式2 | 直接指针差异对比方法区 与 Java堆 的比较 资料来源下一篇预览 | 对象的消亡-垃圾回收机制 前言 之前在《Java虚拟机运行时数据分区介绍》一文中介绍过Java对象一般都存储在堆中本文章将在上篇文章的基础上详细介绍一下对象的创建过程与堆内存模型。 对象的创建过程
Java程序运行过程中无时无刻不在创建对象。从语言层面上创建对象通常仅仅是一个new关键字但是从JVM的角度来说当遇到一个new指令时会做以下几件事情。
检查这个指令的参数是否能够在常量池中定位到一个类的符号引用并且这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有则必须执行类的加载过程。给对象分配内存空间所需内存大小在类加载完成后便可以完全确定内存空间的分配方式在下一个小结做详细介绍JVM还要对对象进行必要的初始化例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等这些信息存放在对象头之中。根据虚拟机当前的运行状态的不同如是否启用偏向锁等对象头会有不同的设置方式。在此时所有的字段都为默认的0值此时要执行构造函数即Class文件中的init()方法。 内存空间的分配方式
一般来说内存分配有两种方式分别如下 方式1 | 指针碰撞
假设所有的对象是在内存中并排排列的那么我们仅需要一个“分界指针”指出当前最后一个对象所在位置即可也就是空闲空间的起始位置这样一来当创建新的对象的时候只需要将该指针向后移动一个对象大小的空间即可。
为了方便理解整张图 当没有新对象进来的时候分界指针指向空闲空间的起始位置当进来新对象之后假设新对象大小为1kb那么指针就向后移动1kb的空间这1kb就用来存储新的对象 该方式适用于标记整理算法、标记复制算法等垃圾回收算法。 方式2 | 空闲列表
如果对象不是有序的存储在内存中的而是如下图一样散乱的存储程序的最开始可能是有序的随着对象被清理而不整理则可能会出现这种情况那么JVM会为空闲空间维护一个列表该列表记录着所有空闲空间的起始位置与大小当需要new一个新的对象的时候会在列表中寻找到一个可以放得下该对象的空间将对象放在该空间中并更新空闲列表。 维护空闲空间列表新的对象进来在空闲空间列表中查找可以放得下该对象的空间放入对象并更新空闲空间列表 该方式适用于标记清除算法。 线程安全问题 | 避免空间冲突的方式
线程安全问题由于在对象的创建是一件非常频繁的事情假设有100个线程同时需要创建对象他们进入到堆内存后以上述方式1为例100个线程同时拿到了当前分界指针的位置并在该位置开始写入内容那么就会造成内存混乱100个线程创建的对象互相重写前一个线程写入的内容最终必定会造成该位置的内容不可用。 最终的结果可能比我画的图要复杂的多各个对象的字节码穿插出现。
如何才能避免这种问题呢JVM采用了两种方案 方式1 | 同步处理加锁)
事实上虚拟机采用的是CAS配上失败重试的方式保证更新操作的原子性当一个线程抢占到锁之后其它线程只能等待当该线程执行结束后该内存空间已经有对象了且分界指针已经更新此时其余线程再次抢占锁如此循环往复就没有了线程安全问题。 方式2 | 本地线程分配缓存
本地线程分配的方式如下图所示JVM会为每一个线程分配一个新的空间用来存储接下来该线程中new出来的对象这样就不会存在空间抢占问题从而避免了线程安全问题。 为每一条线程分配空闲空间将线程创建的对象写入其对应的缓存空间中去若是缓存空间不足则重新分配空间 对象的内存布局
一个对象由三个部分组成分别为对象头、实例数据、对齐填充如下图所示 这张图每个内容都有一定的含义下面会详细介绍。
Part1 | 对象头
对象头一般存储两类信息 Mark Word
Mark Word用于存储对象自身的运行时数据如哈希码、GC分代年龄、锁状态标识、线程持有锁、偏向线程ID、偏向时间戳等。这类数据在长度32位的虚拟机中长度为32比特在未开启指针压缩技术的64位虚拟机中长度为64比特。官方称此为Mark Word。
对象需要存储的运行时数据很多其实已经超过了64位Bit Map结构所能记录的最大限度但是对象头中的信息是与对象自身定义的数据无关的额外存储成本考虑到虚拟机的空间效率Mark Word被设计成一个有着动态定义的数据结构以便在极小的空间内存储尽可能多的数据根据对象的状态复用自己的存储空间。
例如在32位的HotSpot虚拟机中如对象未被同步锁锁定的状态下Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码4个比特用于存储对象分代年龄2个比特用于存储锁标志位1个比特固定为0在其他状态轻量级锁定、重量级锁定、GC标记、可偏向。 类型指针
类型指针用于指向对象的类元数据。类元数据包含了类的结构信息如类名、父类、接口、方法、字段等。这部分信息是静态的不会在运行时发生变化。类型指针的主要作用是使JVM能够快速地找到对象对应的类信息以便执行方法调用和字段访问等操作。
类型指针通常指向方法区中的类对象。方法区中包含了每个类的类元数据结构Class Metadata包括类的常量池、字段和方法的描述符、方法的字节码等。
此外如果对象是一个Java数组那在对象头中还必须有一块用于记录数组长度的数据因为虚拟机可以通过普通 Java对象的元数据信息确定Java对象的大小但是如果数组的长度是不确定的将无法通过元数据中的信息推断出数组的大小。 Part2 | 实例数据
实例数据部分是对象真正存储的有效信息即我们在程序代码里面所定义的各种类型的字段内容无论是从父类继承下来的还是在子类中定义的字段都必须记录起来。
这部分的存储顺序会受到虚拟机分配策略参数-XXFieldsAllocationStyle参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oopsOrdinaryObject PointersOOPs相同宽度的字段总是被分配到一起存放在满足这个前提条件的情况下在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的XXCompactFields参数值为true默认就为true那子类之中较窄的变量也允许插入父类变量的空隙之中以节省出一点点空间。 Part3 | 对齐填充
对齐填充并不一定是必须存在的仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍也就是对象大小必须是8的倍数对象头已经被精心设计成8的倍数因此如果对象实例数据不满足8的倍数的话会用对齐填充来对齐。
* 示例 | 代码与图
我们在Java中定义一个类内容如下
public class Example {int num1;byte byte1;boolean bool1;
}假设对象头占用16字节字段num1占用4字节字段byte1占用1字节字段bool1占用1字节则对象的布局如下 16字节的对象头 6字节的实例数据 由于实例数据不足8位用2字节的对齐填充进行补齐 对象的访问定位
这一段写的比较多看起来下划线、加粗、高亮交替出现也比较乱但是真的很重要也很有趣我尽量用较少的语言描述清楚。
关于下文会用到的栈、栈帧等内容可以在《【JVM】Java虚拟机运行时数据分区介绍》找到对应介绍。 我们在代码中定位对象的情况无非只有几种代码如下 作为成员变量作为成员方法作为方法参数作为方法中的实例对象 不过需要注意的是无论作为哪种方式出现在代码中最终对他们的操作都一定是在方法中的而方法是以栈帧的形式出现在栈中的所以对于对象的任何操作都可以理解成是从栈帧出发寻找到这个对象的存储地址。 OK深入理解一下上面一段话会想到什么呢 有没有什么东西有操作内容却不是方法 答案就是代码块与静态代码块 先来简单复习下内容代码块会在每次创建一个新的对象的时候执行一次静态代码块会在类加载的过程中初始化的时候执行且仅执行一次。 代码块与静态代码块并不会创建栈帧。当类被加载的时候JVM会为该类生成一个名为clinit的方法静态代码块会加入到该方法中一起执行。当使用构造方法创建一个对象的时候JVM会为该构造函数创建一个新的栈帧普通代码块作为构造方法的一部分一起执行并且在这个栈帧前面运行。 public class JimExample {private JimExample e1; // 实例变量引用存储在堆中private static JimExample e2; // 类变量引用存储在方法区的静态区中// param 是一个方法参数引用存储在栈帧中public void m1(JimExample param) {// obj 是一个局部变量引用存储在栈帧中JimExample obj new JimExample();}}至此我们已经了解到了对于对象的使用定位寻址一定是从栈帧中出发的而对对象的访问主要是通过句柄和直接指针来实现的。 方式1 | 句柄
使用句柄访问的话Java堆中将可能会划分出一块内存来作为句柄池reference(在Java栈的本地变量表中)存储的就是对象的句柄地址而句柄中包含了对象实例数据与类型数据各自具体的地址信息其结构如下图所示 注意一个是实例数据一个是类型数据句柄池中两个指针为一个句柄两个指针一个指向实例数据一个指向类型数据。 方式2 | 直接指针
使用直接指针访问的话Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息reference中存储的直接就是对象地址如果只是访问对象本身的话就不需要多一次间接访问的开销如图下图所示: 注意堆中存储的数据不再是句柄池而是一个实实在在的对象只不过该对象有一个指针指向了该对象的对象类型数据上所以访问完整的对象要先通过reference找到该对象的实例数据再通过实例数据中的指针找到对象的类型数据。 差异对比 使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址在对象被移动垃圾收集时移动对象是非常普遍的行为时只会改变句柄中的实例数据指针而reference本身不需要被修改。 直接指针来访问最大的好处就是速度更快它节省了一次指针定位的时间开销由于对象访问在Java中非常频繁因此这类开销积少成多也是一项极为可观的执行成本就本书讨论的主要虚拟机HotSpot而言它主要使用第二种方式进行对象访问有例外情况如果使用了Shenandoah收集器的话也会有一次额外的转发具体可参见第3章但从整个软件开发的范围来看在各种语言、框架中使用句柄来访问的情况也十分常见。 方法区 与 Java堆 的比较
特性方法区Method Area实例池/堆Instance Pool/Heap存储内容类的结构信息、常量池、静态变量、字节码、类的元数据等。通过new关键字创建的对象实例和数组。生命周期类加载时开始存在类卸载时移除。对象被创建时开始存在GC回收时移除。内存管理使用不同于堆的管理机制HotSpot JVM中JDK 8前为永久代PermGenJDK 8后为元空间Metaspace。分为新生代和老年代新生代包括Eden区、Survivor区S0和S1。特点存储静态信息不随程序运行变化垃圾回收较少。存储动态数据随程序运行变化GC频繁执行。 资料来源 周志明《深入理解Java虚拟机》 下一篇预览 | 对象的消亡-垃圾回收机制
JVM虚拟机每时每刻都在创建对象但是如果创建出来的对象一直存放在内存中那么程序的内存早晚有不够用的一天在这里简单介绍一下C的对象管理可能不少资料中都提到过C的程序员在代码中是神一样的存在他们掌握着每一个对象的生死这是由于C需要使用delete/free关键字与new关键字对应手动删除创建出来的对象代码如下所示
int* p new int; // 动态分配一个整数
*p 10; // 使用这个整数
delete p; // 释放分配的内存但是Java中并没有这种操作这是由于JVM有垃圾回收机制简单来说JVM会定期扫描实例对象是否还“有用”如果没有用的话就给它清理掉。