JVM(三)对象及引用
一、对象创建过程
-
检查加载
检查类是否以及被加载(是否存在对应类型的class)。
-
分配内存
-
内存划分方式
-
指针碰撞
如果堆中的内存是绝对规整的,那么就会按照对象的大小直接进行内存的划分,此情况称为指针碰撞。
-
空闲列表
如果堆中的内存是碎片化的、不规整的,那么JVM就不能进行指针碰撞,JVM需要维护一个列表,记录哪些内存地址被使用,哪些没有被使用,在给对象分配内存的时候只需要找到和对象一致大小的连续内存地址进行划分。此情况称为空闲列表。
Tip:至于什么时候采用那种方案由JVM来决定。如果是Serial、ParNew 等带有压缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。
-
-
解决并发安全
在给对象划分内存的时候同时还需要考虑的问题是创建对象在虚拟机中时非常的频繁的那么就会存在并发的相关问题,当给A对象划分的时候,B也在划分这个内存那么就存在了问题,在JVM中有以下两种解决办法:
-
CAS加失败重试
当在进行内存划分时,对当前的内存地址进值行一个保存(当前地址是没有其他数据),之后进行一些数据的预处理后再取当前内存地址的值,与之前取到的old值进行比较如果相等则直接分配给当前对象,如果不一致那么就重新进行一轮(当前如果当前内存地址已经存在了对象就会去找其他的内存地址。)
-
TLAB(分配缓存)
这个是比较简单粗暴的,预先给每个线程预先在堆中划分一块私有内存给当前的线程使用,那么每个线程就会拥有一块独有的内存,就可以直接给对象分配。当内存被使用光之后,可以重新申请一块内存。此方式效率是杠杠的。
-XX:+UseTLAB
:允许在年轻代空间中使用线程本地分配块(TLAB)。默认情况下启用此选项。要禁用TLAB,请指定-XX:-UseTLAB
。
-
-
-
内存空间初始化
对对象中的属性进行初始化(默认值),这一步保证了对象的实例属性可以不用赋值就可以直接使用。
-
设置
接下来话就是对对象进行必要的一些设置,比如这个对象是属于哪个类的、对象的哈希码、对象的GC分代年龄等等,这些信息都是存储在对象头中的。
在Hotspot VM中,对象在内存中的布局分为三块::对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头主要分为两个部分,第一个部分主要存储对象自身的运行时数据。如:哈希码、GC分代年龄、锁状态标识等等。
对象头的另一个部分主要指向这个对象是属于哪个类(class)的。如果对象是数组的话那么还会记录数组的长度信息。
实例数据则是对象中的真实的有效的数据,各个类型字段的内容。
对齐填充的话不是一定存在的因为一个对象的大小必须是8字节或者8的倍数,如果这个对象不符合这个条件的话那么就会进行补充使得对象可以符合这个条件。
-
对象初始化
到现在为止在JVM视角来看的话一个新的对象已经产生了,但是从代码上来看的话,对象才刚刚开始创建,所以接下来的话就是调用构造方法,这样一个对象才被真正的创建了出来。
二、对象定位
一般来说我们都是在栈中根据对象的引用(reference)来操作对象的,那虚拟机到底是怎么定位到对象的?在
-
句柄
使用句柄的话,会在堆中划出一块句柄池内存区域,而引用就是指向句柄池中的句柄,每一个句柄包括了对象实例数据与类型的地址信息。这样做的话就会比较稳定,当对象被移动的时候就只需要更改句柄的指针即可,而引用就还是指向这个句柄。
-
直接指针(Hotspot)
直接指针的就是引用直接指向实例对象,这样做的话就是一个字,快!它比句柄的话减少了一次指针的定位开销。
三、什么是垃圾
在堆中存放的几乎都是实例对象,随着程序的运行,堆中的对象会越来越多那么垃圾回收器就需要对一些没有用的对象(垃圾)进行回收以确保堆的内存可以被极大的利用。
-
手动内存
在C/C++中的话就需要手动进行申请内存,之后还要手动回收内存。而手动回收内存的话就会存在两个问题:
(1)、忘记回收
(2)、多次回收
-
自动内存
自动回收的就比较爽,写代码就完全不用自己申请,回收内存啥的了。
四、对象存活的判断
既然是自动回收的话,那么JVM就需要判断什么可以回收什么不可回收。
-
引用计数法
在每个对象中添加一个引用计数器,每有一个地方引用那么就会加一,当每有一个引用被断开就减一,如果这个计数器为0的时候表示这个对象可以被回收了。
但是有一个情况会导致对象无法永远无法被回收,当两个对象互相引用的时候那么这两个对象的计数器都会用于存在1,那么就需要其他的手段来解决这个办法。
Python的垃圾回收就是采用的这个方案+其他手端来做的。
-
可达性分析(根可达)
这块面试问的还是比较多的。
此方法就是Hotspot VM目前所采用的,这个算法的话就是采用“GC Root”来确定对象的头目,根据这个头目来向下搜索,搜索过的路径叫做引用链,只要对象在不在引用链上那么就是没用的垃圾,可以被回收。
GC Root主要包块以下几种:
- 虚拟机栈里的局部变量表所引用的对象;各个现场被调用方法堆栈中使用到的参数、局部变量、临时变量等。
- 方法区里的静态属性所引用的对象。
- 方法区例的常量所引用的对象。
- 本地方法栈Native方法(JNI)所引用的对象。
- JVM 的内部引用(class 对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。
- 所有被同步锁(synchronized 关键)持有的对象。
- JVM 内部的JMXBean、JVMTI 中注册的回调、本地代码缓存等。
- JVM 实现中的“临时性”对象,跨代引用的对象
五、class回收条件
class回收的话条件比较苛刻,必须要满足以下几个条件,而且还不是肯定被回收,会被设置的JVM参数所影响。
1、这个类没有任何实例。
2、加载这个类的类加载器(ClassLoader)被回收了。
3、这个类没有被任何地方所引用,反射也没法访问这个类的那种。
4、-Xnoclassgc
参数
六、Finalize方法
可以拯救被回收的对象,但是只能拯救一次而且优先级比较低。
目前很多地方不推荐使用,不多介绍。
七、各种引用
-
强引用
String str = new String("abc")
这个代码里面的 "=",就属于强引用,就不会被GC回收掉。 -
软引用(SoftReference)
当系统出现OOM异常的时候就会回收掉软引用的对象,以确保有足够的内存来运行程序。一般来说缓存会使用。
-
弱引用(WeakReference)
当发生GC的时候就会被清除掉,比软还不行。基本上也是缓存会采用。
-
虚引用(PhantomReference)
幽灵引用,最弱(随时会被回收掉)
垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。
八、对象分配原则
-
栈上分配
逃逸分析:其实不是所有对象都是分配在堆中的,比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。
从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。
如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高JVM 的效率。 -
堆上分配
堆在上一个文章中就说了,在堆中是分代的。
-
对象优先Eden区分配
一般来说所有创建的对象都会被优先分配在Eden区。
-
大对象直接老年代分配
如果对象太大的话就被直接分配到老年代区,比如一个很长的字符串或者数量很多的数组。
在写代码的时候就需要避免这些大的对象,如果在复制一个大对象,拿么开销就非常大。
Hotspot 虚拟机提供了
-XX:PretenureSizeThreshold
参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden 区及两个Survivor区之间来回复制,产生大量的内存复制操作。 -
长期对象存活老年代分配
Hotspot VM中大多数的收集器都采用了分代收集来管理堆内存,在分配内存的时候就需要决定哪些对象放在新生代哪些放在老年代。
在前面就说到每个对象的对象头中就存放了GC分代年龄,当Eden经过第一次Minor GC后仍然存活的对象就会被移动到Survivor区域,并且GC分代年龄加一,对象在Survivor区域每经过一次Minor GC都会加一,当它的年龄增加到一定程度(并发的垃圾回收器默认为15),CMS 是6 时,就会被晋升到老年代中。
-XX:MaxTenuringThreshold
调整
-
对象动态对象年龄判断
当对象被移动到Survivor区的时候,当Survivor区中的所有对象大小超过Survivor区空间的一半的话就被直接被移动到老年代区。
-
空间分配担保
在发生Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC 是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次Full GC。
-