1. 对象的创建和分配

创建对象(如克隆、反序列化)通常仅仅一个new关键字,但在虚拟机中,对象的创建的过程需要如下步骤:

  1. 类加载检查

    先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、初始化过,若没有,则必须先执行相应的类加载过程。
  2. 为新生对象分配内存

    对象所需内存大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。根据java堆内存是否绝对规整,划分方法不同:
  • 1)指针碰撞(Bump the Pointer): Java堆中内存绝对规整(所有用过的内存放在一边,空闲的内存放在另一边,中间 放一个指针作为分界点的指示器),所分配的内存仅需要把指针向空闲空间那边挪动一段与对象大小相等的距离。

  • 2)空闲列表(Free List),Java堆中内存并不规整(已使用的内存和空闲的内存相互交错),虚拟机通过维护一个记录哪些内存可用的列表,在分配时从列表中找到一块中够大的空间划分给对象实例,并更新列表上的记录。

    分配方式由Java堆是否规整决定,而是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。

    【注】由于对象创建在虚拟机中是非常频繁的行为,仅修改一个指针所指向的位置,并发情况下也不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存情况。解决方案有两种:

  • 1)对分配内存空间的动作进行同步处理,虚拟机采用CAS配上失败重试方式保证更新操作的原子性;

  • 2)本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),给每个线程在Java堆中预先分配一小块内存,哪个线程要分配内存,就在哪个线程的TLAB上分配,TLAB用完分配新的时才需要同步锁定。可以通过-XX:+/-UseTLAB参数来设定是否使用TLAB。

  1. 将分配的内存空间初始化为零值(不包括对象头)

    若使用TLAB,可以提前至TLAB分配时进行。这一步操作保证了对象实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  2. 对对象头进行必要设置

    如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。根据虚拟机当前的运行状态不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  3. 执行<init>方法

    把对象按照程序员意愿进行初始化,即执行<init>方法。

————————————————

原文链接:https://blog.csdn.net/smileiam/article/details/80364641

2. 对象头

2.1 对象在堆内存中的存储布局

  在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

注:如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit。

HotSpot虚拟机对象的对象头部分包括两类信息。

  第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下所示。

上面是32位JVM的MarkWord,64位JVM的Mark Word如下:

  对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身,这点我们会在下一节具体讨论。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。

注:这里的“查找对象的元数据信息并不一定要经过对象本身,这点我们会在下一节具体讨论”,这里指的是通过句柄的方式来定位到堆中的对象,与之相对的是直接定位方式,此时对象头中的“Class Metadate Address”就发挥了作用,它用于定位对象的类型,指向的是Mete Space(元空间 JDK1.8)的某个区域。

  接下来实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(OrdinaryObject Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空

隙之中,以节省出一点点空间。

  对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

2.2 对象头分析

分析工具:

查看对象头的神器

介绍一款可以在代码中计算java对象的大小以及查看java对象内存布局的工具包:jol-core,jol为java object layout的缩写,即java对象布局。使用只需要到maven仓库http://mvnrepository.com搜索java object layout,选择想要使用的版本,将依赖添加到项目中即可。

使用jol计算对象的大小(单位为字节):

ClassLayout.parseInstance(obj).instanceSize()

使用jol查看对象的内存布局:

ClassLayout.parseInstance(obj).toPrintable()

5.MarkWord

通过倒数三位判断当前MarkWord的状态,就可以判断出其余位存储的是什么。

[markOop.hpp文件]

enum {  locked_value             = 0, // 0 00 轻量级锁
unlocked_value = 1,// 0 01 无锁
monitor_value = 2,// 0 10 重量级锁
marked_value = 3,// 0 11 gc标志
biased_lock_pattern = 5 // 1 01 偏向锁
};

现在,我们再看下User对象打印的内存布局。

通过前面的讲解,我们已经知道,对象头的前64位是MarkWord,后32位是类的元数据指针(开启指针压缩)

从图中可以看出,在无锁状态下,该User对象的hashcode为0x7a46a697。由于MarkWord其实是一个指针,在64位jvm下占8字节。因此MarkWordk是0x0000007a46a69701,跟你从图中看到的正好相反。这里涉及到一个知识点“大端存储与小端存储”。

  • Little-Endian:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。

  • Big-Endian:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。

学过汇编语言的朋友,这个知识点应该都还记得。本篇不详细介绍,不是很明白的朋友可以网上找下资料看。

6.写一个synchronized加锁的demo分析锁状态

接着,我们再看一下,使用synchronized加锁情况下的User对象的内存信息,通过对象头分析锁状态。

案例1

public class MarkwordMain {

   private static final String SPLITE_STR = "===========================================";
private static User USER = new User(); private static void printf() {
System.out.println(SPLITE_STR);
System.out.println(ClassLayout.parseInstance(USER).toPrintable());
System.out.println(SPLITE_STR);
} private static Runnable RUNNABLE = () -> {
while (!Thread.interrupted()) {
synchronized (USER) {
printf();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}; public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(RUNNABLE).start();
Thread.sleep(1000);
}
Thread.sleep(Integer.MAX_VALUE);
}
}

从该对象头中分析加锁信息,MarkWordk为0x0000700009b96910,二进制为0xb00000000 00000000 01110000 00000000 00001001 10111001 01101001 00010000。

倒数第三位为"0",说明不是偏向锁状态,倒数两位为"00",因此,是轻量级锁状态,那么前面62位就是指向栈中锁记录的指针

案例2

public class MarkwordMain2 {

    private static final String SPLITE_STR = "===========================================";
private static User USER = new User(); private static void printf() {
System.out.println(SPLITE_STR);
System.out.println(ClassLayout.parseInstance(USER).toPrintable());
System.out.println(SPLITE_STR);
} private static Runnable RUNNABLE = () -> {
while (!Thread.interrupted()) {
synchronized (USER) {
printf();
}
}
}; public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(RUNNABLE).start();
}
Thread.sleep(Integer.MAX_VALUE);
}
}

从该对象头中分析加锁信息,MarkWordk为0x0000700009b96910,二进制为0xb00000000 00000000 01111111 11110000 11001000 00000000 01010011 11101010。

倒数第三位为"0",说明不是偏向锁状态,倒数两位为"10",因此,是重量级锁状态,那么前面62位就是指向互斥量的指针

————————————————

版权声明:本文为CSDN博主「灬少年郎灬」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/baidu_28523317/article/details/104453927

在上面的两个实例中,第一个的锁表现为轻量级锁,第二个表现为重量级锁。然而并非一直都是如此,它只是某个线程的瞬时状态,它是动态变化的。

3. 对象头和锁

  java中锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

3.1 关于偏向锁的获取和释放:

  当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出

同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(即替换线程ID),注意这里存在两种情况

  1. 获取锁成功:那么它会直接将mark word中的线程ID,由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。
  2. 获取锁失败:则表示这时会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会进行升级,升级为轻量级锁。(这里存在一个偏向锁撤销的过程,先撤销偏向锁,然后升级为轻量级锁)

下图描绘了两个线程在争抢偏向锁时的状态:

  偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

  偏向锁只适用于一个线程访问同步块时的场景。如果有两个或以上的线程同时竞争锁,则可能会出现竞争失败,导致升级为轻量级锁的情形。

3.2 关于轻量级锁的加锁和释放

(1)轻量级锁加锁

  线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。这里存在自旋后仍旧没有获取到锁的情形,此时同样也会升级为重量级锁。

(2)轻量级锁解锁

  轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是两个线程同时争夺锁,导致锁膨胀的流程图。

  因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

4. 对象的访问定位

  创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

  • 如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图2-2所示。

  • 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图2-3所示。这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

  使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。

引用链接:https://blog.csdn.net/smileiam/article/details/80364641

关于Java的对象,锁和对象的内存布局、访问定位的更多相关文章

  1. java synchronized类锁,对象锁详解(转载)

    觉得还不错 留个记录,转载自http://zhh9106.iteye.com/blog/2151791 在java编程中,经常需要用到同步,而用得最多的也许是synchronized关键字了,下面看看 ...

  2. 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用

    欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...

  3. java的对象锁和对象传递

    1.对象传递 在JAVA的參数传递中,有两种类型,第一种是基本类型传递,比如int,float,double等,这样的是值传递,第二种是对象传递,比方String或者自己定义的类,这样的是引用传递. ...

  4. Java的类锁、对象锁和方法锁

    在Java中,对于synchronized关键字,大家看到的第一反应就是这个关键字是进行同步操作的,即得名"同步锁". 当用它来修饰方法和代码块时,默认当前的对象为锁的对象,即对象 ...

  5. Java对象(创建过程、内存布局、访问方法)

    (Java 普通对象.不包括数组.Class 对象等.) ​ 对象创建过程 类加载 遇到 new 指令时,获取对应的符号引用,并检查该符号引用代表的类是否已被初始化.如果没有就进行类加载. 分配内存 ...

  6. Java锁Synchronized对象锁和类锁区别

    java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁.线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁.获得内置锁的唯一途径就是进入这个锁的保 ...

  7. java 对象锁和类锁的区别(转)

    java 对象锁和类锁的区别   转自; ) ); ; ) ); 上述的代码,第一个方法时用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象,当然,如果需要同步其他对象实例,也不可 ...

  8. java基础Synchronized关键字之对象锁

    java中Synchronized关键字之对象锁    当有多个线程对一个共享数据进行操作时,需要注意多线程的安全问题. 多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同 ...

  9. 从头认识java-17.4 具体解释同步(3)-对象锁

    这一章节我们接着上一章节的问题,给出一个解决方式:对象锁. 1.什么是对象锁? 对象锁是指Java为临界区synchronized(Object)语句指定的对象进行加锁,对象锁是独占排他锁. 2.什么 ...

  10. 深入理解 Java 对象的内存布局

    对于 Java 虚拟机,我们都知道其内存区域划分成:堆.方法区.虚拟机栈等区域.但一个对象在 Java 虚拟机中是怎样存储的,相信很少人会比较清楚地了解.Java 对象在 JVM 中的内存布局,是我们 ...

随机推荐

  1. ICPC North Central NA Contest 2018

    目录 ICPC North Central NA Contest 2018 1. 题目分析 2. 题解 A.Pokegene B.Maximum Subarrays C.Rational Ratio ...

  2. ⛅剑指 Offer 11. 旋转数组的最小数字

    20207.22 LeetCode 剑指 Offer 11. 旋转数组的最小数字 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转.输入一个递增排序的数组的一个旋转,输出旋转数组的最小 ...

  3. PHP curl_multi_setopt函数

    (PHP 5 >= 5.5.0) curl_multi_setopt — 设置一个批处理cURL传输选项. 说明 bool curl_multi_setopt ( resource $mh , ...

  4. PHP xml_get_error_code() 函数

    定义和用法 xml_get_error_code() 函数获取 XML 解析器错误代码.高佣联盟 www.cgewang.com 如果成功,该函数则返回错误代码.如果失败,则返回 FALSE. 语法 ...

  5. 搭建Redis主从复制的集群

    在主从复制模式的集群里,主节点一般是一个,从节点一般是两个或多个,写入主节点的数据会被复制到从节点上,这样一旦主节点出现故障,应用系统能切换到从节点去读写数据,这样能提升系统的可用性.而且如果再采用主 ...

  6. 2017面向对象程序设计(Java)第五周工作总结

    时光如逝,岁月如梭,不知不觉已经开学五个星期了.在代老师的带领下,我们一步一步走近Java,也渐渐的适应了翻转课堂的个性化教学,此时此刻相信同学们对Java也有了更加深入的了解.下面我对第五周的助教工 ...

  7. Android 程序间的广播和Manifest找不到(解决方法)

    昨天写的是广播接收端的一些操作, 今天学的是广播的发送,上节介绍的标准广播和有序广播指的是发送端发送后,接收端的广播形式. 既然要发送,那就可以自定义发送广播: 把EditText的内容拿出来广播. ...

  8. PHP7 生产环境队列 Beanstalkd 正确使用姿势

    应用场景 为什么要用呢,有什么好处?这应该放在最开头说,一件东西你只有了解它是干什么的,适合干什么,才能更好的与自己的项目相结合,用到哪里学到哪里,学了不用等于不会,我们平时就应该多考虑一些这样的问题 ...

  9. Python Tricks —— 使用 pywinrm 远程控制 Windows 主机

    启用 WinRM 远程服务: winrm quickconfig 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经做案例的人,却不 ...

  10. Python玩转各种多媒体,视频、音频到图片

    我们经常会遇到一些对于多媒体文件修改的操作,像是对视频文件的操作:视频剪辑.字幕编辑.分离音频.视频音频混流等.又比如对音频文件的操作:音频剪辑,音频格式转换.再比如我们最常用的图片文件,格式转换.各 ...