.JVM内存分配机制超详细解析 (上)

简介: .JVM内存分配机制超详细解析

一、对象的加载过程


之前研究过类的加载过程。具体详情可查看文章:https://www.cnblogs.com/ITPower/p/15356099.html

那么,当一个对象被new的时候,是如何加载的呢?有哪些步骤,如何分配内存空间的呢?


1.1 对象创建的主要流程


还是这段代码为例说明:


public static void main(String[] args) {
    Math math = new Math();
    math.compute();
    new Thread().start();
}


当我们new一个Math对象的时候,其实是执行了一个new指令创建对象。我们之前研究过类加载的流程,那么创建一个对象的流程是怎样的呢?如下图所示。下面我们一个环节一个环节的分析。

1187916-20211009162114437-1538967719.png


1.1.1类加载检查


当虚拟机执行到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个


符号引用代表的类是否已经被加载、解析和初始化过(也就是检查类是否已经被加载过)。如果没有,那必须先执行相应的类加载流程。


1.1.2分配内存空间


类加载检查通过以后,接下来就是给new的这个对象分配内存空间。对象需要多大内存是在类加载的时候就已经确定了的。为对象分配空间的过程就是从java堆中划分出一块确定大小的内存给到这个对象。那么到底如何划分内存呢?如果存在并发,多个对象同时都想占用同一块内存该如何处理呢?


1)如何给对象划分内存空间?


通常,给对象分配内存有两种方式:一种是指针碰撞,另一种是空闲列表。


  • 指针碰撞


指针碰撞(Bump the Pointer),默认采用的是指针碰撞的方式。如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

1187916-20211009174843972-198757107.png

  • 空闲列表

如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录


1187916-20211009175356567-1846142829.png

不同的内存分配方式,在垃圾回收的时候采用不同的方法。

2)如何解决多个对象并发占用空间的问题?

当有多个线程同时启动的时候,多个线程new的对象都要分配内存,不管内存分配使用的是哪种方式,指针碰撞也好,空闲列表也好,这些对象都要去争抢这块内存。当多个线程都想争抢某一块内存的时候,这时该如何处理呢?通常有两种方式:CAS和本地线程分配缓冲。

  • CAS(compare and swap)

CAS可以理解为多个线程同时去争抢一个快内存,抢到了的就使用,没抢到的就重试去抢下一块内存。

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

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

什么是TLAB呢?简单说,TLAB是为了避免多线程争抢内存,在每个线程初始化的时候,就在堆空间中为线程分配一块专属的内存。自己线程的对象就往自己专属的那块内存存放就可以了。这样多个线程之间就不会去哄抢同一块内存了。jdk8默认使用的就是TLAB的方式分配内存。

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过-XX:+UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB),­-XX:TLABSize 指定TLAB大小。

1.1.3 初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也

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

到这些字段的数据类型所对应的零值。

1.1.4 设置对象头

我们来看看这个类:

public class Math {
    public static int initData = 666;
    public static User user = new User();
    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        new Thread().start();
    }
}


对于一个类,通常我们看到的是成员变量和方法,但并不是说一个类的信息只有我们目光所及的这些内容。在对象初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header中。 在HotSpot虚拟机中,对象在内存中包含3个部分:


  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对象填充(Padding)


实例数据就不多说了,就是我们经常看到的并使用的数据。对象头和填充数据下面我们重点研究。先来说对象头。


1. 对象头的组成部分


HotSpot虚拟机的对象头包括以下几部分信息:


第一部分:Mark Word标记字段,32位占4个字节,64位占8个字节。用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。


第二部分:Klass Pointer类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 开启压缩占4个字节,关闭压缩占8个字节。

第三部分:数组长度,通常是4字节,只有对象数组才有。


2.Mark Word标记字段


如下图所示是一个32位机器的对象头的mark word标记字段。对象不同的状态对应的对象头的结构也是不一样的。根据锁状态划分对象有5种状态,分别是:无状态、轻量级锁、重量级锁、GC标记、偏向锁。

1187916-20211012100329041-636820695.png


无锁状态,就是普通对象的状态。一个对象被new出来以后,没有任何的加锁标记,这时候他的对象头分配是


  • 25位:用来存储对象的hashcode
  • 4位:用来存储分代年龄。之前说过一个新生对象的年龄超过15还没有被回收就会被放入到老年代。为什么年龄设置为15呢?因为分代年龄用4个字节存储,最大就是15了。
  • 1位:存储是否是偏向锁
  • 2位:存储锁标志位


最后这两个就和并发编程有关系了,后面我们会重点研究并发编程的时候研究这一块。


3.Klass Pointer类型指针


在64位机器下,类型指针占8个字节,但是当开启压缩以后,占4个字节

一个对象new出来以后是被放在堆里的,类的元数据信息是放在方法区里的,在new对象的头部有一个指针指向方法区中该类的元数据信息。这个头部的指针就是Klass Pointer。而当代码执行到math.compute()方法调用的时候,是怎么找到compute()方法的呢?实际上就是通过类型指针去找到的。(知道了math指向的对象的地址,再根据对象的类型指针找到方法区中的源代码数据,再从源代码数据中找到compute()方法)。


public static void main(String[] args) {
    Math math = new Math();
    math.compute();
}


1187916-20211011103155719-1334698781.png


对于Math类来说,他还有一个类对象, 如下代码所示:


Class<? extends Math> mathClass = math.getClass();

这个类对象是存储在哪里的呢?这个类对象是方法区中的元数据对象么?不是的。这个类对象实际上是jvm虚拟机在堆中创建的一块和方法区中源代码相似的信息。如下图堆空间右上角。


1187916-20211012193723699-1009376012.png

那么在堆中的类对象和在方法区中的类元对象有什么区别呢?


类的元数据信息是放在方法区的。堆中的类信息,可以理解为是类装载后jvm给java开发人员提供的方便的访问类的信息。通过类的反射我们知道,我们可以通过Math的class拿到这个类的名称,方法,属性,继承关系,接口等等。我们知道jvm的大部分实现是通过c++实现的,jvm在拿到Math类的时候,他不会通过堆中的类信息(上图堆右上角math类信息)拿到,而是直接通过类型指针找到方法区中元数据实现的,这块类型指针也是c++实现的。在方法区中的类元数据信息都是c++获取实现的。而我们java开发人员要想获得类元数据信息是通过堆中的类信息获得的。堆中的class类是不会存储元数据信息的。我们可以吧堆中的类信息理解为是方法区中类元数据信息的一个镜像。


Klass Pointer类型指针的含义:Klass不是class,class pointer是类的指针;而Klass Pointer指的是底层c++对应的类的指针


4.数组长度


如果一个对象是数组的话,除了Mark Word标记字段和Klass Pointer类型指针意外,还会有一个数组长度。用来记录数组的长度,通常占4个字节。

对象头在hotspot的C++源码里的注释如下:


5.对象对齐(Object alignment)


我们上面说了对象有三块:对象头,实体,对象对齐。那么什么是对象对齐呢?

对于一个对象来说,有的时候有对象对齐,有的时候没有。JVM内部会将对象的读取信息按照8个字节对齐。至于为什么要按8个字节对齐呢?这是计算机底层原理了,经过大量的实践证明,对象按照8个字节读取效率会非常高。也就是说,最后要求字节数是8的整数倍。可以是8,16,24,32.


6.代码查看对象结构


如何查看对象的内部结构和大小呢?我们可以通过引用jol-core包,然后调用里面的几个方法即可查看


引入jar包


引入jar包:
  <dependency>
      <groupId>org.openjdk.jol</groupId>
      <artifactId>jol-core</artifactId>
      <version>0.9</version>
  </dependency>

测试代码


import org.openjdk.jol.info.ClassLayout;
/**
 * 查询类的内部结构和大小
 */
public class JOLTest {
    public static void main(String[] args) {
        ClassLayout layout = ClassLayout.parseInstance(new Object());
        System.out.println(layout.toPrintable());
        System.out.println();
        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());
        System.out.println();
        ClassLayout layout2 = ClassLayout.parseInstance(new Object());
        System.out.println(layout2.toPrintable());
    }
    class A {
        int id;
        String name;
        byte b;
        Object o;
    }
}

执行代码运行结果:


java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
com.lxl.jvm.JOLTest$A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           12 f2 00 f8 (00010010 11110010 00000000 11111000) (-134155758)
     12     4                int A.id                                      0
     16     1               byte A.b                                       0
     17     3                    (alignment/padding gap)                  
     20     4   java.lang.String A.name                                    null
     24     4   java.lang.Object A.o                                       null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
Object对象的内部结构:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

这里一共有四行:


  • 前两行是对象头(Mark Word), 占用8个字节;
  • 第三行是Klass Pointer类型指针,占用4个字节,如果不压缩的话会占用8个字节;
  • 第四行是Object Alignment对象对齐,对象对齐是为了保证整个对象占用的位数是8的倍数。


数组对象的内部结构
[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION             VALUE
      0     4        (object header)         01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)         6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

这里一共有5行:


  • 头两行是Mark word标记字段,占了8位;
  • 第三行是Klass Pointer类型指针,占了4位;
  • 第四行是数组特有的,标记数组长度的,占了4位。
  • 第五行是对象对齐object alignment,由于前面4行一共是16位,所以这里不需要进行补齐


A(自定义)对象的内部结构
com.lxl.jvm.JOLTest$A object internals:
 OFFSET  SIZE          TYPE DESCRIPTION      VALUE
      0     4          (object header)       01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4          (object header)       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4          (object header)       12 f2 00 f8 (00010010 11110010 00000000 11111000) (-134155758)
     12     4          int A.id              0
     16     1          byte A.b              0
     17     3          (alignment/padding gap)                  
     20     4   java.lang.String A.name      null
     24     4   java.lang.Object A.o         null
     28     4          (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

这一共有四行:


  • 前两行是对象头(Mark Word), 占用8个字节;
  • 第三行是Klass Pointer类型指针,占用4个字节,如果不压缩的话会占用8个字节;
  • 第四行是int类型 占4位。
  • 第五行是byte类型:占1位。
  • 第六行是byte补位:步3位。
  • 第七行是String类型:占4位
  • 第八行是Object类型:占4位
  • 第九行是object alignment对象对齐补4位。前面28位,不是8的倍数,所以补4位。


1.1.5.执行方法


这里的init方法,不是构造方法,是c++调用的init方法。执行方法,即对象按照程序员的意愿进行初始化,也就是说真正意义上的为属性赋值(注意,这与上面的初始化赋零值不同,这是赋程序员设置的值),并且调用构造方法。


1.1.6 指针压缩


1. 什么是java对象的指针压缩


从jdk1.6开始,在64位操作系统中,jvm默认开启指针压缩。指针压缩就是将Klass Pointer类型指针进行压缩,已经Object对象,String对象进行指针压缩。看下面的例子:


import org.openjdk.jol.info.ClassLayout;
/**
 * 查询类的内部结构和大小
 */
public class JOLTest {
    public static void main(String[] args) {
        System.out.println();
        ClassLayout layout2 = ClassLayout.parseInstance(new A());
        System.out.println(layout2.toPrintable());
    }
    public static class A {
        int id;
        String name;
        byte b;
        Object o;
    }
}

运行这段代码,A的类结构:


com.lxl.jvm.JOLTest$A object internals:
 OFFSET  SIZE          TYPE DESCRIPTION      VALUE
      0     4          (object header)       01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4          (object header)       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4          (object header)       12 f2 00 f8 (00010010 11110010 00000000 11111000) (-134155758)
     12     4          int A.id              0
     16     1          byte A.b              0
     17     3          (alignment/padding gap)                  
     20     4   java.lang.String A.name      null
     24     4   java.lang.Object A.o         null
     28     4          (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

默认情况下是开启指针压缩的。上面分析过这个类结构,这里主要看第三行Klass Pointer和第七行String占4位,第八行Object占4位。我们知道这里保存的都是指针的地址。

下面我们手动设置关闭指针压缩:


指针压缩的命令有两个:UseCompressedOops(压缩所有的指针对象,包括header头和其他) 和 UseCompressedClassPointers(只压缩指针对象)
开启指针压缩:  -XX:+UseCompressedOops(默认开启),
禁止指针压缩:  -XX:-UseCompressedOops 
参数的含义:
          compressed:压缩的意思
          oop(ordinary object pointer):对象指针


在main方法的VM配置参数中设置XX:-UseCompressedOops

1187916-20211011145441439-59096572.png

然后再来看运行结果:

com.lxl.jvm.JOLTest$A object internals:
 OFFSET  SIZE        TYPE DESCRIPTION           VALUE
      0     4        (object header)            01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)            00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)            d0 0c be 26 (11010000 00001100 10111110 00100110) (649989328)
     12     4        (object header)            02 00 00 00 (00000010 00000000 00000000 00000000) (2)
     16     4        int A.id                   0
     20     1        byte A.b                   0
     21     3        (alignment/padding gap)                  
     24     8   java.lang.String A.name         null
     32     8   java.lang.Object A.o            null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

来看变化点:


  • Klass Pointer类型指针原来是4位,现在多了4位。类型指针占了8位
  • String对象原来占用4位,不压缩是8位
  • Object对象原来占用4位,不压缩占用8位


从现象上可以看出压缩和不压缩的区别。那么为什么要进行指针压缩呢?


2.为什么要进行指针压缩?


1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据, 占用较大宽带,同时GC也会承受较大压力(占用内存少,可以存储更多对象,触发GC的频率降低)。为了减少64位平台下内存的消耗,默认启用指针压缩功能 。


2.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)


3.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间


4.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内 存不要大于32G为好.

相关文章
|
15天前
|
PHP 项目管理 开发者
深入解析PHP的命名空间和自动加载机制
【4月更文挑战第4天】 在PHP的编程世界中,命名空间和自动加载机制是构建大型应用程序时不可或缺的工具。本文将深入探讨这两个概念,揭示它们如何简化代码结构、避免类名冲突以及提高代码维护性。通过对PHP命名空间的由来、作用域和使用方法的细致剖析,以及对自动加载机制工作原理和应用实践的全面讲解,读者将获得有效管理复杂项目中依赖关系的能力。
|
21天前
|
存储 缓存 Java
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
34 1
|
21天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
60 0
|
2天前
|
存储 算法 数据安全/隐私保护
深入理解操作系统的内存管理机制
【4月更文挑战第17天】 在现代计算机系统中,操作系统扮演着资源管理者的角色,其中内存管理是其核心职能之一。本文探讨了操作系统内存管理的关键技术,包括虚拟内存、物理内存分配与回收、分页和分段机制,以及内存交换技术。通过分析这些机制的原理和实现,我们旨在加深读者对操作系统如何有效管理和保护内存资源的理解。
6 1
|
3天前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
|
4天前
|
算法
深入理解操作系统的内存管理机制
【4月更文挑战第15天】 本文将探讨操作系统中至关重要的一环——内存管理。不同于通常对内存管理概念的浅尝辄止,我们将深入研究其核心原理与实现策略,并剖析其对系统性能和稳定性的影响。文章将详细阐述分页系统、分段技术以及它们在现代操作系统中的应用,同时比较它们的效率与复杂性。通过本文,读者将获得对操作系统内存管理深层次工作机制的洞见,以及对设计高效、稳定内存管理系统的理解。
|
7天前
|
存储 前端开发 安全
JVM内部世界(内存划分,类加载,垃圾回收)(上)
JVM内部世界(内存划分,类加载,垃圾回收)
38 0
|
7天前
|
算法 Linux 调度
深度解析:Linux内核的进程调度机制
【4月更文挑战第12天】 在多任务操作系统如Linux中,进程调度机制是系统的核心组成部分之一,它决定了处理器资源如何分配给多个竞争的进程。本文深入探讨了Linux内核中的进程调度策略和相关算法,包括其设计哲学、实现原理及对系统性能的影响。通过分析进程调度器的工作原理,我们能够理解操作系统如何平衡效率、公平性和响应性,进而优化系统表现和用户体验。
18 3
|
11天前
|
存储 缓存 算法
深度解析JVM世界:垃圾判断和垃圾回收算法
深度解析JVM世界:垃圾判断和垃圾回收算法
|
11天前
|
存储 算法 安全
深度解析JVM世界:JVM内存分配
深度解析JVM世界:JVM内存分配

推荐镜像

更多