16、JVM实战:深入分析JVM中的引用及分配策略

今天和大家分析对象的引用和分配策略:

一、各种引用

1、强引用

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

2、软引用 SoftReference

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

VM参数 -Xms10m -Xmx10m -XX:+PrintGC

*

运行结果

*

例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费 内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都 要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。

3、弱引用 WeakReference

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

参看代码

*

*

注意: 软引用 SoftReference 和弱引用 WeakReference ,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存 中的内容是可以被释放的。 实际运用(WeakHashMap 、 ThreadLocal )

4、虚引用 PhantomReference

幽灵引用,最弱(随时会被回收掉) 垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。

二、对象的分配策略

1、栈上分配

没有逃逸(了解即可)

即方法中的对象没有发生逃逸。

2、逃逸分析的原理: 分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。

比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。 从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。

如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高 JVM 的效率。

逃逸分析代码

*

这段代码在调用的过程中 Myboject 这个对象属于不可逃逸, JVM 可以做栈上分配然后通过开启和关闭 DoEscapeAnalysis 开关观察不同。 开启逃逸分析(JVM 默认开启)

*

查看执行速度

*

关闭逃逸分析

*

查看执行速度

*

测试结果可见,开启逃逸分析对代码的执行性能有很大的影响!那为什么有这个影响?

3、逃逸分析

如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。 采用了逃逸分析后,满足逃逸的对象在栈上分配

*

没有开启逃逸分析,对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行慢

*

代码验证

开启GC 打印日志

-XX:+PrintGC

开启逃逸分析

*

可以看到没有 GC 日志 ,关闭逃逸分析

*

可以看到关闭了逃逸分析, JVM 在频繁的进行垃圾回收( GC ),正是这一块的操作导致性能有较大的差别。

5、对象优先在 Eden 区分配

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

6、大对象直接进入老年代

大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。

大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到 - - 群“朝生夕灭”的“短命大对象”,我们写程序 的时候应注意避免。 在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好 它们。 而当复制对象时,大对象就意味着高额的内存复制开销。 HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。

这样做的目的: 1. 避免大量内存复制 ,2. 避免提前进行垃圾回收,明明内存有空间进行分配。

PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。 -XX:PretenureSizeThreshold=4m

7、长期存活对象进入老年区

HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。 为做到这点,虚拟机给每个对象定义了一个对象年龄(Age) 计数器,存储在对象头中。

*

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1 ,对象在 Survivor 区中每熬过一次 Minor GC ,年龄就增加 1 ,当它的年龄增加到一定程度 ( 并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。 -XX:MaxTenuringThreshold 调整

8、对象年龄动态判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中 相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的 年龄

9、空间分配担保

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

10、本地线程分配缓冲 (TLAB) 上节课已经分析

今天到此结束,下一篇我们分析jvm垃圾回收,敬请期待!