继上一篇 JVM学习之路1-内存模型 介绍完JVM内存模型之后,这篇准备聊聊对象的内存布局以及逃逸分析。我们知道对象一般是分配在堆上的,但是你知道对象在堆上是怎么存放的吗?我们平时程序中在使用的时候是怎么找到对象的?
知识点
1、内存对象布局
2、逃逸分析
对象内存布局
先说一下我们平时是怎么创建对象的A a = new A();
如上所示,一个对象A就被创建出来了。看似简单的一行语句,其实虚拟机为我们做了很多事情。
首先虚拟机去常量池中查找是否有类A的符号引用,并检查该符号引用的类A是否已经被虚拟机所加载过,如果没有则先进行加载(具体的类加载机制我们会在后面的系列文章中介绍),如果已经被加载过则可以确定为该类对象所分配的内存大小并进行内存分配,大概流程如下:
这里涉及到两种分配方式:指针碰撞和空闲列表
指针碰撞
简单来说,基于内存规整的前提下将内存分为两部分,用过的内存放到左侧,没用过的内存放到右侧,中间放一个指针来分界,当为对象分配内存时,向空闲区移动一块该对象大小的空间:
空闲列表
上面是基于内存规整的情况下进行指针碰撞,如果内存不规整的话,怎么处理?那就要说到空闲列表了,简单来说就是已分配内存和未分配内存是交错的,虚拟机维护一份列表来知道哪些内存是可用的,在进行对象分配的时候会从列表中取一块足够空间的内存,分配结束后更新列表:
上面两种分配方式哪种更好呢?这个没有一定哪个好或者哪个不好,取决于java的内存是否规整,而java内存是否规整又取决于所用的垃圾收集器是否带有压缩整理法决定,在后面介绍垃圾收集器的时候再具体聊。
TLAB
分配对象的对象的时候不是线程安全的,可能在分配对象A的时候,指针都没来得及修改对象B又拿到原来的指针进行分配,如何避免呢?虚拟机在这方面有两种解决方案:
1、分配对象更新指针的时候使用CAS来保证操作的原子性
2、使用TLAB机制,也就是对于每个线程都先分配一个缓存进行内存的预分配,每次线程需要分配申请内存的时候都先在该缓存中进行,只有缓存不够的时候再加锁分配新的TLAB(-XX:+/-UseTLAB)。
对象结构
这里我画了个图,看这个图基本上大家就比较清晰了,我还是大概解释一下。
对象头
存放两部分数据:
1、运行时数据,比如哈希码、gc分代年龄、锁状态等。
2、类型指针,用来查找该对应指向的类元数据在哪,通过该类元数据就知道对象属于哪个类的对象。
实例数据
就是存放对象的字段内容,举个栗子:
存的就是age和name信息。
填充
没啥意义,起到占位符的作用,因为虚拟机分配对象大小是8字节的倍数,为了能够补齐。
对象访问
对象内存分配和对象结构都讲完了,那我们在方法中是怎么访问到对象的呢?上一篇文章中(jvm学习之路1-内存模型),我们在介绍jvm内存模型的时候说到了虚拟机栈,在方法调用的时候会产生一个栈帧,栈帧中存放了局部标量信息,就是这个局部变量表里对于对象会有一个reference,该内容存的就是对象的引用,我们就是通过它来找到对象数据的。而引用又分为两种:
1、对象句柄池,优点是稳,对象地址变了,不影响栈中引用;
2、对象指针,优点是快,数据直接找到,少一步中间商(hotspot方式);
这两个概念比较简单,我们参照书里说的,直接给两幅图:
逃逸分析
对象内存布局基本讲完了,回顾一下,我们上面讲的对象都是在堆上分配的,那是不是所有对象都是在堆上分配的呢?答案当然是否定的,这里就是我们这部分想要说的逃逸分析技术,他其实就是jvm的一种优化。
为什么需要逃逸分析
正常对象在堆上分配,由gc销毁,但是这样是比较耗性能的,大家都知道对于一个程序来说,gc次数越少会好,为了尽量减少gc消耗,逃逸分析技术就诞生了。
什么是逃逸分析
网上有位博友这么形容逃逸,用了一段简单直接的代码,我觉得挺直截了当的,可以供参考:
stringBuilder是在方法的内部变量,而此时它被直接返回,这样stringBuilder就有可能被其他地方的方法或参数所改变,这样它的作用域就不只是demo1了,虽然它是一个局部变量,但其发生了“逃逸”。
那么,我可以改一下代码:
如此,就没有返回StringBuilder,而是toString(),那么StringBuilder没有从方法中直接脱离,就没有发生逃逸。所以我们写代码的时候也要注意利用好这点优化。
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
优化了哪些
- 栈上分配
一般情况下,不会逃逸的对象所占空间比较大,如果能使用栈上的空间,那么大量的对象将随方法的结束而销毁,减轻了GC压力
- 同步消除
如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
- 标量替换
Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
如何开启
jdk1.8以上是默认开启的。
开启:-XX:+DoEscapeAnalysis
关闭:-XX:-DoEscapeAnalysis
总结
这篇文章主要介绍了两部分内容:
1、jvm中对象的内存布局,包括内存分配、对象结构组成、如何访问对象。
2、jvm的优化技术-逃逸分析,什么是逃逸分析、逃逸分析的好处、如何使用逃逸分析。
参考资料:
周志明《深入理解java虚拟机》
https://www.cnblogs.com/fuguoliang/p/9753061.html
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。