细说jvm(三)、对象创建的内存分配

·  阅读 847

之前的文章

1、细说jvm(一)、jvm运行时的数据区域

2、细说jvm(二)、java对象创建过程

对象创建的内存分配

在对象创建的时候给对象分配内存总共是可能有如下的几种可能:

(1)将对象分配在栈上 (2)使用TLAB (3)分配在eden

我们一点一点的来说下,每一点展开都是个知识点

栈上分配

这里需要先说的一个是逃逸分析,在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。而hotspot能够在动态加载方法的时候对代码进行逃逸分析,如果发现一个新对象的引用仅仅是在这个方法的范围内,那么这个对象的分配区域就会仅仅在栈上。为了证明我说的是对的,我需要用一段代码来证明一下,在代码开始之前,我先简单介绍几个会用到的jvm的参数,不然你可能会比较懵逼。

  • -Xss 这个参数是指明栈空间的大小,我们这里为了让一个栈有足够的大小,因此给2m的大小
  • -Xms 这个是堆的初始化大小
  • -Xmx 这个是堆的最大大小
  • -XX:+PrintGCDetails 开启打印垃圾回收日志
  • -XX:+UseConcMarkSweepGC 使用CMS垃圾回收器
  • -XX:+PrintGCDateStamps 打印GC发生的时间,用的humanbeing的方式
  • -XX:-DoEscapeAnalysis 关闭逃逸分析
我们仔细观察上面的参数,发现有的参数前面带着+或者-,jvm的参数有两类,一类是需要设置具体的值的,另外一类只是单纯的开启和关闭的,
单纯的开启和关闭就用的是+和-,+指的是开启,-指的是关闭,设置值的在后面跟上值就行了
复制代码

上面是我使用的参数,具体的设置如下:

-Xss2048k
-Xms50m
-Xmx50m
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDateStamps
-XX:-DoEscapeAnalysis
复制代码

代码如下:

public static void main(String[] args) throws IOException {
        while (true) {
            new MyEntity(1, "a");
        }
}

// MyEntity类如下
public class MyEntity {

    private Integer id;

    private String name;

    public MyEntity(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
}
复制代码

然后我们开始跑main方法,如我们想的,GC会疯狂运行,因为我们关闭了逃逸分析,GC日志如下: 我们先不在本章教你读GC日志(这是个很长的话题),这里贴出来的目的只是证明关闭逃逸分析的影响而已,我们再从另外一方面证明堆内存中有很大量的MyEntity对象,我们打开终端,windows是打开cmd,输入jvisualVM,它的界面如下:

双击我画红框的进程,这个进程就是我们正在跑着的java程序,打开之后界面如下: 按顺序选择我画红框的按钮,之后你可以看到的如下图: 可以看到MyEntity实例数量在堆内存中非常多,其实它的数量应该是一会儿多一会儿少,因为在每次GC的时候总会有很多被回收掉(GC的细节我们下篇文章开始说)。

接下来我们证明下,逃逸分析开启之后,对象是有可能被分配到栈上的。

参数如下:注意仅仅修改最后一项的-为+号

-Xss2048k
-Xms50m
-Xmx50m
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDateStamps
-XX:+DoEscapeAnalysis
复制代码

代码和上面一样,但是这次跑起来根本没有GC日志输出,我的IDEA控制台干干净净一片: 同时,继续看jvisualVM的内存监控如下,你会发现内存中根本没有MyEntity的实例

TLAB分配

TLAB全称是Thread Local Allocate Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。 由于对象一般会分配在堆上,而堆是所有线程共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(jvm采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。

TLAB本身占用Eden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

由于TLAB空间一般不会很大,因此大对象无法在TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。如果想要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定一个TLAB的大小。 -XX:+PrintTLAB可以跟踪TLAB的使用情况。

一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。

为什么说不推荐修改TLAB相关的东西呢?这是因为TLAB的优化是极其难控制的,在不同的业务场景下对象创建的情况差别会非常大,
因此我们一般不会优化这里的参数,只是使用默认的参数。
复制代码
分配在eden区域

当jvm判断不能使用前两种分配方式的时候就会触发这种分配方式,在这种情况下,会有两种选择:

1、指针碰撞:所谓的连续内存是指Java堆中的内存是绝对规整的,用过的内存在一边,空闲的内存在另一边。中间有个指针作为分界点,
这时如果要分配新内存,只要指针向空闲的内存一方移动一下就可以了。这种分配内存的方式就叫指针碰撞。

2、空闲列表:如果Java堆中的内存并不是完整的,也就是不是连续的。这时使用的内存和空闲的内存没有任何规则,无法用指针碰撞的方
式来分配内存。这时虚拟机只能采取其它办法来标识出哪些内存是使用的,哪些内存是空闲的,所以虚拟机就要维护一个列表,用来存储哪些
内存是空闲的,分配内存时,只要从列表中划分一块区域存储对象实例,并更新列表上的记录就可以了。这种方式就叫空闲列表
复制代码

具体使用的是哪种,取决于使用的垃圾回收器使用的哪种算法,一般来说,我们的hotspot在使用CMS和G1垃圾回收器的时候都是用的第二种。

其实分配在eden这种说法并不绝对,因为当一个对象非常大大到了eden都放不下的时候,这时候还要保证这个分配一定成功,这时候就会让这个对象进入老年代,这个是jvm的内存分配担保机制。

对象创建时候的内存分配就说这么多,下一篇我们开始说垃圾回收

分类:
阅读
标签: