暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

对象的那些事

逐码 2018-02-22
175

gris 转载请注明原创出处,谢谢!

说起java中的对象,大家肯定都不陌生。Java是一门面向对象的语言,在java中我们所做的一切都离不开对象。那到底什么是对象呢?要了解清楚对象是什么,我们首先要来了解清楚java的一个模型机制。为了更好的描述和理解,我将使用一些英文的术语。 java是建立在types和values的基础上的,一个value代表了一个特定的信息,而一个type就是一系列values的集合。当我们说value X是type Y的一个实例时,这其实就是说value X是type Y所代表的values集合中的某一个成员。所以实例是表示一种关系而不是一种东西。而在java中支持两种types:primitive type和reference type。而reference type又可以分为两种:Class和Array。一个java对象就是一个reference type的实例,例如,一个数组或者一个Class的实例都可以称为一个reference type的实例。因此,我们可以说“实例”和“对象”是同义词。下面让我们看一下几种不同的对象:

  1. // 一个Class的对象

  2. Class classObject = Class.forName("java.lang.String");

  3. // 一个StringBuilder的对象

  4. StringBuilder stringBuilderObject = new StringBuilder("hello");

  5. // 一个数组的对象

  6. int[] arrayObject = new int[2];

  7. System.out.println(classObject.getClass());

  8. System.out.println(stringBuilderObject.getClass());

  9. System.out.println(arrayObject.getClass());

以上的代码将打印下面的信息:

  1. class java.lang.Class

  2. class java.lang.StringBuilder

  3. class [I

其中[表示一个一维数组,二维数组用两个[[表示,I表示一个int的基础类型。 要想了解清楚对象,除了type模型之外,还要了解Class,因为类是创建对象的蓝图或者说类是对象的原型。要想创建一个对象,即当虚拟机遇到一条new指令时,首先将会去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行类的加载过程,具体的类加载过程可以点此查看。

对象的创建

终于来到了对象的创建这一步了,我们知道要想使用一个对象,必须要先将对象创建出来。那么在创建对象的过程中到底发生了什么事情呢,我们有必要来了解清楚。首先让我们大致的看一下对象创建的过程中发生了哪些事:

  1. 分配内存。

  2. 属性被初始化为默认值。

  3. 调用指定构造器的第一行代码,除非这是一个 Object
    对象.。第一行的意思是指要么明确的调用 super()
    或者 this()
    , 要么隐性的调用 super()

  4. 执行实例初始化器并将各个属性初始化为他们期望的值(实际上属性的初始化通常被编译为实例初始化器中的一个内联的部分)。

  5. 执行构造器中剩余部分的代码。

当类的加载检查通过后,虚拟机将会为新生的对象分配内存。一个对象所需要分配的内存的大小在类加载完成后就可以确定了,分配内存的过程就是一个把一块确定大小的内存从java堆中划分出来的过程。内存该如何去划分跟java堆中的内存是否规整有关。 如果内存规整,所有用过的内存放在一边,空闲的内存放在另一边,中间放一个指针,那内存的划分就是将指针向空闲的内存那边挪动一段与对象大小相等的距离即可,这种分配方式称为 “指针碰撞”
。 如果内存不规整,虚拟机必须维护一个列表,纪录哪些内存是可用哪些不可用。在分配内存的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的纪录,这种分配方式称为 “空闲列表”
。 内存分配通常来说是简单的,但若是在虚拟机中执行非常频繁的内存分配,那难免会出现线程安全的问题。为了解决这个问题,一般有两种解决方案:一种是对分配内存的过程进行同步处理;另一种是给每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),把内存分配按照线程划分到不同的空间中去执行。虚拟机是否使用TLAB,可以通过 -XX:+/-UseTLAB
参数来设置。

所有属性在使用之前必须要有默认值,通常这个默认值被称为“零值”,根据数据类型不同,零值也不一样,具体的零值可以参考下面的表格:

数据类型零值
int0
long0L
short(short)0
char'\u0000'
byte(byte)0
booleanfalse
float0.0f
double0.0d
referencenull

这些工作都完成之后,对象还没有真正的创建好。从程序的角度来看,对象的创建从这里才刚刚开始,接下来会执行 实例初始化器
--也就是<init>方法,该方法和类加载机制中 类初始化器
--<clinit>方法不同,<clinit>方法最多只会执行一次,而<init>方法在每次创建对象时都会执行。

对象的构成

了解完对象的创建过程后,我们需要知道对象都是由哪些东西构成的,每个不同的构成部分都有哪些作用,这对后期更加深入的了解其他的特性是非常有帮助的。 从对象在内存中存储的布局上来说,对象(最多)可以分为3块区域:

  • 对象头(Header)

  • 实例数据(Instance Data)

  • 对齐填充(Padding)

为什么说 最多
3块区域呢?因为对齐填充并不是必须的,可能有的对象中没有对齐填充,这个我们等会会了解到。

对象头

对象头(这里都是以HotSpot虚拟机为例)中包含两部分内容: Markword
Klasspointer
。 其中Mark word的长度根据不同的虚拟机架构而有所区别:在32位的虚拟机中是4byte(即32bit);在64位的虚拟机中是8byte(即64bit)。 在Mark word中存储着对象自身运行时的一些数据,这些数据都是非常重要的,他们包括:

  • 哈希码(HashCode)

  • GC分代年龄

  • 锁状态标志位

  • 线程持有的锁

  • 对象偏向的线程ID

  • 偏向时间戳

在HotSpot虚拟机的markOop.hpp(oop是 ordinaryobjectpointer
的意思)源代码的一部分注释中,有如下关于Mark word在不同的虚拟机架构下的布局描述:

  1. //  32 bits:

  2. //  --------

  3. //  hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)

  4. //  JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)

  5. //  size:32 ------------------------------------------>| (CMS free block)

  6. //  PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

  7. //

  8. //  64 bits:

  9. //  --------

  10. //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)

  11. //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)

  12. //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)

  13. //  size:64 ----------------------------------------------------->| (CMS free block)

  14. //

  15. //  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)

  16. //  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)

  17. //  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)

  18. //  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

其中,部分变量简单的描述如下:

hash: 保存对象的哈希码

age: 保存对象的分代年龄

biased_lock: 偏向锁标识位

lock: 锁状态标识位

JavaThread:保存持有偏向锁的线程ID

epoch: 保存偏向时间戳

可以看到Mark word中存储的信息主要跟垃圾回收和对象的锁机制有关。我们知道java中synchronized关键字就是通过对象锁来实现的同步,从少量的并发到大量的并发,对象锁也会呈现出不同的状态,从偏向锁,到轻量级锁,再到膨胀成重量级锁,而这一切都是保存在Mark word中的。对象在不同的锁状态下,Mark word中存储的内容是不同的,具体可以用下面这张表格来表示

对象头中的另一部分数据是Klass pointer,用来指向对象所属的类元数据。查找一个对象所属的类元数据信息就可以通过该Klass pointer,但这并不是唯一的方式。到目前为止我们所说的对象都是普通对象,对于数组类型的对象,那么对象头中还要有一块数据用来记录数组的长度。

实例数据

实例数据是对象中存储的实际操作时所需要的数据,包括定义的各种字段的值,还包括从父类中继承的字段的值。

对齐填充

前面说了 对齐填充
并不是必须的,他的作用就是一个占位符。因为在HotSpot中要求对象的起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。这就会导致一个问题,如果有的对象的大小正好是8字节的整数倍,那一切安好,如果有的对象大小不是8字节的整数倍,这时就需要通过对齐填充来将对象的大小补齐到8字节的整数倍。

对象的销毁

了解完了对象的创建和对象的构成之后,让我们再来了解一下对象的销毁。通常来说,我们无需关注对象的销毁,因为一个对象是否还有用,是否需要销毁并回收其内存,这是GC需要做的。说起对象的销毁,我们需要想到的就是 finalizer
,让我们暂且将他称作终结器吧。 java中的终结器和构造器是相反的。构造器用来执行一个对象的初始化,而终结器则是用来执行对象的销毁。垃圾回收会自动释放对象所使用的内存资源,但对象可能还保存着其他类型的资源,如打开的文件和网络连接等等。垃圾收集器无法释放这些资源,所以我们需要写一个对象的终结器,用来执行关闭文件、断开网络连接、删除临时文件等等。 终结器是一个不带参数,没有返回值的方法。每个类只且只能有一个终结器方法,它必须被命名为finalize()。终结器可以抛出任何异常或错误,但当终结器自动被垃圾收集器调用时,任何异常或错误将被忽略。 有以下几点需要注意的情况:

  • 如果一个对象有一个终结器方法,当该对象变得不再可用(或无法访问)时,在垃圾回收器回收该对象之前,终结器方法将会被调用。

  • java不能保证垃圾回收何时发生或者说对象将以何种顺序被回收。因此,无法保证终结器将在何时、以何种顺序、被什么线程所调用。

  • java可能会在垃圾回收器还未收集完全部的对象时就退出了,所以有一些对象的终结器可能永远不会被执行到。对此,为了解决这个问题,在java1.1中可以通过 Runtime
    方法 runFinalizersOnExit()
    来强制虚拟机在退出时执行终结器方法。但该方法可能会导致死锁的问题,因此在1.2中被废弃了。在1.3中可以通过 addShutdownHook()
    方法来安全的执行终结器方法。

  • 当终结器方法被调用后,对象并没有立即释放。这是因为可能在终结器方法中通过this指针将该对象进行复活。因此,当finalize()
    方法被执行后,垃圾收集器必须再次确定该对象没有引用了才能回收它。但是在 finalize()
    方法中复活一个对象,并不是一个好的方法,因为 finalize()
    方法只会被调用一次。可以通过继承 java.lang.ref.PhantomReference
    类来确保对象不能复活。

我是gris,如果文章对您有帮助,欢迎您点赞加关注,并欢迎您关注我的公众号:


文章转载自逐码,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论