Java中的对象

目录

一、封装继承和多态

封装

继承

多态

二、JAVA对象

JAVA对象内存分配

JAVA对象内存布局

对象的访问定位


一、封装继承和多态

我们都说JAVA是面向对象编程的语言,有三大特性封装、继承和多态。

封装

        封装简单的理解就是对类内部的属性和方法通过一些关键字的修饰,进而达到隐藏内部细节的目的。我们在类中通过public、protected、default、private等修饰符修饰属性和方法。然后通过对象实例化之后才能对类中的属性或方法直接调用(静态方法除外)。出于数据保护的考虑,对某些属性,某些方法(例如单例中的构造函数)使用了private关键字。封装在一定程度上增加了代码的简洁性和安全性。

继承

        继承就是子类继承父类,但是JAVA是一个单继承的,不能继承多个父类。另外一点需要说明的是,在继承关系中,子类并不能继承父类的构造函数。       

多态

        多态是对对象行为的一种描述,在java中基于封装和继承特性,定义了编译时多态和运行时多态。编译时的多态就是方法的重载(方法名相同,参数列表不同),运行时的多态是基于继承特性实现的一种重写(子类重写父类方法)。而在Java中实现多态,需要有几个前提继承、重写(不是必须)、有父类引用指向子类对象。

二、JAVA对象

        每天都面向对象编程那么JAVA中的对象到底怎么被创建出来的呢,对象到底长什么样子呢?一个对象占多少内存,在虚拟机栈中如何定位到对象的,带着这些疑问,我们一起探讨一下。

JAVA对象内存分配

        这里我们讨论的对象创建主要是指new 关键字创建的对象,不包括克隆或则反序列化。当JAVA虚拟机遇到new字节码的指令时,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个类是否被创建过,是否已经被加载、解析、初始化过,如果没有那么必须先执行类的加载过程(类的加载机制后续会慢慢补充)。

        如果没有被加载过,那就开始创建对象。创建对象时首先为该对象在堆上分配一块内存,对象分配内存的大小在类加载完成之后便可确定。那么如何为对象分配内存呢?一般有两种方式,指针碰撞和空闲列表。

指针碰撞

         如果JAVA堆中的内存是规整的,被占用的内存在一边,未被占用内存在另一边,指针放在分界线上,当为对象分配内存时,只需要将指针向未被占用的方向移动一段大小正好为新对象大小的距离,这种分配内存的方式为指针碰撞(Bump the Pointer)。

        但是这种指针碰撞在并发情况下并不是线程安全的,假如一个线程A修改了指针位置,在指针位置还没有来得及修改,另一个线程B却修改了指针位置,然后A又修改了指针位置,这个时候线程B操作的对象就发生了改变。解决这个问题有两种方式,第一种为对象分配内存空间使用同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同空间中操作,即每个线程在JAVA堆中预先分配一小块内存,称为本地线程分配缓存TLAB(Thread Local Allocation Buffer),那个线程要分配内存,就在那个线程的本地缓冲区中分配(感觉有点类似ThreadLocal的思想),当本地缓存用完了,分配新的缓存区时才需要同步锁定。

空闲列表

         如果JAVA堆中内存不是规整的,使用的内存和空闲内存交互在一起,那么就没法进行指针碰撞了,虚拟机必须维护一个列表,记录那些内存是空闲的,那些内存是已使用的,那么在分配对象的时候就会从列表中找一块足够大的空间分给对象实例,并更新记录列表,这种方式称为空闲列表(Free List)。

        以上选择那种分配方式,取决于JAVA堆中的内存是否规整,堆中的内存是否规整又取决于JVM使用哪种垃圾回收器,像Serial、ParNew这种基于标志整理的垃圾回收器,jvm采用指针碰撞的方式分配内存,而像CMS这种基于标记清除的回收器,理论上JVM采用空闲列表分配内存。

        那么当为对象分配完内存之后,就要进行加载、验证、准备、解析、初始化等操作。在上面这些执行完之后,从JVM角度来所,一个对象已经创建完成,但是从程序员角度来看创建对象才开始。

JAVA对象内存布局

        在JVM虚拟机中对象在堆中一般分为对象头、对象体(实例数据)、填充对齐。

对象头

对象头包括三个字段 Mark Word(标志字,一般存储GC标记,哈希码、锁状态等)、Class Pointer (类对象指针,用于存放方法区class对象地址,虚拟机通过这个指针确定这个对象是哪个类的实例)、Array Length(数组长度 ,此字段当对象是一个java数组时,必须有这个字段,其他类型对象此字段是不存在的)。

在32位中对象头的结构部布局如下图

在这里插入图片描述

 

在64位中对象头的结构部布局如下图 

 我的电脑是64位的,那就用代码验证一下

首先引入pom依赖

  <dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.10</version>
  </dependency>

在32位系统中, Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;
在64位系统中, Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;为了更好的观察java对象的内存布局关闭JVM的指针压缩,修改JVM参数-XX:-UseCompressedOops

public class User {

    public static void main(String[] args) {
        User user = new User();
        System.out.println("16进制对象的hashcode: "+Integer.toHexString(user.hashCode()));
        System.out.println("二进制的hashcode: "+Integer.toBinaryString(user.hashCode()));
        System.out.println( ClassLayout.parseInstance(user).toPrintable() );
        }
//        0111101 01100100 01101100 00110111
}

 

再来看一下hashcode,由于大小端的原因,所以这些存储的信息都是反着的,注意观察

 从上面的分析我们可以得出结论64位虚拟机中一个空对象至少要占16字节,32位虚拟机下一个空对象至少要占8个字节。

对象体

对象体包含对象的实例变量(成员变量),用于成员属性,包括父类成员属性值,这部分内存按照4字节对齐

填充对齐

也叫对齐字节,其作用是用来保证JAVA对象所占内存字节数为8的整倍数。当对象头本身不是8的整倍数时,需要填充数据来保证8字节的对齐。

对象的访问定位

        对象在堆中创建出来之后,那么在程序中如何访问这些对象呢?通常访问的方式有两种句柄池和直接指针。

句柄池的方式访问

 直接指针访问的方式

 

        从上面图上可以直观的看到,句柄访问的话reference中指向的是句柄池的位置。使用句柄访问最大的好处就是reference存储的是句柄地址,在对象移动时(垃圾回收后)只需要修改句柄池中对象的实例指针,而reference本身不需要修改,但是相对直接指针访问的来说访问速度会稍慢。对于直接指针访问优势就是直接访问速度更快,节省了一次指针定位时间的开销而在发生对象移动时,需要修改reference中的信息。在Hotspot虚拟机中主要是用直接指针访问的方式来定位对象的。

 参考 :  https://www.ngui.cc/el/1113662.html

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熟透的蜗牛

永远满怀热爱,永远热泪盈眶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值