java对象在内存中如何分布 | java上锁原来就是内存占位,so easy

2,161

前言

  • 本章节作为java锁章节的开山之作,他的地位绝对是重中之重。java中创建对象通过关键字new直接创建。但是一个对象在内存中占多少字节,每块字节是什么作用,这些相信大家很少关注。如果想学好java多线程我觉得了解对象内存分布是有必要的。
  • 为了可视化分析运行下的Java对象的内存情况,本文使用的是openjdk提供的内存分析工具;并且我们分析前提是64位hotspot虚拟机环境下
 <dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.9</version>  
 </dependency>

对象分布

  • 首先我们得明确一个Java对象在内存中是什么样的结构。这里有人可能会说能有啥结构。就是内存数据二进制存储呗。这样的回答好像说了又好像没说。
  • 这里我们通过一个demo案例来具体讲解。首先我们系统中存在这么一个对象
 @Data
 public class User {
     private int age;
 }
  • 一个User对象仅有一个Int类型的属性。他在内存中不仅仅是表述int类型的数据。图中应为kclass point

image-20211207105206289.png

  • 对于一个普通的Java对象就是上述的数据结构存储在内存中的。在64位系统中markword站8字节;klass point在不开启指针压缩的前提下占8字节,否则占位4字节;这里的内容就是我们对象中的属性,因为Java中属性都是有类型的而每种类型占位也是不一样的,比如上面我们int类型是占位4字节;至于补齐字节是啥意思呢?在64位中要求一个对象在内存中所占字节必须是8的倍数。

image-20211207110314447.png

  • 按照我们补齐字节的公式,能够计算出针对User这个对象在内存中补齐字节是0 ,因为他本身占16字节是8 的倍数。至于为什么是8的倍数呢?一个对象在64为系统中占用字节数必须是8的倍数。因为8B=64b;针对字节单位我整理如下表格
1B8b
1KB1024B
1MB1024KB
1GB1024MB
  • 而我们所说的字节就是B , 计算机最小单元是位就是b。
  • 这里我们通过jol来看下

image-20211207111129194.png

  • 通过JOL我们也可以测测Null对象占多少字节。大家都知道是4字节这里就不测试了
  • 然后我们给User对象增加一个double属性。在看看内存分布

image-20211207111655882.png

  • 上面我们通过jol能够查看到对象在内存中分布情况。但是Java中还存在一种数据类型数组 。数组在内存中分布情况稍微不一样。

image-20211207112145287.png

  • 在对象头中除了markword和klass point以外还会存储数组长度。这里能够看到是4字节的。换句话说Java对象中数组长度最大是2^32(理论上)。
  • 我们知道数组中索引是int类型,而java中int类型最大值是(2^31)-1 。所以数组长度根本用不到2^32这个量级。另外针对数组长度JVM作出了限制最大是(2^31)-2 ;
  • 试想一下如果我们数组中存储的是基本单位最小长度就是4字节。2^31*4计算下来约等于16GB 。 在想想我们平时给JVM分配多大内存空间吧。所以数组长度限制在那个量级上足够我们使用。如果真的到达那个量级了也不是我们需要考虑的事情。首先虚拟机那关就挂了

markword

  • markword是对象内存模型中印出来的概念。由上我们得治在64位系统中markword占8字节。这8字节可是对象很重要的属性。包括对象HashCode、GC次数、锁标记等信息都是存储在markword中的。

image-20211207135807574.png

  • 从左到右依次是高位到低位的顺序。但是在我们的JOL输出的模型中是按照内存的顺序进行输出的,所以低位的反而是先输出。在加上我们是按字节为单位输出的。所以先输出低位字节。

image-20211207140055041.png

  • 比如说上面JOL输出内存模型。第一个输出的是01是十六进制表达式。而01对应的是8位。这8位对应的上图中末8位
  • 01后面的是00 , 他是第二个输出的字节,他对应的是倒数后16位到倒数后8位

image-20211207140936753.png

  • 一个普通对象markword就是上图所示。因为他既没有GC信息也没有产生hashcode ,更没有被锁住,所以内存是0000000000000001 。 注意这是高位到低位显示。下面我们试着调用hashcode试试

image-20211207143925519.png

  • 很是奇怪,在hashcode对应位并没有存储响应的hashcode , 上面已经打印出来hashcode为400c11fa 。 这是为什么呢?最终发现是又因为我的User类用的是lombok注解。修改下就可以了

image-20211207144108863.png

  • 另外我将user对象进行上锁,这时候我们在看看他的markword吧

image-20211207144238473.png

  • 通过观察最后三位,我们很清楚知道当前对象已经上锁且是轻量级锁。关于对象锁的生命周期我们后面详细说说。什么时间是偏向锁、如何转成轻量级锁、最终是重量级锁、还有什么叫自旋锁、无锁到底是不是锁等等问题我们下章见。

指针压缩

  • 上面对象内存分布中我们提到对象头这个概念。对象头=markword+klass point +array length;
  • 但是klass point所占字节是不固定的。如果开启了指针压缩那么他就小点为4字节;否则就是8字节
  • 为什么存在指针压缩?这就牵涉到32位系统和64位系统了

在32位到64位的转变中,我们能够直观的感受到内存容量的变化。在一个32位的系统中,内存地址的宽度就是32位,这就意味着,我们最大能获取的内存空间是2^32(也就是4G)字节。这个容量明显不够用!在一个64位的机器中,理论上,我们能获取到的内存容量是2^64字节,接下来,我们就谈谈compressed oops能帮我们做什么

image-20211207115857599

  • 通过上图我们能够得出几点结论

    1. 64位系统指针变大那么相同内存下存放的指针数量就变少了,同时存储的普通对象就会变少。很容易就触发了GC,可以理解因为内存被指针占用了
    2. 容器存储的指针数量变少了,就导致对用被引用的范围变小了。即CPU缓存命中率变低了
  • 针对上面存在的问题,64系统出现了指针压缩的这个概念;在JVM中我们可以通过-XX:-UseCompressedOops来设置指针压缩关闭。

  • 开启(-XX:+UseCompressedOops) 可以压缩指针。 关闭(-XX:-UseCompressedOops) 可以关闭压缩指针

  • 如果GC堆大小在 4G以下,直接砍掉高32位,避免了编码解码过程;
  • 如果GC堆大小在 4G以上32G以下,则启用 UseCompressedOop;
  • 如果GC堆大小 大于32G,压指失效,使用原来的64位(所以说服务器内存太大不好......)。