Hotspot Java内存模型

目录

一、JRE/JDK/JVM是什么关系?

二、运行时数据区的组成

三、运行时常量池与String对象

四、直接内存和DirectByteBuffer对象

五、基本类型变量的内存分配与自动装箱

六、Java对象内存布局与指针压缩

七、Java对象访问定位

八、工作内存和主内存


一、JRE/JDK/JVM是什么关系?

JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。程序开发者不需要关心底层JRE的实现,不同的操作系统会有对应的不同的JRE,不同厂商提供的JRE也会有很大差异,对此Sun公司只提供了开发规范而未做实现细节要求。

JDK(Java Development Kit)是程序开发者用来开发、调试java程序用的java工具类库,提供了日常开发中通用的基础设施的实现。在JDK的安装过程中会自带JRE,所以在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。

JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

二、运行时数据区的组成

根据 JVM 规范,JVM在执行JAVA程序的过程中会把它所管理的内存分为虚拟机栈、本地方法栈、程序计数器(PC寄存器)、堆、方法区五个部分,这五个部分称为JAVA运行时的数据区,如下图所示:

Hotspot Java内存模型_第1张图片

各区域跟线程的关系如下:

Hotspot Java内存模型_第2张图片

1.程序计数器

      又称作为PC寄存器,在汇编语言中,是指CPU中的寄存器,它保存的是程序当前执行的指令的地址,当CPU需要执行指令时,根据程序计数器中保存的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。JVM中的程序计数器并不是物理概念上的CPU寄存器,但是功能相同,即保存当前需要执行的指令的地址,其存储的数据所占空间的大小不会随程序的执行而发生改变,注意如果线程执行的是native方法,则程序计数器中的值是undefined。

    因为一个CPU的内核同一时刻只能执行一条线程中的指令,所以多线程并发执行时CPU是通过线程调度在多个线程间来回切换执行。为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。所以,程序计数器是每个线程所私有的。

2.Java栈(虚拟机栈)

     Java栈是Java方法执行的内存模型,随线程创建和销毁,线程的栈内存大小通过参数-Xss 指定,jdk5以后默认为1M。因为每个线程都对应一个独立的虚拟机栈,所以栈大小直接影响所能创建的线程数量,应该根据应用的实际运行情况调整。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接、方法返回地址(Return Address)等。详情参考:

     https://blog.csdn.net/ychenfeng/article/details/77247807

    重点关注StackOverflowError堆栈溢出错误产生的过程,以下面的代码为例说明:

public class TestDemo {

    private int index = 1;

    public void method() {
        index++;
        //如果不注释掉,则不会抛出异常
        /*
        if(index>10000){
           return;
        }
        */
        method();
    }

    @Test
    public void testStackOverflowError() {
        try {
            method();
        } catch (StackOverflowError e) {
            System.out.println("程序所需要的栈大小 > 允许最大的栈大小,执行深度: " + index);
            e.printStackTrace();
        }
    }
}

  抛出异常过程详解:

执行testStackOverflowError方法会创建一个线程,同时创建一个与该线程关联的虚拟机栈(栈内存)

第一次调用method()时,会创建一个栈帧并压栈,执行index++会在该栈帧的操作数栈中写入数据,接着递归调用methed()方法,又会创建一个栈帧并压栈,因为method()方法没有返回逻辑会一直不断的递归调用method()方法,就不断的创建新的栈帧并压栈,从而导致实际栈内存不断扩大。当栈内存超过系统配置的栈内存,就会出现java.lang.StackOverflowError异常。

如果不注掉if代码,当条件被触发时,该次method()方法调用完成,该次调用对应的栈帧出栈,如果有返回值则写入下一个栈帧的操作数帧,然后调用结果往上不断返回,对应的栈帧不断出栈,直到方法执行完成。

3.本地方法栈
  本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。

4、方法区

      方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将永久代移除了。
5.Java 堆

     在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,因此Java 堆是垃圾收集器管理的主要区域,又叫做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。Java对象的内存布局参考:https://www.jianshu.com/p/91e398d5d17c

     Java8中堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。从JDK8开始永久代(PermGen)被元空间(Metaspace)代替用于实现方法区,两者最大的区别是元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制。所有新创建的对象都将在新生代Eden区域中分配内存,如果年轻代的数据在一次或多次GC后存活下来,那么将被转移到老年代,这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

      默认情况下,老年代占三分之二的堆空间,年轻代占三分之一的堆空间, eden区占8/10 的年轻代空间,survivor from区占1/10 的年轻代空间,survivor to占1/10 的年轻代空间。通过命令java -XX:+PrintFlagsFinal -version查看所有默认的jvm参数。

Hotspot Java内存模型_第3张图片
 

     堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大。Sun Hotspot为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的。但如果对象过大的话则仍然是直接使用堆空间分配,因此通常多个小的对象比大的对象分配起来更加高效。

三、运行时常量池与String对象

     运行时常量池是方法区的一部分,class文件中用于存放编译期生成的各种字面量和符号引用的常量池会在类加载后放到运行时常量池中保存。字面量包含文本字符串和final常量值,如String s="test"中的test,final int num=124中的124;符号引用包含类和接口的全限定名,字段的名称和描述符和方法的名称和描述符。应用程序也可在程序运行期间将新的常量放入常量池中,如调用String对象的intern()方法,如果常量池没有跟该String equal()的String对象,则会将该String放入运行时常量池中。参考如下用例:

@Test
    public void test() throws Exception {
        //s和s2在编译期确认,都指向常量池中同一个String对象
        String s = "shl";
        String s2 = "shl";
        //s3在运行时确认,即执行到此代码时重新生成一个新的对象
        String s3 = new String("shl");//等价于String s3=new String(s1);
        System.out.println(s == s2); //true
        System.out.println(s2 == s3); //false
        //==比较两个引用类型变量时判断其是否指向同一个对象,equals()是对象自定义的比较对象是否相同的方法,String改写了Object中默认的equals()方法
        //字符串相同两个String对象equals()返回true
        System.out.println(s2.equals(s3));//true
        //intern方法比较特殊,如果常量池包含equal()相等的String对象则返回该对象,否则把当前对象加入到常量池中并返回该对象
        System.out.println(s == s2.intern()); //true
        System.out.println(s == s3.intern()); //true
        System.out.println(s3 == s3.intern()); //false
        String s5 = new String(RandomStringUtils.randomNumeric(5));
        System.out.println(s5 == s5.intern());//true
    }

    @Test
    public void test2() throws Exception {
        String s0 = "HelloWorld";
        String s1 = "World";
        //编译期完成两个字符串的连接后等价String s2 = "HelloWorld";
        String s2 = "Hello" + "World";
        //s1不是final变量所以实际的连接操作在运行时进行
        String s3 = "Hello" + s1;
        System.out.println((s0 == s2));  //true
        System.out.println((s0 == s3));  //false

        final String s4 = "World";
        String s5 = "Hello" + s4;
        System.out.println((s0 == s5)); //true
    }


    @Test
    public void test4() throws Exception {
        String s0 = "HelloWorld";
        //s1是final变量但是取值依赖于getWorld()的返回值,所以在运行时初始化
        final String s1 = getWorld();
        String s2 = "Hello" + s1;
        System.out.println((s0== s2)); //false
    }

    private static String getWorld() {
        return "World";
    }

 注意String类不是java的基本类型,但是跟基本类型的包装类一样都是不可变类,对其任何的改变都是生成一个新的String对象,参考如下用例:

@Test
    public void test5() throws Exception {
        String s="";
        long startTime=System.currentTimeMillis();
        for(int i=0;i<10000;i++){
            //String不是基本类型,但是跟基本类型的包装类一样都是不可变类,每次连接操作都生成一个新的字符串对象
            //让s变量指向新对象,所以整个循环耗时长
            s+="1";
        }
        System.out.println(System.currentTimeMillis()-startTime);
    }

    @Test
    public void test6() throws Exception {
        StringBuilder s=new StringBuilder();
        long startTime=System.currentTimeMillis();
        for(int i=0;i<10000;i++){
            //不用生成新的对象,所以非常快
            s.append("1");
        }
        System.out.println(System.currentTimeMillis()-startTime);
    }

    参考:java中特殊的String类型

              深入浅出java常量池

四、直接内存和DirectByteBuffer对象

      直接内存又叫堆外内存,是Java进程向OS申请的位于用户空间的一段内存,与堆内内存相对应,不受JVM Young GC影响,但Full GC会回收未使用的直接内存。直接内存的最大值通过参数-XX:MaxDirectMemorySize或-Dsun.nio.MaxDirectMemorySize来指定,默认值取决于GC的方式。通常在以下场景下使用直接内存:

  • 保存生命周期中等或较长的对象,从而减少老年代的大小,避免老年代过大导致Full GC停顿时间较长,可借助Apache OHC组件实现,因为读写对象时需要将其从堆外内存拷贝至堆内内存,然后做反序列化或者序列化成Java可直接使用的对象,会导致CPU上升,轻微的性能损耗
  • 文件读写,网络通信数据读写等I/O操作,正常的I/O操作会将待操作的数据从堆内内存拷贝至堆外内存,然后以堆外内存的虚拟地址执行系统调用,这样做了为了避免系统调用期间Young GC在内存整理过程中导致堆内内存地址改变系统调用失败。如果I/O操作直接将数据写入至堆外内存或者从磁盘直接读取到堆外内存,然后用这个地址执行系统调用则省去中间拷贝至堆内内存的操作,显著提高I/O效率

     Java NIO中可通过DirectByteBuffer完成堆外内存的创建、使用和销毁,完全基于sun.misc.Unsafe即JNI实现,如下:

Hotspot Java内存模型_第4张图片

     Hotspot Java内存模型_第5张图片

对应的堆内内存的实现类是HeapByteBuffer,完全基于数组实现,如下:

Hotspot Java内存模型_第6张图片

Hotspot Java内存模型_第7张图片

      DirectByteBuffer内存的分配通过Unsafe的allocateMemory(int)方法实现,底层是库函数malloc()方法,申请前会检查当前可用的直接内存是否符合要求,如果不符合要求则通过JavaLangRefAccess.tryHandlePendingReference()方法将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放,仍不满足需求则通过System.gc()会触发一个full gc,将老年代里的DirectByteBuffer对象及其堆外内存进行回收,因为调用System.gc()并不能够保证full gc马上就能被执行,所以会间隔一段时间重试9次,仍不满足则抛出OutOfMemoryError("Direct buffer memory”)异常。

      DirectByteBuffer的内存回收通过Cleaner完成,当创建一个DirectByteBuffer时会通过Cleaner.create(this, new Deallocator(base, size, cap)) 创建一个Cleaner,用于对当前构造的DirectByteBuffer对象的垃圾回收过程进行跟踪,当DirectByteBuffer被回收了,最终调用Deallocator释放堆外内存。

     DirectByteBuffer中放入对象的测试用例如下:

public class User implements Serializable{
    private String name;

    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

@Test
    public  void test() throws Exception {
        User user=new User();
        user.setAge(12);
        user.setName("shl");
        ByteArrayOutputStream byteArrayOut=new ByteArrayOutputStream();
        ObjectOutputStream out=new ObjectOutputStream(byteArrayOut);
        out.writeObject(user);
        byte[] srcData=byteArrayOut.toByteArray();
        System.out.println(srcData.length);
        ByteBuffer buffer=ByteBuffer.allocateDirect(srcData.length);
        buffer.put(srcData);
        buffer.flip();
        byte[] distData=new byte[srcData.length];
        buffer.get(distData);
        ByteArrayInputStream byteArrayInput=new ByteArrayInputStream(distData);
        ObjectInputStream in=new ObjectInputStream(byteArrayInput);
        User user2=(User)in.readObject();
        System.out.println(user2);
        System.out.println(user==user2);
    }

     参考:堆外内存 之 DirectByteBuffer 详解

                JDK源码阅读-DirectByteBuffer

                JDK源码阅读-FileOutputStream

                JDK源码阅读-FileInputStream

               JVM源码分析之堆外内存完全解读

五、基本类型变量的内存分配与自动装箱

     Java的数据类型分为两大类,基本类型(primitive type)和引用类型(reference type),基本类型包含整数类型(byte、short、int、long),浮点类型(float、double),布尔类型(boolean),字符类型(char),returnAddress类型(java中没有对应的数据类型,表示指向某个操作码的指针),引用类型包含类类型,接口类型和数组类型,引用类型表示对某个对象的引用,即C中的指针,null是引用类型的特殊取值,表示不指向任何对象。注意基本类型变量在内存的字节数是固定的,不随运行的环境而改变,但是引用类型因为基于C语言的指针,会随着运行环境改变而改变,32位虚拟机占4个字节,64位虚拟机占用8个字节,64位时开启指针压缩则占4个字节。

    基本类型和引用类型变量都是直接分配内存,用"="操作符会直接改变内存中的值,因此不存在基本类型对应的常量池。注意String类的变量是一个引用类型而非基本类型,参考如下用例:

    @Test
    public void test8() throws Exception {
        //num和num2是基本类型变量,两个变量独立分配内存,内存中的取值无任何关联
        int num=123;
        int num2=123;

        //s和s2是引用类型,两个变量独立分配内存,但是内存中的指针指向字符串常量池中同一个对象
        String s="ab";
        String s2="ab";

        //==比较基本类型时,比较两者取值是否相同
        System.out.println(num==num2);//true
        //==比较引用类型时,比较两者是否指向同一个对象
        System.out.println(s==s2);//true
    }

      每个基本类型都有对应的包装类,包装类和String类一样,都是不可变的final类,不能被继承。JDK5引入了基本类型到对应包装类的自动装包和拆包机制,装包通过Integer.valueOf(int i)方法实现,拆包通过Integer.intValue()方法实现,可通过debug断点调试观察,重点关注装包,源码如下:

   
  public static Integer valueOf(int i) {
     //当i在low和high之间时返回IntergerCache中的对象,否则构造一个新的
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }


private static class IntegerCache {
        static final int low = -128;
        static final int high;
        //缓存的Integer对象数组
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            //读取JVM启动参数
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    //如果不为空,则取该值与127的最大值
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    //确保i不超过最大值
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
            //初始化Integer数组
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

        Short,Long,Byte,Character,Boolean都实现了对应的对象缓存,Boolean包含两个静态常量对象TRUE和FALSE,Character的范围是0-127,其它三个都是-128到127之间。Fload,Double未实现对象缓存,参考如下用例:

    private final Integer aF=128;
    private final Integer bF=128;

    private  String s="128";
    private  String s2="128";

    @Test
    public void test9() throws Exception {
        //不存在类似于String的常量池
        System.out.println(aF==bF);
        System.out.println(s==s2);
    }

    @Test
    public void name() throws Exception {
        //自动装箱,在对象缓存范围内,a和b指向同一个对象
        Integer a=127;
        Integer b=127;
        int c=127;
        Integer d=new Integer(127);
        System.out.println(a==b);//true
        //c自动拆箱,a和c比较取值是否相等
        System.out.println(a==c);//true
        //没有走自动装箱,重新生成一个新对象
        System.out.println(a==d);//false
        //d自动拆箱
        System.out.println(c==d);//true
        //超过自动装箱范围,都生成新的变量
        Integer a2=128;
        Integer b2=128;
        System.out.println(a2==b2);//false
        int a3=128;
        int b3=128;
        System.out.println(a3==b3);//true
        //a2自动拆箱,比较取值
        System.out.println(a2==a3);//true
    }

        参考:Java自动装箱与拆箱及其陷阱

                   java数据类型

六、Java对象内存布局与指针压缩

      一个Java对象在内存中包括对象头、实例数据和补齐填充3个部分,如下图:

Hotspot Java内存模型_第8张图片

    对象头包含以下信息:

   1、Mark Word

       这部分主要用来存储对象自身的跟业务无关的运行时数据,在32位系统占4字节,在64位系统中占8字节。为了让Mark Word中存储更多的信息,JVM将字的最低两个位设置为标记位,标识当前对象的状态,不同标记位下不同位区间含义不同,32位如下图,其中epoch是偏向时间戳,轻量级锁表示访问当前对象需要获取对应的轻量级锁。

Hotspot Java内存模型_第9张图片

    2、Class Pointer

     指向对象对应的Class类型数据的对象指针,即OOP(ordinary object pointer),所有的引用类型变量都是OOP,在32位系统占4字节,在64位系统中占8字节。使用8字节的OOP时内存占用比4字节的大约多50%,为了减少内存占用,JVM从jdk1.6 update14开始支持指针压缩,在64位下默认开启。指针压缩的实现是基于Java对象最低按8字节对齐的,即所有的java对象的大小必须是8字节的整数倍,因此所有对象的内存地址的最低3位都是3个0,指针压缩时可以把真实的内存地址右移3位,取低32位得到实际地址,指针解码时可以把实际地址左移3位得到真实地址,即开启指针压缩下JVM可利用的最大内存空间是32GB(2^35),当JVM最大可用内存超过32GB时,指针压缩自动失效,强制使用8字节对象指针。

     因为指针压缩和解码都是访问堆对象时执行的,所以堆内对象的引用类型变量都会被压缩,如对象的静态变量、全局变量/对象数组的数组元素,而指向永久代(JDK8中称为元空间)的对象指针不会被压缩,如这里的Class Pointer,位于虚拟机栈中的引用类型变量不会被压缩,如调用参数,返回值,局部变量,NULL指针等。

    启用指针压缩:-XX:+UseCompressedOops,禁止指针压缩:-XX:-UseCompressedOops。

    3、Length

     如果是数组对象,还有一个保存数组长度的空间,占4个字节。

   实例数据是指java对象中所有实例属性的集合,是占用堆内存的主要部分。为了尽可能按照8字节对齐,减少内存占用,内存中字段的顺序不一定是代码中字段声明的顺序,会按照如下原则做适当调整:

  • 除了对象整体需要按8字节对齐外,每个成员变量都尽量使本身的大小在内存中尽量对齐。比如 int 按 4 位对齐,long 按 8 位对齐。
  • 类属性按照如下优先级进行排列:长整型和双精度类型;整型和浮点型;字符和短整型;字节类型和布尔类型,最后是引用类型。这些属性都按照各自的单位对齐。
  • 优先按照规则一和二处理父类中的成员,接着才是子类的成员。
  • 继承体系里不同类间需要8字节对齐。
  • 当父类中最后一个成员和子类第一个成员的间隔如果不够4个字节的话,就必须扩展到4个字节的基本单位。
  • 如果子类第一个成员是一个双精度或者长整型,并且父类并没有用完8个字节,JVM会破坏规则2,按照整形(int),短整型(short),字节型(byte),引用类型(reference)的顺序,向未填满的空间填充。

     对齐填充是指java对象的整体大小必须是某个数值的整数倍,默认是8字节,通过选项-XX:ObjectAlignmentInBytes=num设置,最小值为8,最大值为256。这样做是为了方便内存分配与管理,避免内存碎片,因为Linux中一个内存页默认是4kb,按8字节对齐可保证一个内存页可以分配给整数个对象。              

      java对象的内存布局和整体占用内存大小可通过OpenJDK的工具包JOL查看,依赖如下:

     Hotspot Java内存模型_第10张图片

     测试用例参考如下:

public class Person {

    private int intTest;

    private boolean boolTest;

    private char charTest;

    private static long longTest;

}

public class User extends Person{
    private double doubTest;

    private short shortTest;

    private Person test;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(User.class).toPrintable());
    }
}

输出结果:
        synchronizedTest.User object internals:
        OFFSET  SIZE                      TYPE DESCRIPTION                               VALUE
        0    12                           (object header)                           N/A
        12     4                       int Person.intTest                            N/A
        16     2                      char Person.charTest                           N/A
        18     1                   boolean Person.boolTest                           N/A
        19     1                           (alignment/padding gap) //优先处理父类属性,父类整体按8字节对齐
        20     2                     short User.shortTest                            N/A
        22     2                           (alignment/padding gap) //父类最后一个属性与子类第一个属性间隔4字节
        24     8                    double User.doubTest                             N/A
        32     4   synchronizedTest.Person User.test                                 N/A
        36     4                           (loss due to the next object alignment)//子类整体按8字节对齐
        Instance size: 40 bytes
        Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

       参考: 如何正确计算Java对象所占内存?

                  Java对象头详解

                  如何计算Java对象所占内存的大小

                  HotSpot JVM中的对象指针压缩

七、Java对象访问定位

    通过new关键字创建一个新对象如Object obj = new Object()时,obj并不是对象本身,而是对该对象的引用,即java基础数据类型之一的reference类型。JVM规范并未规定如何根据reference类型访问对应的对象,主流的实现方式有两种:

1、句柄

如果使用句柄访问的话,那么java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象的实例数据与类型数据各自的具体地址信息,如下图:

Hotspot Java内存模型_第11张图片

2、指针

   Java对象实例数据中保存对对象类型数据的指针,reference中存储的直接就是对象地址。 

Hotspot Java内存模型_第12张图片

       这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存放的是稳定的句柄地址,在对象的移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前Java默认使用的HotSpot虚拟机采用的便是是第二种方式进行对象访问的。

八、工作内存和主内存

     主内存是指所有线程共享的内存,包括Java堆和方法区,工作内存指的是每个线程执行任务时独享的内存,包括Java栈,本地方法栈,程序计数器以及CPU执行该线程指令时用到的高速缓存。当线程执行任务时会将用到的各种变量和即将调用的方法的动态链接从主内存复制一份到工作内存中,注意如果用到的是某个对象,则复制的是该对象的引用,具体执行计算时通过该引用将该对象某个属性或者方法的动态链接复制到工作内存中,计算完毕再由工作内存写入到主内存中。java内存模型定义了8种操作来完成主内存与工作内存之间的交互,这8种操作每一种都是原子操作,具体如下:

     lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
     read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
     load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
     use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
     assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
     store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
     write(写入):作用于主内存,它把store传送值放到主内存中的变量中

     unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

1、不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
4、一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7、如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

Hotspot Java内存模型_第13张图片

 

 

你可能感兴趣的:(Hotspot和Linux内核)