java虚拟机中对象的具体存储方式

01 创建对象的方式

(1) new 构造方法()
调用构造方法
单例的xxxgetInstance或xxBuilder()或xxxFactory()
(2) Class的newInstance()
jdk9被标记为过时 只能调用空参构造器 访问权限为public
(3) Constructor的newInstance(xx)
可以调用空参和带参的构造方法 权限没有要求
(4) Object的clone()
不调用任何构造器
要求当前类实现Cloneable 实现clone()方法
(5) 反序列化
不会调用构造方法
从文件或者网络中获取对象的二进制流
调用java.io.ObjectInputStream对象的readObject()方法
(6) 第三方库
如Objenesis


02 对象创建步骤

  (1) 判断对象对应的类是否加载 链接 初始化
                虚拟机遇到new指令 先检查这个指令的参数能否在Meatspace的常量池中定位到一个类的符号引用 并且检查这个符号引用代表的类是否已经被加载解析和初始化(判断元信息是否存在) 若没有在双亲委派模式下用当前类加载器以ClassLoader+包名+类名为key查找对应的class文件 如果没有找到文件 则抛出ClassNotFoundException异常 若找到加载类并生成对应的Class类对象

 (2)为对象分配内存  
             计算对象占用空间大小  在堆中分一块内存给对象 
             若实例成员变量是引用变量 仅分配引用变量空间
             采用哪种分配方式由java堆是否规整决定  
             堆是否规整由采用的垃圾收集器是否有压缩整理功能决定
             (1) 内存是规整---指针碰撞分方式
                    已使用内存和未使用内存分开 中间放指针作为分界点 
                    分配内存时把指针向空闲边挪动一段与对象大小相等的距离 
                    若垃圾收集器是Serial ParNew(基于压缩算法)  采用这种分配模式 
                    一般采用带有compact(整理)过程的手记器时 使用指针碰撞    
            (2) 内存不规整---空闲列表(Free List) 方式
                    已使用的内存和未使用内存交错在一起 
                    虚拟机维护一个列表记录哪些内存块可用
                    从列表中找到一块足够大的空间分给对象实例 并更新列表上的内容 
           
  (3)   处理并发安全问题
             堆空间是共享的 多个线程new对象时会产生并发线程安全问题
             会采用下面两种方式 
                    CAS失败重试  区域加锁保证更新的原子性
                    每个线程预先分配一块TLAB  通过-XX:+/uSETLAB参数与来设定

(4)初始化分配到的空间
所有的属性设置默认值 保证对象实例字段在不赋值时可以直接使用

(5 设置对象的对象头
将对象所属类(类的元数据信息) 对象的HashCode和对象的GC信息锁信息等数据存储在对象的对象头中 这个过程的具体设置方式取决于JVM实现

(6)显示初始化
初始化成员变量 执行实例化代码块 调用类的构造方法 并把堆独享的首地址赋值给引用变量
因此一般来说(由字节码中是否跟随有invokespecial指令所决定) new 指令之后会接着就是执行方法 把独享按照程序员的医意愿进行初始化 这样一个真正可用的对象才算完全创建出来


01 对象内存布局

对象在内存中存储包括三部分
(1) 对象头(Header)
包括两部分(数组是三部分)
(1) 类型指针(类元信息)Class Pointer/JDK底层是叫markOop
指向该对象的类元信息的首地址(Class MetaData Address),确定该对象所属于的类型
在64位系统中,开启指针压缩占4个字节,不开启压缩指针占据8个字节
(2) Mark Word/(对象标记)/JDK底层是叫klassOop
32位系统中占4个字节(32位),64位系统中,占8个字节(64位)
如哈希码/GC分代年龄/锁状态标志/线程持有的锁/偏向线程ID/偏向时间戳等
哈希值在调用时候才会存储, 每调用的话不会存储
存储结构并非固定,会根据本身状态复用自己的空间。除默认存储结构外还有其他可能结构存储更多信息
在32位JVM 中,Mark Word 的默认存储结构(无锁结构)如下:
25个bit------对象 HashCode
4个bit-------对象分代年龄
1个bit-------是否是偏向锁,默认为0
2个bit-------锁标志位,默认为01

                  (3) 如果是数组,还记录数组的长度

  (2) 实例数据(Instance Data)
        存储对象的数据,包括类中属性(包括从父类继承的),若是数组的实例还包括数组长度,这部分内存按4字节对齐
        规则
             相同宽度的字段总是被分配在一起
             父类中定义的变量会出现在子类之前
             若参数CompactFields为true(默认是true),子类的窄变量可能插入到父类的变量的间隙

   (3)  对齐填充
               仅仅起到占位的作用,保证一个对象的内存大小是8字节的倍数(hotSpot要求的),不是必须的

Mark Word的内容
在这里插入图片描述

64位虚拟机,GC标记标识对象要被回收了
在这里插入图片描述

32位虚拟机
在这里插入图片描述


句柄访问
在这里插入图片描述
直接指针(HotSport采用的)
在这里插入图片描述


打印出java虚拟机的参数
java -XX:+PrintCommandLineFlags -version

在terminal中输入这个命令,可以看大里面有一个是-XX:+UseCompressedClassPointers,就是开始压缩指针
默认是开启的
E:\java\ideaProjects\jdbcTest\target\classes\com\test>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=64576128 -XX:MaxHeapSize=1033218048 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-
UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version “1.8.0_92”
Java™ SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot™ 64-Bit Server VM (build 25.92-b14, mixed mode)


开启压缩指针的话,类型指针针具4个字节,不开启的话占据8个字节
开启的情况查看JOL章节
关闭指针压缩
-XX:-UseCompressedClassPointers

org.openjdk.jol
jol-core
0.9

Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());

第一行和第二行是Mark Word 后两行是类型指针
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 1c f3 13 (00000000 00011100 11110011 00010011) (334699520)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
开启指针压缩,默认是开启的
第一行和第二行是Mark Word 第三行是类型指针,第四行是对齐填充
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 28 0f ff 7f (00101000 00001111 11111111 01111111) (2147421992)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


01 四种引用

  不同的引用类型区别主要体现在GC上,在内存空间不同时回收不同的引用

为什么有4种引用(jdk1.2后有的)
C语言可控制内存的申请和释放,在Java中有时我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对GC回收时机不可控的妥协。有以下几个使用场景可以充分的说明:
利用软引用和弱引用解决OOM问题:用HashMap保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题
通过软引用实现Java对象的高速缓存:比如我们创建了Person类,若每次需查询一个人的信息,哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这将引起大量 Person对象的消耗,并且由于这些对象的生命周期相对较短,会引起多次 GC 影响性能。此时,通过软引用和 HashMap 的结合可以构建高速缓存,提供性能


02 强引用(Strong Reference) 不回收

    最常见的引用类型(99%以上都是强引用)
    new创建一个对象将其赋值给一个变量时,这个变量就是指向该对象的一个强引用
   强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象
   对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将引用赋值为null,就可以被垃圾被收集了,具体回收时机还是要看垃圾收集策略
  相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成Java内存泄漏的主要原因之一
  强引用可能导致内存泄漏

  强引用具备以下特点
         强引用可以直接访问目标对象
         强引用对象不会被系统回收,哪怕抛出OOM,也不会回收强引用所指向对象

强引用对用的类是Reference


03 软引用(Soft Reference) 内存不足即回收

     内存空间足够时,软引用不会回收,只有内存不足时,软引用才会被GC回收
     只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常
     软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。若还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存
    垃圾回收器在某个时刻决定回收软可达的对象时,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue),通过这个队列可以跟踪对象的回收情况
   软引用适合做缓存,文件缓存,图片缓存等

在JDK1.2版后提供了java.lang.ref.SoftReference类来实现软引用,继承了强引用类Reference


04 弱引用(Weak Reference) 发现即回收

  只能生存到下一次垃圾收集发生为止,GC时只要发现弱引用,不管堆空间使用是否充足都会回收
  垃圾回收器线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象,在这种情况下,弱引用对象可以存在较长的时间

  弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况

  软引用、弱引用都非常适合来保存那些可有可无的缓存数据,如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用

在JDK 1.2版后提供了java.1ang.ref.WeakReference类来实现弱引用,继承了强引用类Reference


05 虚引用(Phantom Reference) 对象回收跟踪

   也称幽灵引用或幻影引用,是所有引用类型中最弱的一个

   一个对象是否有虚引用的存在,完全不会决定对象的生命周期,若一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收

   不能单独使用,也无法通过虚引用来获取被引用的对象,通过虚引用的get()方法取得对象时,总是null

  目的是跟踪垃圾回收过程,跟踪垃圾被回收的状态。如:能在这个对象被收集器回收时收到一个系统通知

  虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数,垃圾回收器回收对象时若发现有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况,如果引用队列中多了一个对象,说明有一个虚引用对象被回收,通知做下一步的处理,用来实现比finalize更灵活的回收机制

 由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录

在JDK 1.2版之后提供了PhantomReference类来实现虚引用 ,继承了强引用类Reference,当徐引用被回收后就会装到RederenceQueue中


06 终结器引用

 它用以实现对象的finalize()方法,也可以称为终结器引用
 无需手动编码,其内部配合引用队列使用
 在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。

07 对比

引用类型垃圾回收时间用途生存时间强引用从来不会对象的一般状态JVM停止运行时终止软引用在内存不足时对象缓存内存不足时终止弱引用在垃圾回收时对象缓存gc运行后终止虚引用Unknown标记,哨兵Unknown

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值