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

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

相关文章
|
8天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
|
21天前
|
存储 Java 开发者
浅析JVM方法解析、创建和链接
上一篇文章《你知道Java类是如何被加载的吗?》分析了HotSpot是如何加载Java类的,本文再来分析下Hotspot又是如何解析、创建和链接类方法的。
|
30天前
|
PHP 开发者 UED
PHP中的异常处理机制解析####
本文深入探讨了PHP中的异常处理机制,通过实例解析try-catch语句的用法,并对比传统错误处理方式,揭示其在提升代码健壮性与可维护性方面的优势。文章还简要介绍了自定义异常类的创建及其应用场景,为开发者提供实用的技术参考。 ####
|
2月前
|
存储 缓存 监控
后端开发中的缓存机制:深度解析与最佳实践####
本文深入探讨了后端开发中不可或缺的一环——缓存机制,旨在为读者提供一份详尽的指南,涵盖缓存的基本原理、常见类型(如内存缓存、磁盘缓存、分布式缓存等)、主流技术选型(Redis、Memcached、Ehcache等),以及在实际项目中如何根据业务需求设计并实施高效的缓存策略。不同于常规摘要的概述性质,本摘要直接点明文章将围绕“深度解析”与“最佳实践”两大核心展开,既适合初学者构建基础认知框架,也为有经验的开发者提供优化建议与实战技巧。 ####
|
29天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
缓存 NoSQL Java
千万级电商线上无阻塞双buffer缓冲优化ID生成机制深度解析
【11月更文挑战第30天】在千万级电商系统中,ID生成机制是核心基础设施之一。一个高效、可靠的ID生成系统对于保障系统的稳定性和性能至关重要。本文将深入探讨一种在千万级电商线上广泛应用的ID生成机制——无阻塞双buffer缓冲优化方案。本文从概述、功能点、背景、业务点、底层原理等多个维度进行解析,并通过Java语言实现多个示例,指出各自实践的优缺点。希望给需要的同学提供一些参考。
50 7
|
1月前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
1月前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
356 1

热门文章

最新文章

推荐镜像

更多