Java工程师进阶之路之JVM系列(四) —— 堆
孙玉超
2020-07-01 16:23:30
0 评论
1004 浏览
0 收藏
0 赞
堆简介
堆是JVM非常重要的一块运行时内存,几乎所有的对象都在堆空间分配内存。自然,堆是线程共享区域。从内存分配的角度看,所有线程共享的Java堆中还可以划分出多个线程私有的分配缓冲区。那么JVM堆具体分为哪几个部分?下图是JVM堆的宏观结构。
JVM堆分为新生代和老年代两个区域,在默认情况下。这两个区域的大小比例为 1:2。新生代中又分为三部分,Eden、Survivor 0、Survivor 1。默认情况下,这三个区域的大小比例为 8:1:1。
对象分配过程
当实例化一个对象时,会将对象首先分配在Eden区,如果Eden区满了,此时会执行一次 Minor GC ,将 Eden 区中存活的(还需要被程序使用的)对象复制一份到 S0 区,然后清除掉 Eden 区所有对象,对象的分代年龄 +1。当 Eden 区再次满的时候,此时会再执行 Minor GC,把 S0 中分代年龄大于 15 的对象转入老年代,将 Eden 和 S0 区的存活对象复制一份到 S1 ,如果 S1 放不下,剩下的对象直接转入老年代,然后清空 Eden 和 S0 区。后面循环这个步骤在 S0 和 S1 之间复制存活对象。
Minor GC 、Major GC 和 Full GC
Minor GC | 新生代的垃圾回收,每当 Eden 区满了会执行 |
Major GC | 老年代的垃圾回收(目前只有 CMS GC 才有单独的老年代回收行为) |
Full GC | 整个堆和方法区的垃圾回收 |
对象一定分配在堆空间吗
代码优化之栈上分配
上面说了几乎所有对象都是在堆空间分配内存,也就是说并不一定。在1.7以前的JDK版本的确是所有对象都在堆空间分配,JDK1.7 及以后,如果对象没有发生逃逸,并且对象的大小满足一定条件。它将会直接在当前方法的栈帧中分配内存,这种特性称为 “逃逸分析”。什么叫发生了逃逸?
1.方法内的对象 被作为参数传递给其他其他方法
2.方法内的对象 作为方法的返回值
当发生这两种情况时,我们称为对象发生了逃逸,除此之外,没有发生逃逸并且对象的大小满足在栈帧分配的条件,这些对象将会直接分配在栈中。事实上程序执行过程中很大一部分对象都是未发生逃逸的。
代码优化之同步省略
如果逃逸分析之后发现对象只能被一个线程访问到,那么代码中显示对该对象进行的同步操作将会被消除。例如下面代码在JVM优化后
public void test(){ Object o =new Object(); synchronized (o){ //TODO } }
将会把synchronized 关键字去掉。
代码优化之标量替换
如果逃逸分析之后发现一个对象只能被一个线程访问,那么 JVM 会把当前对象的实例化过程优化,将该对象按照成员变量分解,相当于在方法内生命局部变量。例如
public static void main(String[] args) { User u = new User("SYC",24); System.out.println(u.username+","+u.age); } 上面的代码在优化之后,会变成 public static void main(String[] args) { int age = 24; String username = "SYC"; System.out.println(username+","+age); }
如果 User 中还有其他实体类的引用 ,那么根据实际情况,仍然可以将该引用再按照它本身的规则进行分解。 这就是标量替换机制,能够大幅度降低对象在堆内存分配消耗的时间。
额外补充
堆空间区域大小的比例
不仅仅是大小的比例,各种参数,包括老年代晋升的分代年龄等都是可调的,上面说的只是默认情况下新生代和老年代等区域大小的比例。事实上,Eden、S0、S1的默认比例是 8:1:1 。但是当你运行一个程序使用 jvisualvm 可视化工具或者命令行命令去观察,你会发现实际上并不是按照这个比例。JVM在运行时会根据当前环境做出自适应,采用合适的比例分配,除非在运行参数里显示指定 -XX:SurvivorRatio = 8 。这样JVM运行时会真正无论什么情况都会把 Eden、S0、S1 的大小比例固定在 8:1:1 。
对象分配过程:TLAB
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-Local Allocation Buffer)。默认设定为占用Eden Space的1%。为什么会有 TLAB?因为 堆 是线程共享数据,多线程环境下可能会出现多个线程申请空间分配对象,这样一来每一次分配对象都得考虑同步,会影响效率。进而诞生了 TLAB分配,JVM 会在 Eden 区划分出一小块区域,为每一个线程创造一个 TLAB 小空间用来分配线程私有对象。由于 TLAB 空间很小,所以大对象无法分配在 TLAB。由此可见,堆一定是线程共享的吗?那么以后就可以回答堆中的 TLAB 空间并不是。