程序员必备:JVM核心知识点总结( 四 )


  • 分配担保
如果年轻代的空间不足,又有新的对象需要分配空间,就需要依赖其他内存(这里是老年代)进行分配担保,对象将直接在老年代创建 。
  • 大对象直接在老年代分配
超出某个阈值大小的对象,将直接在老年代分配,可以通过
-XX:PretenureSizeThreshold 配置这个阈值 。
  • 动态对象年龄判定
有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法 。比如 G1,通过 TargetSurvivorRatio 这个参数,动态更改对象提升的阈值 。
老年代的空间一般比较大,回收的时间更长,当老年代的空间被占满了,将发生老年代垃圾回收 。
目前,被广泛使用的是 G1 垃圾回收器 。G1 的目标是用来干掉 CMS 的,它同样有年轻代和老年代的概念 。不过,G1 把整个堆切成了很多份,把每一份当作一个小目标,部分上目标很容易达成 。
程序员必备:JVM核心知识点总结

文章插图
 
如上图,G1 也是有 Eden 区和 Survivor 区的概念的,只不过它们在内存上不是连续的,而是由一小份一小份组成的 。G1 在进行垃圾回收的时候,将会根据最大停顿时间(MaxGCPauseMillis)设置的值,动态地选取部分小堆区进行垃圾回收 。
G1 的配置非常简单,我们只需要配置三个参数,一般就可以获取优异的性能:
  • MaxGCPauseMillis 设置最大停顿的预定目标,G1 垃圾回收器会自动调整,选取特定的小堆区;
  • G1HeapRegionSize 设置小堆区的大小;
  • InitiatingHeapOccupancyPercent当整个堆内存使用达到一定比例(默认是45%),并发标记阶段就会被启动 。
逃逸分析下面着重讲解一下逃逸分析,这个知识点在面试的时候经常会被问到 。
我们常说的对象,除了基本数据类型,一定是在堆上分配的吗?
答案是否定的,通过逃逸分析,JVM 能够分析出一个新的对象的使用范围,从而决定是否要将这个对象分配到堆上 。逃逸分析现在是 JVM 的默认行为,可以通过参数 -XX:-DoEscapeAnalysis 关掉它 。
那什么样的对象算是逃逸的呢?可以看一下下面的两种典型情况 。
如代码所示,对象被赋值给成员变量或者静态变量,可能被外部使用,变量就发生了逃逸 。
public class EscapeAttr {Object attr;public void test() {attr = new Object();}}再看下面这段代码,对象通过 return 语句返回 。由于程序并不能确定这个对象后续会不会被使用,外部的线程能够访问到这个结果,对象也发生了逃逸 。
public class EscapeReturn {Object attr;public Object test() {Object obj = new Object();return obj;}}那逃逸分析有什么好处呢? 1. 栈上分配
如果一个对象在子程序中被分配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配 。栈分配可以快速地在栈帧上创建和销毁对象,不用再分配到堆空间,可以有效地减少 GC 的压力 。
2. 分离对象或标量替换
但对象结构通常都比较复杂,如何将对象保存在栈上呢?
JIT 可以将对象打散,全部替换为一个个小的局部变量,这个打散的过程,就叫作标量替换(标量就是不能被进一步分割的变量,比如 int、long 等基本类型) 。也就是说,标量替换后的对象,全部变成了局部变量,可以方便地进行栈上分配,而无须改动其他的代码 。
【程序员必备:JVM核心知识点总结】从上面的描述我们可以看到,并不是所有的对象或者数组,都会在堆上分配 。由于JIT的存在,如果发现某些对象没有逃逸出方法,那么就有可能被优化成栈分配 。
3.同步消除
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步 。
注意这是针对 synchronized 来说的,JUC 中的 Lock 并不能被消除 。
要开启同步消除,需要加上-XX:+EliminateLocks参数 。由于这个参数依赖逃逸分析,所以同时要打开-XX:+DoEscapeAnalysis 选项 。
JVM 常见优化参数现在大家用得最多的 Java 版本是 Java 8,如果你的公司比较保守,那么使用较多的垃圾回收器就是 CMS。但 CMS 已经在 Java 14 中被正式废除,随着 ZGC 的诞生和 G1 的稳定,CMS 终将成为过去式 。
Java 9 之后,Java 版本已经进入了快速发布阶段,大约是每半年发布一次,Java 8 和 Java 11 是目前支持的 LTS 版本 。
由于 JVM 一直处在变化之中,所以一些参数的配置并不总是有效的 。有时候你加入一个参数,“感觉上”运行速度加快了,但通过 -XX:+PrintFlagsFinal来查看,却发现这个参数默认就是这样了 。


推荐阅读