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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: .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为好.

相关文章
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
14 2
|
13天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
1月前
|
存储 缓存 算法
分布式锁服务深度解析:以Apache Flink的Checkpointing机制为例
【10月更文挑战第7天】在分布式系统中,多个进程或节点可能需要同时访问和操作共享资源。为了确保数据的一致性和系统的稳定性,我们需要一种机制来协调这些进程或节点的访问,避免并发冲突和竞态条件。分布式锁服务正是为此而生的一种解决方案。它通过在网络环境中实现锁机制,确保同一时间只有一个进程或节点能够访问和操作共享资源。
63 3
|
5天前
|
存储 消息中间件 算法
深入探索操作系统的心脏——内核机制解析
本文旨在揭示操作系统核心——内核的工作原理,通过剖析其关键组件与机制,为读者提供一个清晰的内核结构图景。不同于常规摘要的概述性内容,本文摘要将直接聚焦于内核的核心概念、主要功能以及其在系统管理中扮演的角色,旨在激发读者对操作系统深层次运作原理的兴趣与理解。
|
17天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
21 3
|
17天前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
21 2
|
19天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
25 1
|
22天前
|
Java 开发者 UED
Java编程中的异常处理机制解析
在Java的世界里,异常处理是确保程序稳定性和可靠性的关键。本文将深入探讨Java的异常处理机制,包括异常的类型、如何捕获和处理异常以及自定义异常的创建和使用。通过理解这些概念,开发者可以编写更加健壮和易于维护的代码。
|
29天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
1月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
42 3

推荐镜像

更多