3、JVM中的对象

1、对象的创建

 A  a = new A()

A:引用的类型

a::引用的名称

new A():创建一个A类对象

当创建一个对象时,具体创建过程是什么呢?

(1)JVM遇到new的字节码指令后,检查类是否被加载,否,进行类加载

(2)检查加载通过后,对新创建的对象在堆中分配内存

(3)将分配的内存空间进行初始化为0值

(4)设置对象头的信息,将对象的所属类(即类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象头中

(5)调用对象的构造方法进行初始化

2、对象内存的分配策略

对象创建的过程中需要为新对象在堆上划分出一块确定大小的内存空间,JVM中对于划分内存有两种策略,指针碰撞和空闲列表

指针碰撞:当内存空间绝对规整,使用中的内存放一边,未使用的内存放另一边,中间放由一个作为分界器的指针,当进行内存分配时,指针向空闲内存方向挪动与对象大小相等的距离

空闲列表:当堆上的内存空间不是绝对规整,使用和未使用的内存空间呈犬牙交错的形势,此时虚拟机需要维护一个列表,列表中记录了那块内存未被使用,分配内存时需要在列表中找到一块足够大的内存空间或分给新建的对象,并更新表中的记录。

其中指针碰撞的分配策略性能要更高一些,JVM采用哪种分配策略是由堆上内存空间是否绝对规整来决定的,内存空间是否绝对规整是由JVM采用哪种GC来决定的

给对象划分内存空间时,不仅要考虑内存的分配策略,还需要考虑到内存分配时的并发安全,JVM中是怎样确保内存分配时的并发安全呢?

JVM中创建对象十分的频繁,当对象A创建时,刚为其分配内存,还未更新指针或者列表时,对象B来创建,此时就会发生问题

JVM中为了保证并发情况下线程安全,采用了两种方案:CAS失败重试和分配缓冲

CAS失败重试:CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包(Java.Util.Concurrent)中的原子类都使用了CAS技术。

CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS失败重试流程:一块空白的内存,此时是null值,在空白的内存中划分出一块与申请对象大小一致的内存,划分完之后,再来看内存是否为null,是,为对象分配内存成功,否,说明在划分的过程中有别的线程来对这块内存进行了分配的操作,为对象分配内存失败,找到下一块空白的内存,继续上述操作

分配缓冲:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),JVM 在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个 Buffer,如果需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞争的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden 区域申请一块继续使用。

TLAB 的目的是在为新对象分配内存空间时,让每个 Java 应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。
TLAB 只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个 TLAB 用满(分配指针 top 撞上分配极限 end 了),就新申请一个 TLAB。
设置参数:-XX:+UseTLAB 允许在年轻代空间中使用线程本地分配块(TLAB)默认使用
     -XX:-UseTLAB 禁用内存分配
3、对象的分配策略
堆上分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC
但是当对象太大时,需要大量连续内存空间,此时会被分配到老年代,这样可以避免GC时新生代采用复制算法时走出的大量内存复制操作,,避免明明内存有空间进行分配而提前进行垃圾回收
栈上分配
在方法中创建的基本数据类型的对象会被分配到栈上,最好开启逃逸分析
逃逸分析的原理:分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用
比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸
从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度
如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高 JVM 的效率
如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高
4、对象结构

 

对象大小为8的整数倍,方便内存的划分

5、如何定位对象

定位对象的方式有两种:句柄和直接指针

句柄:JVM在堆上划分出一块内存作为句柄池,引用(reference)中存储的对象就是句柄的地址,句柄中包含了对象的实例数据与类型数据真实的地址信息

   优点:引用 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改

直接指针:引用(reference)中存储的对象就是真实地址,Sun HotSpot 是使用直接指针访问方式进行对象访问的

     优点:较比句柄速度要快一些,因为它节省了一次指针定位的时间开销

6、引用的类型

强引用:一般的 Object obj = new Object() ,就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象

软引用:一些有用但是并非必需,用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收(如果这次回收后还是没有足够的

空间,才会抛出内存溢出)

弱引用:一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收

虚引用:幽灵引用,最弱(随时会被回收掉)

7、如何判断对象是否存活

判断对象是否存活的方式有两种:引用计数法和可达性分析

引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1

      这种方法Python中使用,JVM中没有使用

可达性分析:通过以GC Roots对象为起点,向下搜索,看是否存在引用,搜索走过的路被称为引用链,当一个对象到GC Roots没有任何引用链,说明此对象是无用可被回收的

      GC Roots对象:虚拟机栈(栈帧中的本地变量表)中引用的对象

             各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等

             方法区中类静态属性引用的对象
             java 类的引用类型静态变量
             方法区中常量引用的对象,比如:字符串常量池里的引用
             本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
             JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)
             所有被同步锁(synchronized 关键)持有的对象
             JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等
             JVM 实现中的“临时性”对象,跨代引用的对象

Finalize方法:即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与 GCRoots 的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了 finalize),我们可以在 finalize 中去拯救

posted @ 2020-09-29 17:48  CarBlack  阅读(199)  评论(0编辑  收藏  举报