精华内容
下载资源
问答
  • 64位JVMJava对象头详解

    千次阅读 2020-02-22 15:42:31
    关注“Java艺术”一起来充电吧!我们编写一个Java类,编译后会生成.class文件,当类加载器将class文件加载到jvm时,会生成一个Klass类型的对象(c++),称为类描述元数...

     

    关注“Java艺术”一起来充电吧!

    我们编写一个Java类,编译后会生成.class文件,当类加载器将class文件加载到jvm时,会生成一个Klass类型的对象(c++),称为类描述元数据,存储在方法区中,即jdk1.8之后的元数据区。当使用new创建对象时,就是根据类描述元数据Klass创建的对象oop,存储在堆中。每个java对象都有相同的组成部分,称为对象头。

    在学习并发编程知识synchronized时,我们总是难以理解其实现原理,因为偏向锁、轻量级锁、重量级锁都涉及到对象头,所以了解java对象头是我们深入了解synchronized的前提条件。

     

    1

    查看对象头的神器

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

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

    ClassLayout.parseInstance(obj).instanceSize()
    

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

    ClassLayout.parseInstance(obj).toPrintable()
    

     

    2

    使用JOL查看对象的内存布局

    网络搜索了很多资料,对64jvmJava对象头的布局讲解的都很模糊,很多资料都是讲的32位,而且很多都是从一些书上摘抄下来的,难免会存在错误的地方,所以最好的学习方法就是自己去验证,看jvm源码。本篇将详细介绍64jvmJava对象头。

    User类为例

    public class User {
        private String name;
        private Integer age;
        private boolean sex;
    }
    

    通过jol查看User对象的内存布局

    User user = new User()
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
    

    输出内容如下

    • OFFSET:偏移地址,单位字节;

    • SIZE:占用的内存大小,单位为字节;

    • TYPE DESCRIPTION:类型描述,其中object header为对象头;

    • VALUE:对应内存中当前存储的值;

    从图中可以看到,对象头所占用的内存大小为16*8bit=128bit。如果大家自己动手去打印输出,可能得到的结果是96bit,这是因为我关闭了指针压缩。jdk8版本是默认开启指针压缩的,可以通过配置vm参数关闭指针压缩。

    -XX:-UseCompressedOops
    

    现在取消关闭指针压缩的配置,开启指针压缩之后,再看User对象的内存布局。

    开启指针压缩可以减少对象的内存使用。从两次打印的User对象布局信息来看,关闭指针压缩时,name字段和age字段由于是引用类型,因此分别占8个字节,而开启指针压缩之后,这两个字段只分别占用4个字节。因此,开启指针压缩,理论上来讲,大约能节省百分之五十的内存。jdk8及以后版本已经默认开启指针压缩,无需配置。

    从两次打印的User对象的内存布局,还可以看出,bool类型的age字段只占用1个字节,但后面会跟随几个字节的浪费,即内存对齐。开启指针压缩情况下,age字段的内存对齐需要3个字节,而关闭指针压缩情况下,则需要7个字节。

    以默认开启指针压缩情况下的User对象的内存布局来看,对象头占用12个字节,那么这12个字节存储的是什么信息,我们不看网上的资料,而是看jdk的源码。

     

    3

    openjdk源码下载或在线查看

    官网:http://openjdk.java.net/

    我当前使用的jdk版本是jdk1.8,可通过命令行java -version查看,也可通过下面方式查看,这个大家应该都很熟悉了。

    System.out.println(System.getProperties());
    

    打开官网后点击左侧菜单栏的Groups找到HotSpot,在打开的页面的Source code选择Browsable souce,之后会跳转到http://hg.openjdk.java.net/,选择jdk8u,跳转后的页面中继续选择jdk8u下面的hotspot

    传送链接:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/。

    • 点击zip可将源码打包下载;

    • 点击browse可在线查看源码;

     

    4

    Java对象头详解

    在开启指针压缩的情况下,User对象的对象头占用12个字节,本节我们通过源码了解对象头都存储了哪些信息。

    Java程序运行的过程中,每创建一个新的对象,JVM就会相应地创建一个对应类型的oop对象,存储在堆中。如new User(),则会创建一个instanceOopDesc,基类为oopDesc

    [instanceOop.hpp文件:hotspot/src/share/vm/oops/instanceOop.hpp]
    class instanceOopDesc : public oopDesc {
    }
    

    instanceOopDesc只提供了几个静态方法,如获取对象头大小。因此重点看其父类oopDesc

    [oop.hpp文件:hotspot/src/share/vm/oops/oop.hpp]
    class oopDesc {
       friend class VMStructs;
     private:
      volatile markOop  _mark;
      union _metadata {
        Klass*      _klass;
        narrowKlass _compressed_klass;
      } _metadata;
      ........
    }
    

    我们只关心对象头,普通对象(如User对象,本篇不讲数组类型)的对象头由一个markOop和一个联合体组成,markOop就是MarkWord。这个联合体是指向类的元数据指针,未开启指针压缩时使用_klass,开启指针压缩时使用_compressed_klass

    markOopnarrowKlass的类型定义在/hotspot/src/share/vm/oops/oopsHierarchy.hpp头文件中:

    [oopsHierarchy.hpp头文件:/hotspot/src/share/vm/oops/oopsHierarchy.hpp]
    typedef juint  narrowKlass;
    typedef class markOopDesc* markOop;
    

    因此,narrowKlass是一个juintjunit是在globalDefinitions_visCPP.hpp头文件中定义的,这是一个无符号整数,即4个字节。所以开启指针压缩之后,指向Klass对象的指针大小为4字节。

    [/hotspot/src/share/vm/utilties/globalDefinitions_visCPP.hpp]
    typedef unsigned int juint;
    

    markOop则是markOopDesc类型指针,markOopDesc就是MarkWord。不知道你们有没有感觉到奇怪,在64jvm中,markOopDesc指针是8字节,即64bit,确实刚好是MarkWord的大小,但是指针指向的不是一个对象吗?我们先看markOopDesc类。

    [markOop.hpp文件:hotspot/src/share/vm/oops/markOop.hpp]
    //  64 bits:
    //  --------
    //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    //  size:64 ----------------------------------------------------->| (CMS free block)
    class markOopDesc: public oopDesc {
     ......
    }
    

    markOop.hpp头文件中给出了64bitMarkWord存储的信息说明。markOopDesc类也继承oopDesc。如果单纯的看markOopDesc类的源码,根本找不出来,markOopDesc是用那个字段存储MarkWord的。而且,根据从各种来源的资料中,我们所知道的是,对象头的前8个字节存储的就是是否偏向锁、轻量级锁等等信息(全文都是以64位为例),所以不应该是个指针啊。

    为了解答这个疑惑,我是先从markOopDesc类的源码中,找一个方法,比如,获取gc对象年龄的方法,看下jvm是从哪里获取的数据。

    class markOopDesc: public oopDesc {
    public:
     // 获取对象年龄
      uint  age() const {
        return mask_bits(value() >> age_shift, age_mask);
      }
      // 更新对象年龄
      markOop set_age(uint v) const {
        return markOop((value() & ~age_mask_in_place) | (((uintptr_t)v & age_mask) << age_shift));
      }
      // 自增对象年龄
      markOop incr_age() const {
        return age() == max_age ? markOop(this) : set_age(age() + 1);
      }
    }
    

    那么,value()这个方法返回的就是64bitMarkWord了。

    class markOopDesc: public oopDesc {
     private:
      // Conversion
      uintptr_t value() const { return (uintptr_t) this; }
    }
    

    value方法返回的是一个指针,就是this。从set_ageincr_age方法中也可以看出,只要修改MarkWord,就会返回一个新的markOopmarkOopDesc*)。难怪会将markOopDesc*定义为markOop,就是将markOopDesc*当成一个8字节的整数来使用。想要理解这个,我们需要先补充点c++知识,因此我写了个demo

    自定义一个类叫oopDesc,并且除构造函数和析构函数之外,只提供一个Show方法。

    [.hpp文件]
    #ifndef oopDesc_hpp
    #define oopDesc_hpp
    
    #include <stdio.h>
    
    #include <iostream>
    using namespace std;
    
    // 将oopDesc* 定义为 oop
    typedef class oopDesc* oop;
    
    class oopDesc{
    public:
        void Show();
    };
    #endif /* oopDesc_hpp */
    
    [.cpp文件]
    #include "oopDesc.hpp"
    
    void oopDesc::Show(){
        cout << "oopDesc by wujiuye" <<endl;
    }
    

    使用oop(指针)创建一个oopDesc*,并调用show方法。

    #include <iostream>
    #include "oopDesc.hpp"
    using namespace std;
    int main(int argc, const char * argv[]) {
        oopDesc* o = oop(0x200);
        cout << o << endl;
        o->Show();
        return 0;
    }
    

    测试输出

    0x200
    oopDesc by wujiuye
    Program ended with exit code: 0
    

    因此,通过类名(value)可以创建一个野指针对象,将指针赋值为value,这样就可以使用this作为MarkWord了。如果在oopDesc中添加一个字段,并提供一个方法访问,程序运行就会报错,因此,这样创建的对象只能调用方法,不能访问字段。

    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对象的hashcode0x7a46a697。由于MarkWord其实是一个指针,在64jvm下占8字节。因此MarkWordk0x0000007a46a69701,跟你从图中看到的正好相反。这里涉及到一个知识点“大端存储与小端存储”。

    • 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);
        }
    }
    

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

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

     

    案例2

    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();
                }
            }
        };
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 3; i++) {
                new Thread(RUNNABLE).start();
            }
            Thread.sleep(Integer.MAX_VALUE);
        }
    }
    

    从该对象头中分析加锁信息,MarkWordk0x0000 7ff0 c800 53ea,二进制为0xb00000000 00000000 01111111 11110000 11001000 00000000 01010011 11101010

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

     

    公众号:Java艺术

    展开全文
  • 目录 一、JAVA内存结构 1.1 JVM启动流程: 1.2 JVM基本结构 1.2.1基本结构图 1.2.2 Java中的内存分配 ...2.3 java内存模型对并发提供的保障:原子性、可见性。...三、Java对象模型 3.1 oop-klass...

    目录

    一、JAVA内存结构

    1.1 JVM启动流程:

    1.2 JVM基本结构

    1.2.1基本结构图

    1.2.2 Java中的内存分配

    二、Java内存模型

    2.1 主内存和工作内存

    2.2 内存间交互操作

    2.3 java内存模型对并发提供的保障:原子性、可见性。有序性

    2.4 先行发生原则

    2.5 volatile型变量

    三、Java对象模型

    3.1 oop-klass model

    3.2 Klass体系

    3.3 InstanceClass

    3.4 内存存储

    四、总结


    Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的Java内存结构、Java内存模型和Java对象模型,这就是三个截然不同的概念,但是很多人容易弄混。

    可以这样说,很多高级开发甚至都搞不不清楚JVM内存结构、Java内存模型和Java对象模型这三者的概念及其间的区别。甚至我见过有些面试官自己也搞的不是太清楚。不信的话,你去网上搜索Java内存模型,还会有很多文章的内容其实介绍的是JVM内存结构。


    一、JAVA内存结构

    Java内存结构是每个java程序员必须掌握理解的,这是Java的核心基础。由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。

    1.1 JVM启动流程:

    JVM启动时,是由java命令/javaw命令来启动的。

    1.2 JVM基本结构

    1.2.1基本结构图

    1、类加载子系统:负责从文件系统或者网络加载Class信息,加载的信息存放在一块称之方法区的内存空间。
    2、方法区:就是存放类的信息、常量信息、常量池信息、包括字符串常量和数字常量等,还存放静态代码
    3、Java堆:在Java虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放到
    Java堆中,还有定义的数组也存放在堆里面堆空间是所有线程共享
    4、直接内存:JavaNio库允许Java程序直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
    5、Java栈:存放基本数据类型,局部变量每个虚拟机线程都有一个私有栈,一个线程的Java栈在线程创建的时候被创建,Java栈保存着局部变量、方法参数、同事Java的方法调用、
    返回值等。
    (解释:线程私有)
    6、本地方法栈,(Java语言调用外部语言(C语言),方法使用native(native关键字在CAS底层代码中也有用到))最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通过使用C语言写)
    8、PC(Program Couneter)寄存器也是每个线程私有的空间, Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,
    如果是本地方法,则PC寄存器值为Underfined,寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。
    9、虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。

    1.2.2 Java中的内存分配

    Java程序在运行时,需要在内存中的分配空间。为了提高运算效率,就对数据进行了不同空间的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

    具体划分为如下5个内存空间:(非常重要)

    • :存放局部变量
    • :存放所有new出来的东西
    • 方法区:被虚拟机加载的类信息、常量、静态常量等。
    • 程序计数器(和系统相关)
    • 本地方法栈

    1、程序计数器:

    每个线程拥有一个PC寄存器

    在线程创建时创建

    指向下一条指令的地址

    执行本地方法时,PC的值为undefined

    2、方法区: 

    保存装载的类信息

      类型的常量池

      字段,方法信息

      方法字节码

    通常和永久区(Perm)关联在一起

    3、堆内存:

    和程序开发密切相关

    应用系统对象都保存在Java堆中

    所有线程共享Java堆

    对分代GC来说,堆也是分代的

    GC管理的主要区域

    现在的GC基本都采用分代收集算法,如果是分代的,那么堆也是分代的。如果堆是分代的,那堆空间应该是下面这个样子:

     

    上图是堆的基本结构,在之后的文章中再进行详解。

    4、栈内存:

    • 线程私有,生命周期和线程相同
    • 栈由一系列帧组成(因此Java栈也叫做帧栈)
    • 帧保存一个方法的局部变量、操作数栈、常量池指针
    • 每一次方法调用创建一个帧,并压栈

    解释:

    Java虚拟机栈描述的是Java方法执行的内存模型:每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

    在Java虚拟机规范中,对这个区域规定了两种异常情况:

    • 如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
    • 虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM

    Java栈之局部变量表:包含参数和局部变量

        局部变量表存放了基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配。

    例如,我写出下面这段代码:

    public class StackDemo {
        
        //静态方法
        public static int runStatic(int i, long l, float f, Object o, byte b) {
            return 0;
        }
    
        //实例方法
        public int runInstance(char c, short s, boolean b) {
            return 0;
        }
    
    }

    上方代码中,静态方法有6个形参,实例方法有3个形参。其对应的局部变量表如下:

    上方表格中,静态方法和实例方法对应的局部变量表基本类似。但有以下区别:实例方法的表中,第一个位置存放的是当前对象的引用。

    Java栈之函数调用组成栈帧

    方法每次被调用的时候都会创建一个栈帧,例如下面这个方法:

    public static int runStatic(int i,long l,float  f,Object o ,byte b){
           return runStatic(i,l,f,o,b);
    }

    当它每次被调用的时候,都会创建一个帧,方法调用结束后,帧出栈。如下图所示:

    Java栈之操作数栈

    Java没有寄存器,所有参数传递都是使用操作数栈

    例如下面这段代码:

     public static int add(int a,int b){
            int c=0;
            c=a+b;
            return c;
        }

    压栈的步骤如下:

      0:   iconst_0 // 0压栈

      1:   istore_2 // 弹出int,存放于局部变量2

      2:   iload_0  // 把局部变量0压栈

      3:   iload_1 // 局部变量1压栈

      4:   iadd      //弹出2个变量,求和,结果压栈

      5:   istore_2 //弹出结果,放于局部变量2

      6:   iload_2  //局部变量2压栈

      7:   ireturn   //返回

    如果计算100+98的值,那么操作数栈的变化如下图所示:

    Java栈之栈上分配:

    小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上

    直接分配在栈上,可以自动回收,减轻GC压力

    大对象或者逃逸对象无法栈上分配

    栈、堆、方法区交互:

    记住了:jvm内存结构 = java内存结构

    二、Java内存模型

    JMM目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节(这里变量指代的是实例字段、静态字段和构成数组对象的元素)

    学习JMM可以和内存模型(处理器、高速缓存器、主内存空间)对等起来:内存模型是为了解决处理器和内存之间的速度差异问题,JMM是为了解决线程之间的信息共享,数据一致性问题

    类比:主内存对应硬件系统中的内存部分,工作空间对应高速缓存

    2.1 主内存和工作内存

        (1)所有变量均存储在主内存(虚拟机内存的一部分)

        (2)每个线程都对应着一个工作线程,主内存中的变量都会复制一份到每个线程的自己的工作空间,线程对变量的操作都在自己的工作内存中,操作完成后再将变量更新至主内存;

        (3)其他线程再通过主内存来获取更新后的变量信息,即线程之间的交流通过主内存来传递

    Note:JMM的空间划分和JVM的内存划分不一样,非要对应的话,关系如下:

        (1)JMM的主内存对应JVM中的堆内存对象实例数据部分

        (2)JMM的工作内存对应JVM中栈中部分区域

    每一个线程有一个工作内存。工作内存和主存独立。工作内存存放主存中变量的值的拷贝。

    当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;

    当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。

    每一个操作都是原子的,即执行期间不会被中断

    对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。如果需要在其他线程中立即可见,需要使用volatile关键字作为标识。

    指令重排:

    指令重排:破坏了线程间的有序性:

    指令重排:保证有序性的方法:

    指令重排的基本原则:

    程序顺序原则:一个线程内保证语义的串行性

    volatile规则:volatile变量的写,先发生于读

    锁规则:解锁(unlock)必然发生在随后的加锁(lock)前

    传递性:A先于B,B先于C 那么A必然先于C

    线程的start方法先于它的每一个动作

    线程的所有操作先于线程的终结(Thread.join())

    线程的中断(interrupt())先于被中断线程的代码

    对象的构造函数执行结束先于finalize()方法

    2.2 内存间交互操作

        JMM定义了8种操作(原子操作),虚拟实现时保证这8中操作均为原子操作,以下为8中操作的介绍以及执行顺序:

        (1)lock(锁定):作用于主内存的变量,把一个变量标志为一个线程占有状态(锁定)

        (2)unlock(解锁):作用于主内存的变量,把一个变量从一个线程的锁定状态解除,以便其他线程锁定

        (3)read(读取):作用于主内存的变量,将变量从主内存读取到线程的工作空间,以便后续load操作使用

        (4)load(载入):作用于工作空间的变量,将load操作从主内存得到的变量放入工作内存变量副本中

        (5)use(使用):作用于工作空间的变量,将工作空间中的变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将执行这个操作。

        (6)assign(赋值):作用于工作内存的变量,把一个从执行引擎接收的值赋给工作空间的变量

        (7)store(存储):作用于工作内存的变量,把工作空间的一个变量传到主内存,以便后续write操作使用

        (8)write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中
    工作示意图:

    Note:这些操作之间是存在一些冲突的,需保持顺序执行

    有关操作的一些规定:

    (1)不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况。

    (2)不允许一个线程丢弃它的最近assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

    (3)不允许一个线程无原因的(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。即不能对变量没做任何操作却无原因的同步回主内存

    (4)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,就是对一个变量执行use和store之前必须先执行过了load和assign操作

    (5)一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

    (6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值

    (7)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

    (8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store write)

    Note:以上可以完全确定Java程序中哪些内存访问操作在并发下是安全的。

    2.3 java内存模型对并发提供的保障:原子性、可见性。有序性

    (1)原子性:

        Java内存模型直接保证得原子性操作包括read、load、use、assign、store和write这六个。可以认为基本数据类型(long和double除外,64字节在32位上需要两部操作)的变量操作是具有原子性的,而lock和unlock对应在高层次的字节码指令monitorenter和monitorexit来隐式的使用底层的这两个操作,高层次的这两个字节码指令在Java中就是同步块,Synchronized关键字,因此在synchronized块之间的操作也具备原子性。

    (2)可见性

        可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

        除了volatile外,Synchronized和final也提供了可见性,

    Synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得

    Final的可见性是指被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了半”的对象),那在其他线程就能看见final字段的值

    (3)有序性

        Java程序在本线程所有操作是有序的(线程表现为串行的),在一个线程中看另一个线程是无序的(指令重排和工作内存与主内存同步延迟现象),可以用Synchronized和volatile来保证线程操作的有序性

    Volatile本身就包含禁止指令重排序的语义

    Synchronized是因为:一个变量在同一时刻只能被一个线程lock,即串行化操作保证有序

    2.4 先行发生原则

        先行发生原则是java内存模型中定义的两项操作之间的偏序关系,这个原则作为依据来判断是否存在线程安全和竞争问题,如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。以下为8个具体原则:

        (1)程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

        (2)传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

        (3)对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

        (4)管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

        (5)volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

        (6)线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作

        (7)线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行

        (8)线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

    2.5 volatile型变量

        Volatile是java虚拟机提供的最轻量级的同步机制,它具有可见性和有序性,但不保证原子性,在大多数场景下,volatile总开销仍然比锁要低

    volatile是强制从主内存(公共堆)中取得变量的值,而不是从线程的私有堆栈中取得变量的值。如下图所示
     

    volatile保证了变量的新值能立即同步到主内存,以及每次使用之前立即从主内存刷新。因此可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点

    (1)volatile可以保证变量对所有线程的可见性,即一条线程修改了变量的值,新值对于其他线程来说是可以立即得知的。volatile变量在各个线程的工作内存中不存在一致性问题(即时存在,由于每次使用之前都得刷新,执行引擎看不到不一致的情况,所以认为是 不存在一致性问题)volatile实际上就使用到了内存屏障技术来保证其变量的修改对其他CPU立即可见。

    (2)volatile禁止指令重排,保证有序性

    *Volatile不能保证的原子性操作有以下两条:

    (1)对变量的写入操作不依赖于该变量的当前值(比如a=0;a=a+1的操作,整个流程为a初始化为0,将a的值在0的基础之上加1,然后赋值给a本身,很明显依赖了当前值),或者确保只有单一线程修改变量。

    (2)该变量不会与其他状态变量纳入不变性条件中,(当变量本身是不可变时,volatile能保证安全访问,比如双重判断的单例模式。但一旦其他状态变量参杂进来的时候,并发情况就无法预知,正确性也无法保障)

    三、Java对象模型

    在内存中,一个Java对象包含三部分:对象头、实例数据和对齐填充。而对象头中又包含锁状态标志、线程持有的锁等标志。

    3.1 oop-klass model

    OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。

    oop体系:

    //定义了oops共同基类
    typedef class   oopDesc*                            oop;
    //表示一个Java类型实例
    typedef class   instanceOopDesc*            instanceOop;
    //表示一个Java方法
    typedef class   methodOopDesc*                    methodOop;
    //表示一个Java方法中的不变信息
    typedef class   constMethodOopDesc*            constMethodOop;
    //记录性能信息的数据结构
    typedef class   methodDataOopDesc*            methodDataOop;
    //定义了数组OOPS的抽象基类
    typedef class   arrayOopDesc*                    arrayOop;
    //表示持有一个OOPS数组
    typedef class   objArrayOopDesc*            objArrayOop;
    //表示容纳基本类型的数组
    typedef class   typeArrayOopDesc*            typeArrayOop;
    //表示在Class文件中描述的常量池
    typedef class   constantPoolOopDesc*            constantPoolOop;
    //常量池告诉缓存
    typedef class   constantPoolCacheOopDesc*   constantPoolCacheOop;
    //描述一个与Java类对等的C++类
    typedef class   klassOopDesc*                    klassOop;
    //表示对象头
    typedef class   markOopDesc*                    markOop;

    如上面代码所示, oops模块包含多个子模块, 每个子模块对应一个类型, 每一个类型的oop都代表一个在JVM内部使用的特定对象的类型。其中有一个变量oop的类型oopDesc是oops模块的共同基类型。而oopDesc类型又包含instanceOopDesc (类实例)、arrayOopDesc (数组)等子类类型。其中instanceOopDesc 中主要包含以下几部分数据:markOop _mark和union _metadata 以及一些不同类型的 field。

    在java程序运行过程中, 每创建一个新的java对象, 在JVM内部就会相应的创建一个对应类型的oop对象来表示该java对象。而在HotSpot虚拟机中, 对象在内存中包含三块区域: 对象头、实例数据和对齐填充。其中对象头包含两部分内容:_mark和_metadata,而实例数据则保存在oopDesc中定义的各种field中。

    _mark:

    _mark这一部分用于存储对象自身的运行时数据, 如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等, 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit, 官方称它为 "Mark Word"。对象需要存储的运行时数据很多, 其实已经超出了32位和64位Bitmap结构所能记录的限度, 但是对象头信息是与对象自身定义的数据无关的额外存储成本, 考虑到虚拟机的空间效率, Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息, 它会根据对象的状态复用自己的存储空间。  

    _metadata:

    _metadata这一部分是类型指针, 即对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针, 换句话说查找对象的元数据信息并不一定要经过对象本身, 其取决于虚拟机实现的对象访问方式。目前主流的访问方式有使用句柄和直接指针两种, 两者方式的不同这里先暂不做介绍。另外, 如果对象是一个Java数组, 那么在对象头中还必须有一块用于记录数组长度的数据, 因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小, 但是从数组的元数据中却无法确定数组的大小。  

    3.2 Klass体系

    //klassOop的一部分,用来描述语言层的类型
    class  Klass;
    //在虚拟机层面描述一个Java类
    class   instanceKlass;
    //专有instantKlass,表示java.lang.Class的Klass
    class     instanceMirrorKlass;
    //专有instantKlass,表示java.lang.ref.Reference的子类的Klass
    class     instanceRefKlass;
    //表示methodOop的Klass
    class   methodKlass;
    //表示constMethodOop的Klass
    class   constMethodKlass;
    //表示methodDataOop的Klass
    class   methodDataKlass;
    //作为klass链的端点,klassKlass的Klass就是它自身
    class   klassKlass;
    //表示instanceKlass的Klass
    class     instanceKlassKlass;
    //表示arrayKlass的Klass
    class     arrayKlassKlass;
    //表示objArrayKlass的Klass
    class       objArrayKlassKlass;
    //表示typeArrayKlass的Klass
    class       typeArrayKlassKlass;
    //表示array类型的抽象基类
    class   arrayKlass;
    //表示objArrayOop的Klass
    class     objArrayKlass;
    //表示typeArrayOop的Klass
    class     typeArrayKlass;
    //表示constantPoolOop的Klass
    class   constantPoolKlass;
    //表示constantPoolCacheOop的Klass
    class   constantPoolCacheKlass;

    和oopDesc是其他oop类型的父类一样,Klass类是其他klass类型的父类。

    Klass向JVM提供两个功能:

    • 实现语言层面的Java类(在Klass基类中已经实现)
    • 实现Java对象的分发功能(由Klass的子类提供虚函数实现)

    HotSpot JVM的设计者因为不想让每一个对象中都含有一个虚函数表, 所以设计了oop-klass模型, 将对象一分为二, 分为klass和oop。其中oop主要用于表示对象的实例数据, 所以不含有任何虚函数。而klass为了实现虚函数多态, 所以提供了虚函数表。所以,关于Java的多态,其实也有c++虚函数的影子在。

    3.3 InstanceClass

    VM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个InstanceClass对象,用来在JVM层表示Java类。

    InstanceClass内部结构:

    //类拥有的方法列表
    objArrayOop     _methods;
    //描述方法顺序
    typeArrayOop    _method_ordering;
    //实现的接口
    objArrayOop     _local_interfaces;
    //继承的接口
    objArrayOop     _transitive_interfaces;
    //域
    typeArrayOop    _fields;
    //常量
    constantPoolOop _constants;
    //类加载器
    oop             _class_loader;
    //protected域
    oop             _protection_domain;
        ....

    在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点, 它的klass就是它自身。

    3.4 内存存储

    我们首先来看看下面这段代码的存储结构。

    class Model
    {
        public static int a = 1;
        public int b;
    
        public Model(int b) {
            this.b = b;
        }
    }
    
    public static void main(String[] args) {
        int c = 10;
        Model modelA = new Model(2);
        Model modelB = new Model(3);
    }

    存储结构如下:

    由此我们能得出结论: 对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。

    在JVM加载java类的时候, JVM会给这个类创建一个instanceKlass并保存在方法区, 用来在JVM层表示该java类。当我们使用new关键字创建一个对象时, JVM会创建一个instanceOopDesc对象, 这个对象包含了对象头和元数据两部分信息。对象头中有一些运行时数据, 其中就包括和多线程有关的锁的信息。而元数据维护的则是指向对象所属的类的InstanceKlass的指针。

    四、总结

    我们再来区分下JVM内存结构、 Java内存模型 以及 Java对象模型 三个概念。
    JVM内存结构,和Java虚拟机的运行时区域有关。
    Java内存模型,和Java的并发编程有关。
    Java对象模型,和Java对象在虚拟机中的表现形式有关。

    关于这三部分内容,本文并未分别展开,因为涉及到的知识点实在太多,如果读者感兴趣,可以自行学习。

    最后,这三个概念非常重要,一定要严格区分开,千万不要在面试中出现答非所为的情况。

     

     

     

    展开全文
  • 二、对象的内部结构(内存分配) 1、对象头 hashcode GC分代年龄 线程编号 锁编号 时间戳 引用计数 … 2、实例数据   存储所定义的各种类型字段内容,无论是从父类继承下来,还是在子类中定义的,都需要记录起来...

    一、对象的创建过程

    在这里插入图片描述

    二、对象的内部结构(内存分配)

    1、对象头

    • hashcode
    • GC分代年龄
    • 线程编号
    • 锁编号
    • 时间戳
    • 引用计数

    2、实例数据

      存储所定义的各种类型字段内容,无论是从父类继承下来,还是在子类中定义的,都需要记录起来。

    3、对齐填充

      起着占位符的作用,实例数据起始地址必须是8字节的整数倍,(对像的大小必须是8字节的整数倍)。

    三、对象的访问

      通过栈上的reference引用数据来访问堆上的实例数据。

    1、直接指针

      reference引用存储是对象地址,直接指向对象的实例数据,同时一个存储一个指向对象类型数据的指针。

    2、句柄

      reference引用存储是句柄地址,句柄地址存储两个对象和类型数据两个指针,一个指向对象实例数据,一个指向对象类型数据。
    在这里插入图片描述

    展开全文
  • 大家都知道,在java中,对象的实例是在堆内存中存储,那么一个对象实例在堆内存中是一种怎样的结构呢 一个Object对象实例是多少字节? 带着这个疑问我们来探讨一下jvm的底层原理 首先,Object没有成员变量,对象的...

    大家都知道,在java中,对象的实例是在堆内存中存储,那么一个对象实例在堆内存中是一种怎样的结构呢

    一个Object对象实例是多少字节?

    带着这个疑问我们来探讨一下jvm的底层原理
    首先,Object没有成员变量,对象的方法都是存放在方法区里的代码区里,我们通过程序可以看到,new 一个Object对象,就会有一个堆空间的地址赋给栈里的变量名,所以,肯定是在堆空间里开辟了一块内存空间的。

    通过查阅资料得知,栈空间存的只是一个内存地址,这个地址在32为系统中是占用4字节(32位/8位=字节),在64位系统中是占用8字节。
    堆空间在32位系统中占8字节,在64位系统中占用16字节。其实这里面到底存了什么呢?

    这里面存放了两个信息,一个是指向类代码区地址,还有一个是存放对象实例的状态信息。

    对象实例在堆内存中的结构如图所示:
    在这里插入图片描述
    详细介绍如图所示:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    展开全文
  • 比如本文我们要讨论的JVM内存结构、Java内存模型和Java对象模型,这就是三个截然不同的概念,但是很多人容易弄混。 可以这样说,很多高级开发甚至都搞不不清楚JVM内存结构、Java内存模型和Java对象模型这三者的...
  • 对象头的内部结构一张图了解所有细节 1、创建了Customer()实例 和 Account()实例 2、对象头里包括:运行时元数据、类型指针、实例数据、对齐填充 ① 运行时元数据里又包括:哈希值(HashCode)、GC分代年龄、锁状态...
  • JVM(一):Java对象的存储结构

    千次阅读 2018-07-03 10:55:22
    一、Java对象在内存中结构在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象(Header)、实例数(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构: 1、对象...
  • java对象结构

    万次阅读 多人点赞 2017-04-19 22:31:57
    对象结构 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构: 对象 HotSpot...
  • java对象头信息

    千次阅读 多人点赞 2019-09-02 14:27:14
    1. 一个java对象到底占用了多少内存空间,应该如何计算? 2. 为什么在jdk1.6后,synchronized关键字性能有所提高,为什么会提高?并且很多文章中都说synchronized锁有偏向锁、轻量锁、重量锁等状态? 3. java对象...
  • Java对象头对象组成详解

    万次阅读 多人点赞 2018-07-20 10:46:47
    Java对象保存在内存中时,由以下三部分组成: 1,对象 2,实例数据 3,对齐填充字节 一,对象 java的对象由以下三部分组成: 1,Mark Word 2,指向类的指针 3,数组长度(只有数组对象才有)   ...
  • java对象结构与Monitor工作原理

    千次阅读 2020-11-06 16:36:06
    这里写目录标题简介对象头 简介 对象头
  • JVM成神之路-Java对象模型

    千次阅读 2018-07-23 15:01:17
    一个Java对象可以分为三部分存储在内存中,分别是:对象(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头(包含锁状态标志,线程持有的锁等标志) 实例数据 对齐填充 oop-klass model(...
  • 哈希值:它是一个地址,用于栈对堆空间中对象的引用指向,不然栈是无法找到堆中对象的 GC分代年龄:记录幸存者区对象被GC之后的年龄age,,一般age为15之后下一次GC就会直接进入老年代 锁状态标志:记录一些加锁的...
  • Java对象结构详解

    2019-11-18 19:34:52
    synchronized (obj) { ...要弄清楚这个问题,就有必要了解一下在JVM虚拟机中一个Java对象是怎么存在的,换句话说就是在虚拟机中用什么结构来表示一个Java对象,或者一个Java对象的组成结构是什么样的。 划重点~~...
  • 今天看别人的博客,讲到面试相关的问题,其中有一个知识点是:synchronized关键字,Java对象头、Markword概念、synchronized底层实现,monitorenter和monitorexit指令,一脸蒙逼,虽然早期把《深入理解Java虚拟机》...
  • 比如本文我们要讨论的JVM内存结构、Java内存模型和Java对象模型,这就是三个截然不同的概念,但是很多人容易弄混。可以这样说,很多高级开发甚至都搞不不清楚JVM内存结构、Java内存模型和Java对象模型这三者的概念及...
  • JVM内存结构 JVM内存分为线程私有区和线程共享区 线程私有区 1、程序计数器 ✓(记录当前线程执⾏到哪⼀条字节码指令位置) 当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免...
  • java对象在内存中的结构

    千次阅读 2018-08-30 15:35:16
    今天看到一个不错的PPT:Build Memory-efficient Java Applications,开篇便提出了一个问题,在Hotspot JVM中,32机器下,Integer对象的大小是int的几倍? 我们都知道在Java语言规范已经规定了int的大小是4个字节...
  • 对象是在堆中创建的 对象的内部结构图为
  • JVM-理解java对象的堆内存结构

    千次阅读 2018-06-26 09:33:40
    java对象在堆中的基本内存结构,分为三个部分:1.对象(header):包括Mark Word(标记字段)和Class Pointer(类型指针)2.实例数据(instance data):对象真正存储的有效信息,即代码中定义的各种类型的字段内容3.对齐...
  • JVM虚拟机种Java对象的内存结构如图所示分为三大块:对象(Header)、对象种的实际数据(Instance Data)、对齐填充(Padding)。 对象头(Header) Mark Word:用于存储对象自身的运行时数据,如哈希码...
  • JVM之内存结构详解

    万次阅读 多人点赞 2019-10-18 12:49:05
    对于开发人员来说,如果不了解JavaJVM,那真的是很难写得一手好代码,很难查得一手好bug。同时,JVM也是面试环节的中重灾区。今天开始,《JVM详解》系列开启,带大家深入了解JVM相关知识。 我们不能为了面试而面试...
  • java内存结构分析java内存结构java栈结构分析:栈帧局部变量表操作数栈动态连接返回地址运行时常量池对象的创建过程类加载的执行流程图对象创建的过程:对象内存分配方式指针碰撞空闲列表栈上分配:内存逃逸:对象...
  • java对象头 MarkWord

    万次阅读 多人点赞 2019-06-05 20:41:15
    原文链接:[https://blog.csdn.net/scdn_cp/article/details/86491792#comments] 我们都知道,Java对象存储在堆(Heap)内存。那么一个Java对象到底包含什么呢?概括起来分为对象、对象体和对齐字节...
  • 本文讲解了java对象在内存中的结构。 建议有c,汇编语言基础的同事学习,如果想深入理解java语言,看看这个。
  • JVM8(4)java虚拟机内部结构

    千次阅读 2017-05-17 14:35:29
    JVM规范描述的是一种抽象化的虚拟机的行为,而不是任何一种广泛使用的虚拟机实现。 要去“正确地”实现一台Java虚拟机,其实并不像大多数人所想的那样高深和困难——只需要正确读取class文件中每一条字节码指令,...
  • 浅谈JVM内存结构,Java内存模型和Java对象模型

    多人点赞 热门讨论 2021-10-01 22:45:43
    Java对象模型:4. 三者区别: 1. JVM内存结构: Java代码是要运行在Java虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,其中有些区域随着虚拟机...
  • 在前面两篇文章中了解到Java对象实例是如何在HotSpot虚拟机的Java堆中创建的,以及创建后的内存布局是怎样的。下面详细了解在Java堆中的Java对象是如何访问定位的:先来了解reference类型数据是什么,再来了解两种...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 223,867
精华内容 89,546
关键字:

java对象头结构64位jvm

java 订阅