您现在的位置是:首页 > 文章 > Java工程师进阶之路之JVM系列(四) —— 堆 网站文章

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 中还有其他实体类的引用 ,那么根据实际情况,仍然可以将该引用再按照它本身的规则进行分解。 这就是标量替换机制,能够大幅度降低对象在堆内存分配消耗的时间。


额外补充


尚硅谷宋红康老师的视频中在堆小结提出,其实Oracle 的 HotSpot 系列虚拟机,由于逃逸分析机制并不成熟,并没有实现逃逸分析之对象栈上分配。因为逃逸分析本身就是一个消耗性能的操作,假如逃逸分析之后发现该对象并不是逃逸的。那么,这将会很消耗性能。就是说目前对象并没有实现会在栈上分配。

但是在开启和关闭逃逸分析机制写 demo 时,-XX:+DoEscapeAnalysis 参数的使用 , 会发现局部方法内的对象,经过逃逸分析之后代码执行所花费的时间明显小于关闭逃逸分析机制时代码执行时间。值得注意的是,这是由于标量替换机制导致的结果,并不是因为栈上分配,而是因为 JVM 将对象分解为成员变量,相当于局部变量存在在栈帧的局部变量表。

所以目前看来,Java 对象一定是在堆中分配吗?答案是肯定的。至少在目前的 JDK 版本中,并未实现将对象在栈上分配。


堆空间区域大小的比例


不仅仅是大小的比例,各种参数,包括老年代晋升的分代年龄等都是可调的,上面说的只是默认情况下新生代和老年代等区域大小的比例。事实上,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 空间并不是。


转载请注明出处:转载请注明出处

上一篇 : Nginx跨域无法访问字体图标文件 下一篇 : 一道难倒大部分程序员的String面试题

留言评论

所有回复

暮色妖娆丶

96年草根站长,2019年7月接触互联网踏入Java开发岗位,喜欢前后端技术。对技术有强烈的渴望,2019年11月正式上线自己的个人博客