文前角色简介
💡yb:一个练习两年半的java程序员,菜鸡一枚,但为人谦虚低调,喜欢脚踏实地钻研技术。
💡c某:yb的师兄,自喻上知五百年,下知五百年,中间还知五百年,天文地理无所不知框,无所不晓的国企摸鱼程序员,抬杠学的忠实爱好者,酒桌装x小能手。
前文
又是一个周末,好不容易不加班的yb正躺在床上睡着懒觉。
“只因你太美baby,只因你太美baby,只因你是在是太美baby........"
yb的手机突然响起铃声。yb不得不皱着眉头拿起电话:喂谁呀,大周末的还让不让人睡懒觉了。
是yb吧,我是你师兄c某,你忘了上周末不是约好了来我家吃炒粉嘛?电话那头传来c某自信的声音。
yb心想,上周是问了师兄一个关于jvm对象的问题来着,当时师兄借口没空,说下周去他家吃他练习了两年半的炒饭,到时候他好好给我讲解讲解。莫非师兄现在已经准备好了。于是答应到:等会,等我洗漱了,马上过来。
不一会,yb已经来到了c某家中,对着c某做的炒粉大快朵颐后。迅速进入正题:师兄你今天叫我过来,不只是为了吃个炒粉吧。
c某:当然不是,你小子还记得上周问我的那个问题吗?
yb:😤记得记得,当时让你给我讲讲对象来着,结果你尿遁了,搞得最后还是我去结得帐。可谓是记忆深刻。
c某闻言尴尬地说道:那不是真的快憋不住了嘛。今天叫你过来就是打算好好给你讲解讲解这个问题的💩。
yb:好呀好呀。
c某:在给你讲解之前先问你一个问题,你了解你的对象吗?
yb笑着说: 😓你这不是为难我y莫人嘛,我母胎单身二十多年了,你让我去哪儿搞个对象了解呀。
c某:.........
1、对象的创建过程?
c某:众所周知,java是一门面向对象的编程语言,在程序的运行过程中,会创造出无数个对象出来。对于你来说,可能创建对象只是简简单单地用一个new关键字而已,但是对于jvm来说可不是这么简单。首先jvm遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来jvm将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从jvm堆中划分出来。一般主流分配内存空间的方式有两种,一种是“指针碰撞”,一种是“空闲列表”。
我们先说说指针碰撞是怎么一回事。
1.1 指针碰撞
这种分配内存的方式主要是针对堆中内存是绝对规整的情况下,这个时候堆中所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
1.2 空闲列表
这种分配内存的方式主要是针对堆中内存空间不规整的情况下。堆中已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,jvm就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
由上可以看出采用那种内存分配方式,由jvm中堆的内存结构是否规整所决定,而jvm中的堆内存结构又是由采用的垃圾收集器是否带有压缩整理功能决定。因此采用那种内存分配方式,与采用那种垃圾收集器密切相关。
目前使用Serial、ParNew等带Compact过程的收集器,jvm使用的是指针碰撞。
而使用CMS这种基于Mark-Sweep算法的收集器时,jvm使用的是空闲列表。
在分配完对象之后,jvm需要将分配到的内存空间都初始化为零值(不包括对象头),接下来,jvm要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据jvm当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
再完成这一步之后,这个对象已经创建了百分之八十了。但是对象的所有字段还是为空,方法还没有执行。完成上述操作后,会执行方法把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
说到这,c某停了下来,吞了吞口水,故作高深的说:yb呀,你小子现在明白了,对象在jvm中的创建过程了吗?
yb心想,看来师兄这个星期是有准备的呢。这次是要好好给我上一课,报上次的仇呀。不行我得找两个问题出来挫挫他的锐气。于是说到:师兄,你刚才说jvm分配完内存后,需要对对象进行必要的设置,即从对象的对象头里拿出信息然后对对象进行设置。但是我不太理解对象头这个概念,这到底是什么东西呀。
c某心想,好小子,找茬是吧😏,还好我准备了一个星期,今天随你怎么问,我要是不知道,我就不姓c。于是强装淡定的说道:要说明白这个问题就要先了解对象的内存布局即对象的内存结构。
2、对象的内存结构
在jvm中(以HotSpot为列)对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
2.1 对象头
我们首先来看看对象头,对象头又由对象运行时数据和类型指针这两部分构成。
其中对象运行时数据包含对象哈希码(HashCode)、GC分代年龄、线程持有的锁、偏向线程ID、偏向时间戳等信息。
类型指针主要就是指对象指向它的类元数据的指针,jvm通过这个指针来确定这个对象是那个类的实列。
需要注意的是,对于数组对象的对象头还需要具有一块用于记录数组长度的空间。因为jvm可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
2.2 实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到jvm分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
2.3 对齐填充
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpotJVM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
说到这儿,c某一脸傲娇的看着yb:yb呀,你懂了吗?
yb心想你倒是接着往下讲呀,讲一半停下来,这不明显想让我夸夸你嘛。于是只好无奈地说到:高,实在是高呀,师兄的学识真是如汪洋一般深邃不可见底呀。
c某心想你小子还挺上道,只是这演技太浮夸了,还得历练历练呀。于是说到:过了,过了,你小子彩虹屁夸过头。这么浮夸,一点都不真。忽悠我都不行,以后还怎么自然地去拍领导马屁,还得练呀。对了,yb呀,你知道在使用对象时,我们是如何在定位到堆上的对象的吗?
yb:这我还真不知道,我还没有深究过这块知识。
c某:得,就当买一送一,再给你普及普及对象是如何被定位访问到的吧。
3、对象的访问定位
建立对象的目的是为了使用对象,java程序通过栈上的reference数据来操作堆上的具体对象。由于reference类型在jvm规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。
目前主流定位访问对象方式有两种,一种是使用句柄,一种是通过直接指针访问。
我们先说说第一种方式吧。
3.1 使用句柄访问对象
这种方式,会在堆中划分出一块内存来作为句柄池。reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
3.2 直接指针访问对象
使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
3、结尾
说了很多的c某停下来,喝了杯水。
yb心想,看来师兄这个星期是准备了很多呀,这全是干货,不掺水的那种。但听了这么多内心还是有些疑问,于是问道:师兄呀,对象的创建,内存结构,包括对象的定位。你都讲清楚了。但是java是一门面向对象的语言,一个简单程序都可能产生无数个对象,这么多对象都放到内存的话,这无疑是对内存巨大的伤害。我想一定存在某种机制管理着这么多对象吧,如果任由这些对象杂乱无章的话,JVM内存不是无时无刻都有被撑爆的风险。
c某听着虎躯一震,心想这小子还挺难忽悠的,是有自己的思考的呀,完了完了,他问的我也不知道呀。好不容易建立的形象难道就要崩塌了吗?不行不行,忽悠,我必须忽悠。于是咳了一声,故作镇定的说:这个我当然知道了,但是我不能直接告诉你,这样对你不好,你需要自己摸索,自己去探究,这样学下来的才是你的。我直接和你说,这是害了你呀,对你印象不深刻。
yb将信将疑的小声嘟囔着:你不会也不知道吧。
c某老脸一红,瞪着眼睛:瞎说什么呢?我我我.....我怎么可能不知道呢,我只不是为了你好,你怎么这么不心疼你的giegie呀。那个我尿又来了,先上个厕所。
看着又使出尿遁的c某,yb内心:..........
yb内心:看来还得靠自己呀。