java 对象 栈堆_Java 对象内存分配, 堆还是栈? 这是个问题

Java 对象内存分配, 堆还是栈? 这是个问题

在说 java 的对象分配内存所在位置前, 我们先来看看 C++ 的对象分配是怎样的. C++ 实例化对象的方式有两种:

直接定义对象, 对象被分配在方法栈的本地变量栈上, 生命周期与方法栈一致, 方法退出时对象被自动销毁.

通过 new 关键字在堆上分配对象, 对象要用户手动销毁.#include

usingnamespacestd;

classClassA{

private:

intarg;

public:

ClassA(inta):arg(a){

cout<

}

~ClassA(){

cout<

}

};

intmain(){

ClassAca1(1);// 直接定义对象

ClassA*ca2=newClassA(2);// 使用 new 关键字

return0;

}

输出结果:ClassA(1)

ClassA(2)

~ClassA(1)

直接定义对象的方式会将对象内存分配在栈上, 因此 main 函数退出后会执行 ClassA 的虚构函数, 该对象被回收. 而使用 new 实例化的对象内存分配在堆上, 对象在 main 函数退出后不会执行虚构函数.

C++ 中, 内存可以被分配到栈上或者堆内存中.

那么 java 是否也是这样呢, 如果 java 在必要的时候也是把对象分配到栈上, 从而自动销毁对象, 那必然能减少一些垃圾回收的开销 (java 的垃圾回收需要进行标记整理等一系列耗时操作), 同时也能提高执行效率 (栈上存储的数据有很大的概率会被虚拟机分配至物理机器的高速寄存器中存储). 虽然, 这些细节都是针对 JVM 而言的, 对于开发者而言似乎不太需要关心.

然而, 我还是很好奇.

写一段不怎么靠谱的代码来观察 Java 的输出结果:publicclassClassA{

publicintarg;

publicClassA(intarg){

this.arg=arg;

}

@Override

protectedvoidfinalize()throwsThrowable{

System.out.println("对象即将被销毁:"+this+"; arg ="+arg);

super.finalize();

}

}

publicclassTestCase1{

publicstaticClassAgetClassA(intarg){

ClassAa=newClassA(arg);

System.out.println("getA() 方法内:" + a);returna;

}

publicstaticvoidfoo(){

ClassAa=newClassA(2);

System.out.println("foo() 方法内:" + a);}

publicstaticvoidmain(String[]args){

ClassAclassA=getClassA(1);

System.out.println("main() 方法内:" + classA);foo();

}

}

输出结果:

getA() 方法内: com.rhythm7.A@29453f44

main() 方法内: com.rhythm7.A@29453f44

foo() 方法内: com.rhythm7.A@5cad8086

执行完 getA() 方法后, getA() 方法内实例化的 classA 对象实例 a 被返回并赋值给 main 方法内的 classA. 接着执行 foo() 方法, 方法内部实例化一个 classA 对象, 但只是输出其 HashCode, 没有返回其对象.

结果是两个对象都没有执行 finalize() 方法.

如果我们强制使用 System.gc() 来通知系统进行垃圾回收, 结果如何?publicstaticvoidmain(String[]args){

A a=getA(1);

System.out.println("main() 方法内:" + a);foo();

System.gc();

}

输出结果

getA() 方法内: com.rhythm7.A@29453f44

main() 方法内: com.rhythm7.A@29453f44

foo() 方法内: com.rhythm7.A@5cad8086

对象即将被销毁: com.rhythm7.A@5cad8086; arg = 2

这说明, 需要通知垃圾回收器进行进行垃圾回收才能回收方法 foo() 内实例化的对象. 所以, 可以肯定 foo() 内实例化的对象不会跟随 foo() 方法的出栈而销毁, 也就是 foo() 方法内实例化的局部对象不会是分配在栈上的.

查阅相关资料, 发现 JVM 的确存在一个 "逃逸分析" 的概念.

内容大概如下:

逃逸分析是目前 Java 虚拟机中比较前沿的优化技术, 它并不是直接优化代码的手段, 而是为其他优化手段提供依据的分析技术.

逃逸分析的主要作用就是分析对象作用域.

当一个对象在方法中被定义后, 它可能被外部方法所引用, 例如作为调用参数传递到其他方法中, 这种行为就叫做 方法逃逸. 甚至该对象还可能被外部线程访问到, 例如赋值被类变量或可以在其他线程中访问的实例变量, 称为 线程逃逸.

通过逃逸分析技术可以判断一个对象不会逃逸到方法或者线程之外. 根据这一特点, 就可以让这个对象在栈上分配内存, 对象所占用的内存空间就可以随帧栈出栈而销毁. 在一般应用中, 不会逃逸的局部对象所占比例很大, 如果能使用栈上分配, 那么大量的对象就会随着方法的结束而自动销毁了, 垃圾收集系统的压力就会小很多.

除此之外, 逃逸分析的作用还包括 标量替换 和 同步消除 ;

标量替换 指: 若一个对象被证明不会被外部访问, 并且这个对象可以被拆解成若干个基本类型的形式, 那么当程序真正执行的时候可以不创建这个对象, 而是采用直接创建它的若干个被这个方法所使用到的成员变量来代替, 将对象拆分后, 除了可以让对象的成员变量在栈上分配和读写之外, 还可以为后续进一步的优化手段创造条件.

同步消除 指: 若一个变量被证明不会逃逸出线程, 那么这个变量的读写就肯定不会出现竞争的情况, 那么对这个变量实施的同步措施也就可以消除掉.

说了逃逸分析的这些作用, 那么 Java 虚拟机是否有对对象做逃逸分析呢?

答案是否.

关于逃逸分析的论文在 1999 年就已经发表, 但直到 Sun JDK 1.6 才实现了逃逸分析, 而且直到现在这项优化尚未足够成熟, 仍有很大的改进余地. 不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗. 因为逃逸分析本身就是一个高耗时的过程, 假如分析的结果是没有几个不逃逸的对象, 那么这个分析所花费时候比优化所减少的时间更长, 这是得不偿失的.

所以目前虚拟机只能采用不那么准确, 但时间压力相对较小的算法来完成逃逸分析. 还有一点是, 基于逃逸分析的一些优化手段, 如上面提到的 "栈上分配", 由于 HotSpot 虚拟机目前的实现方式导致栈上分配实现起来比较复杂, 因此在 HotSpot 中暂时还没有做这项优化.

事实上, 在 java 虚拟机中, 有一句话是这么写的:

The heap is the runtime data area from which memory for all class instances and arrays is allocated.

堆是所有的对象实例以及数组分配内存的运行时数据区域.

所以, 忘掉 Java 栈上分配对象内存的想法吧, 至少在目前的 HotSpot 中是不存在的. 也就是说 Java 的对象分配只在堆上.

PS: 如果有需要, 并且确认对程序运行有益, 用户可以使用参数 - XX:+DoEscapeAnalysis 来手动开启逃逸分析, 开启之后可以通过参数 - XX:+PrintEscapeAnalysis 来查看分析结果.

原文 / 个人博客 http://www.unfishable.com/2018/04/30/Java/堆还是栈/#more

来源: https://juejin.im/post/5ae727dd6fb9a07aa54216fc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值