Java对象的定位与分配,对象一定会创建在堆内存里面吗

首先讲下对象的定位

啥叫对象的定位呢?比如:T t = new T(); 定位的意思就是说如何通过引用变量t 去 找到T的实例。咋找呢?通俗点就是:t 的指针不就是指向T对象实例,cpu通过指针里面存放的地址,找到T对象实例的首地址,然后通过对象的大小在内存里面截取呗,比如对象大小是24个字节,那么就从首地址开始截取24个字节呗。没错,是这么干的。但是jvm的执行过程,其实是有两种方式的。

先说结果,再说细节,最后分析比较。java对象的定位主要会分为两种方式:直接和间接。

直接方式,也就是直接指针,t直接指向T对象。如下图:

准确的说,类型数据指针指向的应该是类元对象(C++对象),而不是Class对象。

现在的jvm基本上都是采用这种方式,直接指针。

间接方式,也就是句柄方式:

咱们接触的jvm里面,大多是直接的方式。句柄方式虽然用的少,但是它也有优点,比较下优缺点:直接方式的优点,那就是寻址快,我找一次指针就可以了,但是句柄方式需要找两次,找一次指针之后还要再找一次指针,所以比较慢。但是直接方式也有缺点,比如当GC之下垃圾回收的时候,句柄方式 t 里面存放的指针地址是不用变的,如果用直接指针的方式,每次垃圾回收的时候都要修改t里面的地址值。为啥要修改啊?你拷贝来拷贝去的,不用改地址吗?你挪动地址,那就需要改指针。这样会比较麻烦些。但是句柄方式就不用改吗? 它要改,但是它改的不是t里面值,而是改二元组里面的实例数据指针里面的值。疑问? 那不都是一样的,都要改嘛,其实不一样,句柄方式只改二元组里面的实例数据指针,只改一个,直接方式就不一样了,假如t, m, q等等10个引用指向同一对象,那是不是要改10个啊。

再讲下对象的分配,就是刚刚new出来的对象,是分配到内存的哪个地方

我们知道,jdk默认的分代模型垃圾回收:PS + PO,就是将堆内存分成年轻代和老年代,通常我们说刚产生的对象放在年轻代,伴随这垃圾回收,年轻代里面的对象的年龄到达15之后(CMS默认是6)就会被挪到老年代,但是其实对象的分配不是这么简单的,我们new一个对象其实也不是一定就会在堆内存里面创建一个实例,这里面设计到逃逸分析标量替换

通常我们说,man方法执行,main方法栈帧里面存放一个局部变量t,然后t = new T(), 然后在堆内存T对象就被创建了,其实不是这样的,jvm首先啊尝试着在main方法栈帧里面去创建T对象,如果条件不满足,才会去堆内存里面创建。这个有点颠覆我们的观念了。

为什么要这么做呢?为什么要先尝试着在栈里面去创建对象呢?

不光是java,就是其他语言也一样,它也会将内存分为。栈内存它是被操作系统直接管理的,方法运行完了,直接就弹出这个方法的栈帧,它里面的数据就直接烟消云散了,栈指针直接往下偏移,它不用像堆内存那样需要垃圾回收,所以效率高。既然它效率高,那我们干嘛不尝试着直接在栈里面创建对象呢,我们天天讲着要优化代码,天天嚷嚷着jvm调优对不对,直接优先考虑在栈里面对象,这不也是一种优化吗? 对的,这就回答了上面的问题,为什么要这么做。

通常在java中创建一个对象,大家都认为是在堆中创建。 在jdk6开始有逃逸分析,标量替换等技术,关于在堆中创建对象不再绝对。

也就是要满足逃逸分析标量替换两个条件,才能在栈里面创建对象。

逃逸分析是一种分析技术,分析对象的动态作用域,比如分析一个对象不会逃逸到方法之外或线程之外。通俗点就是分析:指向这个对象的指针有没有逃出对象所在栈帧的范围,如果没有逃出,那么就满足逃逸分析法的条件,或者可以说:我这个方法里面new的对象,别的方法里面有没有用到?在我这个方法外面的地方有没有人用到?如果有,那么就不满足条件,那就只能在堆内存里面创建了。试想一下,如果直接放在栈里面,你这个方法都出栈了,数据都干掉了,别的地方指针还指向它,那就成空指针了。所以只有少部分才能在栈上分配。

标量替换是什么呢,阅读《深入理解Java虚拟机》的过程中,原文是这么解释的:但即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。这是原文的解释,我们可以简单点说,就是即时编译器会对方法中的对象动态作用域进行分析,如果这个对象只是在该方法中使用,可以认为这个对象是没有逃逸出该方法的,当遇到这种情况时,JVM会针对这个对象做一些栈自动分配的事情。那么也就不需要占用堆内存也不需要被垃圾回收器管理,当栈帧压出(方法执行完返回了)该对象的内存会自动释放。

标量可以理解成一种不可分解的变量,如java内部的基本数据类型、引用类型等。 与之对应的聚合量是可以被拆解的,如对象。如果在条件允许的情况下,只需要在栈中使用标量来简化整个逻辑,甚至不需要创建这个对象的实例。

当通过逃逸分析一个对象只会作用于方法内部,虚拟机可以通过使用标量替换来进行优化。这些优化的手段,不仅可以减少运行时的堆内存消耗,也能够有效减少GC的次数,对于整体性能的提升是很显著的。下面用代码测试下:

/**
 * @author wangyong
 */
public class Person {
    public int age;
    public String name;
    public Person(int age, String name){
        this.age = age;
        this.name = name;
    }
}
/**
 * @author wangyong
 */
public class EscapeAnalysis {
    public Person p;

    /**
     * 发生逃逸,对象被返回到方法作用域以外,被方法外部,线程外部都可以访问
     */
    public void escape(){
        p = new Person(26, "TomCoding escape");
    }

    /**
     * 不会逃逸,对象在方法内部
     * @return
     */
    public String noEscape(){
        Person person = new Person(26, "TomCoding noEscape");
        return person.name;
    }
}
/**
 * @author wangyong
 */
public class EscapeAnalysisRunTest {
    public static void main( String[] args ) {
        // noEscape()不会发生逃逸,分别测试关闭标量替换优化 和 开启标量替换优化
        // testEliminateAllocationsWithNoEscape();

        // escape()发生逃逸,分别测试关闭标量替换优化 和 开启标量替换优化
        // testEliminateAllocationsWithEscape();
    }
    private static void testEliminateAllocationsWithNoEscape() {
        int n = 100000000;
        long start = System.currentTimeMillis();
        EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
        for (int i = 0; i < n; i++) {
            escapeAnalysis.noEscape();
        }
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
    private static void testEliminateAllocationsWithEscape() {
        int n = 100000000;
        long start = System.currentTimeMillis();
        EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
        for (int i = 0; i < n; i++) {
            escapeAnalysis.escape();
        }
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
}

接下来我们通过对noEscape()方法进行测试,测试是在jdk8中运行(注jdk8默认是开启逃逸分析,标量替换技术的)。

主要测试两种场景:不使用标量替换 和 使用标量替换。

1.0情形:不逃逸,main方法里面调用testEliminateAllocationsWithNoEscape()方法。

-XX:+PrintGC 打印gc日志

-XX:-EliminateAllocations 关闭标量替换优化

1.1情形:不逃逸,main方法里面调用testEliminateAllocationsWithNoEscape()方法。

-XX:+PrintGC 打印gc日志

-XX:+EliminateAllocations 启用标量替换优化

这个毫秒9毫秒,GC只有0次,首先不发生逃逸,那标量替换是怎么优化的呢?

当通过逃逸分析一个对象只会作用于方法内部,虚拟机可以通过使用标量替换来进行优化。

比如上述noEscape()方法中person对象只会在方法内部,通过标量替换技术得到如下伪代码:

/**
 * 不会逃逸,对象在方法内部
 * @return
 */
public String noEscape(){
    int age = 26;
    String name = "TomCoding noEscape";
    return name;
}

此时只需要在栈中使用标量来简化整个逻辑,甚至不需要创建这个对象的实例。jvm进行了优化,优化之后就代替了下面的代码:这个就叫标量替换技术

/**
 * 不会逃逸,对象在方法内部
 * @return
 */
public String noEscape(){
    Person person = new Person(26, "TomCoding noEscape");
    return person.name;
}

1.2情形:逃逸,main方法里面调用testEliminateAllocationsWithEscape()方法。

-XX:+PrintGC 打印gc日志

-XX:+EliminateAllocations 启用标量替换优化

1.3情形:逃逸,main方法里面调用testEliminateAllocationsWithEscape()方法。

-XX:+PrintGC 打印gc日志

-XX:-EliminateAllocations 关闭标量替换优化

总结:

标量替换只是利用逃逸分析其中的一种优化措施, 它首先是不能发生逃逸,发生逃逸就不会用了。

可以看到通过逃逸分析与标量替换技术有效的减少了gc次数(减少了对象在堆中创建的数量)。

实际编码过程中避免对象逃逸情况是一种理想的情况。可以形成一种编码意识,尽量去减少对象逃逸

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值