JVM中的对象和引用详解

1. 对象的创建过程

在这里插入图片描述
其中的步骤是:当虚拟机碰到一条new指令时,先检查对象是否被加载,如果未被加载,就先将class加载到运行时数据区;然后虚拟机为对象分配内存,分配内存有两种方式:

  • 内存空间如果不是碎片化的,内存中已经使用的和未使用的内存空间之间就有个指示器指针,分配内存就是挪动指示器的过程,这种方式称为指针碰撞(简单高效);

  • 内存空间如果是碎片化的,虚拟机就会维护一张记录哪些空间是空闲可用的,分配内存的过程就是更新这张列表的过程,这种方式称之为空闲列表(复杂)。
    分配内存的方式由垃圾回收器是否做内存整理来决定。

分配内存中需要考虑的重要一点就是并发安全,对象的创建时很频繁的,对内存空间的分配管理需要考虑并发安全的问题,解决这个问题有两个方案:

  • CAS机制:利用CAS机制加上失败重试的方式保证内存分配操作的原子性;
  • 本地线程分配缓冲(TLAB):在每个线程初始化时,JVM会给他分配一块堆空间中的内存,这样每个线程分配内存就在本地进行,如果内存不够就继续向Eden区申请,这样做效率高而且不会引发并发安全问题。
    参数:XX:+UseTLAB 允许在Eden区申请TLAB,默认开启

内存分配完,就是对内存空间进行初始化,这是一个赋零值的过程,之后虚拟机要对对象进行设置
在这里插入图片描述
这样子,一个对象就产生了,虽然所有值都为“零”,但是咱们秃头将军们可以来“玩”她们了,咱们的new指令就可以根据构造方法来对他们进行初始化了。

2. 对象的访问

对象创建出来之后,咱们就可以通过栈上的 reference 数据来操作堆上的具体对象了,访问一般有两种方式:句柄和直接指针

  1. 句柄:java堆中有一块区域叫做句柄池,句柄池存储的是对象的句柄信息,其中包括对象实例数据和类型数据各自的具体地址信息,reference存储的就是对象的句柄地址。这种方式的好处就是即使对象在堆中频繁地移动,改变的也只是句柄中的实例数据指针,而对reference数据不影响;
  2. 直接指针: 这种方式reference中存储的就是对象的内存地址;
  3. 对比:这两种方式各有优缺点,其中直接指针优点就是访问速度快,小缺点是对象实例数据频繁移动,维护这个直接内存地址也需要开销;句柄的优点就是稳定,但是要在堆中创建维护一个句柄池,这样访问性能可能会稍差。

3. 判断对象的存活

2.1 引用计数法: 在对象中添加一个引用计数器,每当有地方引用他就+1,引用失效就-1。但是遇到循环引用问题时需要引入额外的机制来处理,影响效率,python目前用的就是这种;
2.2 可达性分析算法: 以GCRoot为起点,开始往下搜索,这个过程中的路径成为引用链,当一个对象没有和引用链相连时,他就被判定是垃圾。
可作为GCRoot的对象(主要是前四种)
2.2.1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等;
2.2.2. 方法区中类静态属性引用的对象;java 类的引用类型静态变量;
2.2.3. 方法区中常量引用的对象;比如:字符串常量池里的引用;
2.2.4. 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象;

2.2.5. JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器);
2.2.6. 所有被同步锁(synchronized 关键)持有的对象;
2.2.7. JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等;
2.2.8. JVM 实现中的“临时性”对象,跨代引用的对象。

4. finalize方法

在根可达算法判定为垃圾的对象,再经过一次标记筛选之后就会被GC回收,而在这之间,如果对这个对象执行了finalize()对他进行了一次"起死回生",GC就不会回收它,但是finalize()只能拯救它一次,也就说如果再将他指向空,再对他执行finalize(),它依旧会被GC回收。这个方法不可靠,不建议使用

5. 引用

  • 强引用:就普通的Object obj = new Object();就是强引用,只要引用在,GC就不会回收;
  • 软引用(SoftReference): 内存即将溢出时,软引用对象会被回收,一般是用在一些占用内存空间且使用次数少的对象;
  • 弱引用(WeakReference):下一次GC时,弱引用对象会被回收,和软引用一样用在内存资源紧张的情况下以及创建不是很重要的数据缓存;
  • 虚引用(PhantomReference):随时会被回收,垃圾回收的时候收到一个通知,主要作用就是为了监控垃圾回收器是否正常工作

6. 对象的分配策略

6.1. 基本所有的对象都在堆上分配:这里说的是"基本",也就是说还有例外,例外就是在栈上分配,这种分配方式也称为逃逸

6.2. 逃逸分析:调用参数传递到其他方法中,这种称之为方法逃逸,甚至还有可能被外部线程访问到,这种称之为线程逃逸
如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高 JVM 的效率。

6.3. 对象优先在 Eden 区分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC

6.4. 大对象直接进入老年代
大对象就是指需要大量连续内存空间的 Java 对象,他们会占用大量连续的内存空间,会导致提前触GC,而且在复制对象时会造成很大的内存开销。所以
HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。

6.5. 长期存活对象进入老年区
对象头中MarkWord记录了对象GC分代年龄,对象在eden区产生年龄为0,经过一次minorGC进入survivor区,年龄变成1,之后在survivor区每经过一次minorGC年龄就增加1,当到达一定年龄就进入老念代。并发垃圾回收器默认是15,CMS默认是6,参数-XX:MaxTenuringThreshold可以调整。

6.6. 对象年龄动态判定
虚拟机并不是一定要对象年龄到达一定大小才进入老年代,当survivor区所有相同年龄的对象大小总和大于survivor区一半时,年龄大于或等于该年龄的对象就直接进入老年代。

6.7. 空间分配担保
在发生minorGC之前,虚拟机会检查老年代连续可用空间是否大于新生代总空间大小,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次minorGC,尽管这次minorGC是有风险的,如果担保失败则会进行一次FullGC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值