JAVA对象笔记

引言

java是面向对象的一门语言,而多态是面向对象最主要的特性之一,它是一种方法的动态绑定(后期绑定),实现运行时的类型决定对象的行为,多态的表现形式是父类引用执行子类对象,在这个引用上调用的方法使用子类的实现版本,并且多态是实现IOC、模板模式的关键。对应动态绑定的即是静态绑定(前期绑定),java方法里final、static、private和构造方法算是静态绑定。

在C++中,通过虚函数表的方式实现多态,每个包含虚函数的类都具有一个虚函数表,在这个类对象的地址空间的最靠前的位置存有指向虚函数表的指针。在虚函数表中,按照声明的顺序依次排列所有的虚函数,由于C++在运行时并不维护类型信息,所以在编译时直接在子类的虚函数表中将被子类重写的方法替换掉。在java中,在运行时会维护类型信息以及类的继承体系,每个类会在方法区中对应一个数据结构用于存放类的信息,可以通过Class对象(字节码对象)访问这个数据结构。其中,类型信息具有superclass属性指示了其超类,以及这个类对应的方法表(其中只包含这个类定义的方法,不包括从超类继承来的)。而每一个在堆上创建的对象,都具有一个指向方法区类型信息数据结构的指针,通过这个指针可以确定对象的类型。

在Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。


对象头

存储对象自身的运行时数据

哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为"Makr Word"。对象要存储的运行时数据很多,其实已超出32位、64位Bitmap结构所记录的限度,但对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象状态复用自己的存储空间。

//
//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time
//



在32位HotSpot虚拟机中,如果对象处于未被锁定状态,Mark Word的32位空间分成:
25bit:存储对象的哈希码
4bit:存储对象分代年龄
2bit:存储锁标志位
1bit:在不同状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如
 

锁状态

25bit

4bit

1bit

2bit

23bit

2bit

是否偏向锁

锁标志位

无锁态

对象的HashCode

分代年龄

0

01

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向重量级锁的指针

10

GC标记

11

偏向锁

线程ID

epoch

分代年龄

1

01


 

  1. 锁标志位(lock):占2bit,01-无状态锁、偏向锁,00-轻量级锁,10-重量级锁,11-GC标记
  2. 偏向锁标志位(biased_lock):占1位,1-启用偏向锁,0-没有偏向锁
  3. 分代年龄(age):占4位,在GC中,如果对象在Survivor区复制一次,年龄增加1,当对象达到设定的阈值时,将会晋升到老年代。并行GC默认阈值为15,并发GC阈值为6,由于age占4位,最大值是15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因
  4. 对象的hashcode(identity_hashcode):占25位,采用延迟加载技术,调用方法System.identityHashCode()计算,并将结果写到该对象头中,当对象被锁定时,该值会移动到管程Monitor中
  5. 线程ID(thread):持有偏向锁的线程ID
  6. epoch:偏向时间戳
  7. 轻量级锁指针(ptr_to_lock_record):指向栈中锁记录的指针
  8. 重量级锁指针(ptr_to_heavyweight_monitor):指向管程monitor的指针

64位虚拟机中,对象头差不多

锁状态57bit4bit1bit2bit
25bit29bit2bit1bit是否偏向锁锁标志位
无状态unused对象的HashCodeunused分代年龄001
轻量级锁指向栈中锁记录的指针00
重量级锁指向重量级锁的指针10
GC标记11
偏向锁线程IDepochunused分代年龄101

JDK1.6之后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级为重量级锁。

32位JVM一般这样使用锁和Mark Word的

  1. 当没有被当成锁时,就是一普通对象,Mark Word记录对象的HashCode,锁标志位为01,是否偏向锁为0.
  2. 当对象被当成同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是偏向锁位改成了1,前23位记录线程ID,表示进入偏向锁状态.
  3. 当线程A再次试图获得锁时,JVM发现同步锁对象的锁标志位为01,偏向锁位为1,偏向状态,但是线程ID就是线程A自己的ID,标识A已经获得过这个偏向锁,可以执行同步锁的代码.
  4. 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是线程ID记录的不是线程B的ID,那么线程B会先用CAS操作试图获得锁,这里的锁操作是可能成功的,因为线程A一般不会自动释放偏向锁,如果抢锁成功,那么Mark Word里的线程ID修改为线程B的ID,代表线程B获得了这个偏向锁,可以执行同步锁代码,如果抢锁失败,则执行步骤5.
  5. 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁升级为轻量级锁,JVM会在当前线程的线程栈中开辟一块单独的空间Lock Record,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针,上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位修改为00,可以执行同步锁代码,如果保存失败,表示抢锁失败,竞争太激烈,则执行步骤6.
  6. 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁,从1.7之后,自旋锁默认开启,自旋次数由JVM决定,如果抢锁成功,则执行同步锁代码,如果抢锁失败,则执行步骤7.
  7. 自旋锁重试之后抢锁还是失败,同步锁会升级到重量级锁,锁标志为修改为10,在这个状态下,未抢到锁的线程都会被阻塞.

2.类型指针

该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。JVM默认使用+UseCompressedOops开启指针压缩,静态变量、对象变量、对象数组中的元素都会被压缩至32位,Class字节码对象指针、本地变量、堆栈元素、入参、返回值和NULL指针则不会被压缩。如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

实例数据

原生类型(primitive type)的内存占用

boolean 1
byte1
short2
char2
int4
float4
long8
double8


引用类型在32位系统上每个占用4bytes, 在64位系统上每个占用8bytes。

 

对齐填充

由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍,而对象头正好是8字节的整数倍,而对象实例数据部分没有对齐,需要通过对齐填充来补全。

也就是说:

(对象头 + 实例数据 + padding) % 8等于0且0 <= padding < 8

 

 

======================================

一个对象占用多少字节?
======================================

那么对象的模板-类是怎么加载的呢。

类加载 加载-链接-初始化

加载:查找并加载类的二进制数据  

 加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
    1、通过一个类的全限定名来获取其定义的二进制字节流。

    2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

    相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

    加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

连接

– 验证:确保被加载的类的正确性

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。 

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。

 – 准备:为类的静态变量分配内存,并将其初始化为默认值

   准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。注意:

这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
– 解析:把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

初始化,则是为标记为常量值的字段赋值的过程。

换句话说,只对static修饰的变量或语句块进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

 

对于类的加载顺序:

第一次new一个对象

最高父类静态初始化块/最高父类静态field声明时初始化 --> ... -->直接父类静态初始化块/直接父类静态Field声明时初始化 --> 本类静态初始化块本类静态Field声明时初始化 // 这个主要是初始化类

--> 最高父类非静态初始化块/最高父类非静态Field声明时初始化 --> 最高父类构造器 --> ... --> 直接父类非静态初始化块/非静态Field声明时初始化 --> 直接父类构造器 --> 本类非静态初始化块/本类非静态Field声明时初始化 --> 本类构造器

第n(n >= 2)次new一个对象

最高父类非静态初始化块/最高父类非静态Field声明时初始化 --> 最高父类构造器 --> ... --> 直接父类非静态初始化块/非静态Field声明时初始化 --> 直接父类构造器 --> 本类非静态初始化块/本类非静态Field声明时初始化 --> 本类构造器

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值