关于《深入理解Java虚拟机》一书
该书由周志明大神所著,基于JDK7版本,面向Java体系中的中高级开发人员,本书一共分为五个部分,详细的介绍了JVM内存管理机制,类加载机制,虚拟机执行子系统,并发编程下的高效执行以及调优。 接下来的几个月我会分出时间仔细研读此书,并概括其中的有效内容
我为什么要阅读这本书&&我的阅读方式
JVM调优一直都是互联网大企业必问知识点,虽然可能是抱着进一线企业的目的去阅读这本书是有一定的功利心,但这本书确实有巩固基础的作用。 JVM调优一般用在什么样的场景?答:多线程、高并发。 目前我的能力停留在业务的CRUD方面,市面上看到的任何网站我都可以做出来,这个做仅仅为表面上的展现而已,如果考虑到访问量或者说并发量稍微上去一点,我的网站恐怕就得挂。对于现在的我来讲,服务器挂了只能自己瞎百度,这样做是没有任何意义的,如果以后项目进行横向扩容那我又懵逼了,并发量上去了项目死机了我只能请教别人了。这样的技术水平永远不可能进军一线。只会写网站算不得什么了不起的本领,能够开发出兼容性、扩展性强的架构才能算正真的企业项目,以前总是认为理论没用,专业课也不去上,天天在寝室学写业务、学习XX框架,现在回想起来真的蠢。
基础才是干好一切的基石,算法和底层实现原理才是我们学生时代应该掌握的技能,或许以后我成为了架构师会回过头来看这段日子会感觉非常的幸运。继续加油吧,也祝福所有程序员会越过越好。
由于这本书概念性的东西较多,相当多的概念只言片语可能没法讲清楚(作者也有水平,没法照顾到新手),所以必须得结合视频或者到社区、QQ群里提问才能理解,这里我推荐B站配套视频点此进入bilibili。
引言
Java技术体系非常丰富,比如JVM、各种风格的Java API、Java第三方框架(如springboot、mybatis等等),相比之下,JVM的资源少之又少。
造成这种情况的原因是因为Java本身在虚拟机层面隐藏了底层技术的复杂性与操作系统的差异性(实现了跨平台),Java建立了统一的平台实现了编译的程序能够在任何一台虚拟机上正常运行,使得程序员只需要专注实现业务即可
但技术的不断发展,技术领域越来越广泛,程序可能10个人同时使用完全正常,但在10000人使用就会缓慢、死锁、宕机,毫无疑问,要满足1000人同时使用需要更高性能的物理硬件,但在绝大多数情况下,提升硬件效能无法等比例提升程序运作性能和并发能力,所以就需要程序员更加详细了解底层的技术特征,才能写出最适合虚拟机运行和自动化的代码。
全书内容分五大部分:
一、走进JAVA:这一章节主要是让读者自己来编译一个OpenJDK7,介绍发展史等等。
二、自动内存管理机制:讲述JMM(java内存模型)如何划分、什么样的代码操作可能导致内存溢出异常,GC回收的算法以及JDK1.7中提供的几款GC的特点以及原理,介绍了几款可视化排查故障的工具以及一个实际案例。
三、虚拟机执行子系统:详细演示JAVA类加载机制阶段和原理,分析虚拟机在执行代码时如何找到正确的方法,如何执行方法内的字节码以及执行代码时涉及到的代码结构。
四、程序编译与代码优化:分析了Java中泛型、主动装箱和拆箱、条件编译等多种语法糖的前因后果,实战演示了如何使用插入式注解处理器来实现一个检查程序命名规范的编译器插件。
五、高效并发:由于Java提供了多线程支持,该部分主要讲解Java内存模型的结构及操作,以及原子性、可见性和有序性在Java内存模型找那个的体现,介绍了并发先行原则的规则以及使用,介绍了虚拟机实现高效并发所采取的一系列锁优化措施。
第一部分、走进Java
第【1】章 走进Java
【1.1】 概述
概念性的东西,大致表述了Java是怎么牛逼的。。。
【1.2】Java技术体系
是时候祭出这张图了:
看图说话把,我们可以把Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK(Java Development Kit),可以把Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE(Java Runtime Environment),图中可以看出JDK包含JRE,JRE包含JVM。只需要运行JAVA呢,JRE就够了,要编译啥的就得需要JDK,当然两者都必然存在JVM。
从大方向来分,JAVA技术体系可以分为4个平台,分别为:
Java Card:支持小程序在小内存设备运行的平台(智能卡)。
Java ME:支持在移动终端上的平台。
Java SE:面向桌面级应用。
Java EE:支持使用多层架构的企业级应用(ERP、CRM)。
大致过一遍即可。
【1.3】Java发展史
1991年4月Java产品前身Oak出世,1995年5月23日正式发布Java。Java语言提出“Wirite Once,Run Anywhere”,即一次编写,到处运行。Java语言的特性有很多,我觉得比较关键的是Java的分布性。Java的设计之初就是为了在更加方便的在网络上使用,所以它也是分布式语言,如果以后Java程序员不懂分布式的可是要向全国人民谢罪的呦(手动狗头) , 其他知识点大概是历史性的东西,不过值得一提的是JDK1.5引入了Java动态语法,如:泛型、自动装箱、动态注解、枚举、可变长参数、遍历循环、提供了并发包(java.util.concurrent),Sun公司在JDK1.7提出了Lambda项目,但又马上被Oracle收购了。。。。。。由于这本书只进行到了JDK1.7版本,并没有提到之后的事情,这一章节我就告一段落了。。
【1.4】Java虚拟机发展史
历史性的知识点。。
【1.5】展望Java技术的未来
4点:模块化、混合语言、多核并行、进一步丰富语法
【1.6~1.7】编译OpenJDK7
留个伏笔,等全书完结了再回来思考这2章内容
第二部分、自动内存管理机制
第【2】章 Java内存区域与内存溢出异常
【2.1】 概述
开场白。
【2.2】运行时数据区域
Java虚拟机在运行的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域各有各的用途,以及创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖线程的启动和结束而立即建立和销毁,如图:
【2.2.1】 程序计数器
程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令(对于多核CPU来说是一个内核)。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
以上为书中原文,认为写的很好所以照搬了。 但这个玩意如何理解呢?程序计数区就是处于线程独占区的一块内存而已。现在指定一段代码:
public main() {
int a = 10;
int b = 20;
System.out.print("a + b = " + (a + b));
}
代码肯定是至上往下执行的,上面这段代码非常好理解 ,但是如果有了if怎么办?:
public main () {
int a = 10; //标识为1
int b = 20; //标识为2
if (a < b) { //标识为3
System.out.print(a); //标识为4
} else { //标识为5
System.out.print(b); //标识为6
}
}
那这里就用到了计数器了,方法进栈之后,每一行代码在汇编层面都会有个标识,我们这里假设至上往下标识是1,2,3,4....(实际肯定不会这么简单),程序肯定是1->2->3->4..一直往下走的,如何遇到了if,那么程序计数器就会执行判断,并且跳转至满足条件的代码区域。简单的来说,程序计数器就可以理解为"行号跳转器"。我们知道每一个线程都是一个顺序执行单元,那如果我们开一个线程把代码放至线程中执行,那么每一个线程都将会有自己的线程计数器。值得一提的是,如果线程计数器执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个计数器的值为undefined,此区域也是唯一不会出现OutOfMemoryError情况的区域,因为咱不需要操作程序计数器,Java自己会维护。
再引申一个小知识点,Java中有一个"goto"关键字,但是是不允许使用的,这个关键字在java中作为保留关键字存在,C语言中存在goto并且允许使用,这个关键字的作用就是汇编层面上的行号跳转,但是任何C语言的入门书都提到了避免使用,对于水平较低的开发人员来讲,频繁使用goto使得程序结构不清晰,除了你没人能维护你写的玩意,甚至有时候写出来你自己都看不懂。goto可以正常使用,就我个人感觉,完全禁止不合适,禁止向前跳比较好一点(不熟悉汇编瞎使用可能会出现莫名bug),如果完全禁止goto,在一个过程需要申请多个系统资源不用goto处理会相当难看 。
【2.2.2】 *Java虚拟机栈
虚拟机栈描述的是Java方法执行的动态内存模型,每个方法在执行的同时会创建一个栈帧,栈帧里面存放局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
经常有人把Java内存区分为堆内存和栈内存,这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的区域是这两块。。这里面所值的栈就是虚拟机栈,或者说是虚拟机栈中局部变量表的部分。
局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、int等)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型会占2个局部变量空间(Slot),其余的数据类型只占一个,局部变量表的内存空间在编译器完成,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常情况:①如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;②如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
说到这里,应该有了个印象,平时上leetcode刷题写递归容易StackOverflowError应该也有了新的理解。虚拟机栈就是将方法进栈,并创建一个方法对应的栈帧,如果非递归调用,则方法的栈一开始就是确定的,然而如果进行递归调用,则方法进栈之后还未出栈又将继续进栈该方法,爆栈之后会直接抛出栈溢出,每次递归的调用相当于进栈一个方法,系统将多分配一个栈帧,栈的深度会越来越大,可以看得到空间也会直线变化。所以设计递归算法一定要合理一点(不过有些题不用递归真的很难相出思路。)。
【2.2.3】 本地方法栈
本地方法栈与虚拟机栈所发挥的作用是类似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
在虚拟机规范中本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(如Sun HotSpot)直接将本地方法栈和虚拟机栈合二为一。
【2.2.4】 *Java堆
对于大多数应用来说,Java堆(heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中描述的是:所有的对象实例以及数组都要在堆上分配(原文:the heap is the runtime data area from which memory for all class instances and arrays is allocated),但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也逐渐变得不是那么"绝对"了。
Java堆是垃圾回收器主要管理的区域,因此很多时候也被称为"GC堆"(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB),不过不管怎样划分都是为了更好的回收内存,或者更快的分配内存。
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。实现时既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果堆中没有内存完成实例分配,并且堆也无法进行扩展时,将会抛出OutOfMemoryError。
【2.2.5】 *方法区
方法区与Java堆一样,是各个线程共享区域,它用于存储已被虚拟机加载的类信息、常量、静态变量(包含所有的class和static变量),在整个程序中都是唯一的元素。虽然,Java虚拟机规范把方法区描述成堆的一个逻辑部分,但是他有个别名叫做Non-Heap(非堆),目的就是为了和堆区分开来。我看过有许多的博主描述JVM内存结构喜欢分为三类:堆、栈、方法区,这样分是正确的,按照我的理解,方法区就是一个jvm的"资料库",用户存储全局唯一的资料以供其他栈区的顺序执行使用。
在HotSpot虚拟机上开发的程序员更愿意将方法区叫做"永久代",我们知道JVM的GC收集算法是"分代收集算法",所以垃圾收集行为在永久代这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样,"永久"存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收比较难,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。在Sun公司的Bug列表中,曾出现过若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收导致内存泄漏。
根据Java虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
为了方便阅览者理解,我找到了一个例子,代码如下:
//类名:AppMain.java
public class AppMain { //运行时,jvm把AppMain的信息放入方法区(资料库)
public static void main (String[] args) { //static的main方法也放入方法区
Sample test1 = new Sample("测试1"); //Sample对象放入堆区,test1是局部变量引用,放入栈区
Sample test2 = new Sample("测试2");
test1.printName();
test2.printName();
}
}
//类名:Sample.java
public class Sample { //运行时,jvm将Sample放入方法区
private name; //new Sample实例后,name引用放入栈区,name对象放入堆里
//构造方法
public Sample (String name) {
this.name = name;
}
public void printName () { //print方法本身放入方法区里
System.out.println(name);
}
}
上述程序跑起来了,系统收到了我们发出的指令,启动了一个JVM。进程首先从classpath找到AppMain.class文件,读取这个文件中的二进制数据,然后把AppMain类的类信息存放到运行时数据区的方法区中。(这一过程成为AppMain类加载过程)。接下来,JVM找到main方法的字节码,执行之。依次运行,第一条语句就是: Sample test1 = new Sample("测试1"); 说一下这个语句的执行过程:①创建一个Sample实例,JVM此时直奔方法区找Sample的资料,发现没得进行第二步②类加载Sample,理所应当的,Sample类进入了方法区。③开始进行第一步的创建Sample对象,这个创建地址是在堆区,这个Sample实例持有着指向方法区的Sample类的类型信息的引用(Sample在方法区的内存地址)。④test1是一个局部变量,根据前面几章内容可以知道,这个test1肯定是入栈,"="这个指令会将test1变量指向堆区中的Sample实例,也就是说,test1就是之前第三步创建的Sample对象的引用。
到这里为止,虚拟机完成了一条简单的执行任务,可以参考如下图加深理解(手工画的很好难看,有时间重画一份。。。):
Sample test1 = new Sample("测试1");
【2.2.6】 *常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Poll Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
Java虚拟机对Class文件每一部分(自然包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,但对于运行时常量池,Java虚拟机规范并没有做任何要求,不同的是提供商实现的虚拟机可以按照自己的需求来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
关于常量池,几乎是Java面试必问内容,所以这里再多说几点。给出一个题目,请以此说出程序的打印结果,并说明原因:
public class demo {
public static void main (String[] args){
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
String str5 = str4.intern();
sout(str1==str2); //第一行,true
sout(str1==str3); //第二行,false
sout(str1==str5); //第三行,true
sout(str3==str4); //第四行,false
sout(str3==str5); //第五行,false
sout(str1.equals(str2)); //true
sout(str1.equals(str3)); //true
sout(str1.equals(str5)); //true
sout(str3.equals(str4)); //true
sout(str3.equals(str5)); //true
}
}
在回答这道题之前,首先需要搞清楚字符串比较时的两个常用连接符,==和equals。
- 关于== 在Java中有8中基本数据类型,当我们使用==号来比较这8种基本数据类型时,它比较的是操作数的值之间的关系。比如下面的代码,a一定是等于b的,因为他们的值都为1.
int a = 1;
int b = 1;
- 关于equals 除了8种基本数据类型以外,在Java中,一切皆是对象,对于对象的比较,必须使用所有对象都使用的equals方法。如果你坚持使用==号进行比较,那么Jvm比较的就是对象的内存地址。
以上的两条解释选自《Java编程思想》之3.7.1--测试对象的等价性。
有了对==号和equals的基本了解,我们来看上面的题目。
第一行:str1和str2在赋值时,使用的是字符串常量。会把字符串直接放入常量池中,从而实现复用。因此str1和str2引用至常量池中的"abc",所以返回true。
第二行:str3是new关键字创建的,new关键字代表创建一个新对象,因此jvm会在堆中创建一个String对象然后将str3引用至堆中。而str1指向的是一个常量池中的旧地址,因此str1和str3肯定是不同的,所以返回值是false。
第三行:str5是用String类的intern()方法创建的。jdk文档中对intern方法是这样描述的:"返回一个常量池中的固定对象。当intern方法被调用时候,如果常量池中已经包含了这个String对象,那么直接返回这个对象。否则,就向常量中添加这个对象,并返回他的引用"。(翻译来自JDK文档)
public String intern() Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java Language Specification. Returns: a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
通过上面的描述,我们清楚的知道,str5实际上和str1、以及str2都引用至常量池中的"abc"。因此,返回true。
第四行:看到俩new关键字就晓得肯定false啦。
第五行:str3是指向新创建的内存地址,而str5指向常量池中的对象地址,两者是不可能相等的,因此返回值是false。
对于第六、七、八、九、十行:他们全部使用eqauls()方法进行比较。关于String类的equals方法,JDK文档是这样说的:"equals方法返回true当且仅当它的入参对象不为空并且他们代表相同的字符串内容"
说白了,使用equals()方法比较的是两个字符串的内容,由于str1、str2、str3、str4、str5这五个对象的内容都是"abc",因此它们使用equals方法比较全部都是相等的。所以返回值全部是true。
补充一点,对于第三行的代码,有面试官喜欢提问"JVM创建了几个对象",答案是一个或两个。因为有用的new关键字,所以堆中出来一个实例,然后他的值是"abc",但同时如果这个"abc"不存在于常量池,JVM还会在常量池中创建"abc"对象。
【2.2.7】 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常的出现,所以我们放到这里一起讲解。
在JDK1.4中加入了NIO(New Input/Output或者说Non-blocking I/O非阻塞 非常之重要的概念)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致扩展时出现OutOfMemoryError异常。
【2.3】 HotSpot虚拟机对象探秘
介绍完Java虚拟机的运行时数据区之后,我们大致知道了虚拟机内存的概况,读者了解了内存中放了些什么后,也许就会想更进一步了解虚拟机内存中的数据的其他细节,譬如它们是创建、如何布局以及如何访问的。对于这样涉及细节的问题,必须把讨论范围限定在具体的虚拟机和集中在某一个内存区域上才有意义。基于实用优先原则,笔者以常用的虚拟机HotSpot和常用的内存区域Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
【2.3.1】 对象的创建
Java是一门面向对象的程序语言,在语言层面上,创建对象(克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机层面中,对象的创建又是什么样的过程呢?
虚拟机遇到一条new指令时,首先检查这条指令的参数能否定位到常量池中一个符号的引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需分配内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来(划分过程作者也有详细的说明,由于篇幅太长这里只提一下,划分有2种方法:①指针碰撞②空闲列表)由于考虑到划分内存也不是一个安全行为,如果在高并发的环境下,创建对象是一个非常频繁的行为,即便仅仅移动指针,也可能出现脏读。解决这个问题有2个解决方案:第一种是对分配内存空间这种行为进行同步处理,保证分配行为的原子性。第二种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,成为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过 -XX:+/-UseTLAB 参数决定。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化零值,如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应零值。
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
在上述工作都完成之后,从虚拟机的角度来看,一个新的对象已经诞生了,但从Java程序的视角来看,对象创建才刚刚开始------
但从Java角度来看,对象才刚刚开始——init方法还没执行,等对象按照程序员的意愿进行初始化,这样一个正真可用的对象才算产生出来。
【2.3.2】 对象的内存布局
HotSpot虚拟机中,对象的存储分为3个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(padding)。
- Header(对象头):存放运行时数据和类型指针
-
- 运行时数据:比如HashCode,GC分代信息,线程信息等等
-
- 类型指针:对象指向它的类元数据指针
- Instance Data(实例数据):存储实例的有效信息,各种字段内容。根据分配策略进行分配(同类字段分配到一起,父类中定义的变量会出现在子类之前)
- Padding(对齐区域):不必然,主要的意义是来填充多余的内存(对象的大小必须是8字节的整数倍)。
*【2.3.2】 对象的访问定位
建立对象是为了使用对象
我们Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中对象的具体位置。目前主流的方式有两种:使用句柄和直接指针。
两种方式的特点:
- 句柄池
-
- reference存储的是稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference本身不用改变。(注:垃圾回收时移动对象是非常频繁的操作)
- 直接指针
-
- 速度更快,节省了一次指针定位带来的程序开销,由于对象的访问非常频繁,节省下来的开销也是相当可观
而Sun HotSpot使用的是第二种,直接指针来进行对象访问的,但从整体来看,各种语言和框架使用句柄来访问的情况也是十分常见。