Java是否能够栈上分配对象内存? 为何?

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

  • 直接定义对象,对象被分配在方法栈的本地变量栈上,生命周期与方法栈一致,方法退出时对象被自动销毁。
  • 经过new关键字在堆上分配对象,对象要用户手动销毁。
#include <iostream>
using namespace std;

class ClassA {
private:
    int arg;
public:
     ClassA(int a): arg(a) {
         cout << "ClassA(" << arg << ")" << endl;
    }

    ~ClassA(){
         cout << "~ClassA(" << arg << ")" << endl;
    }
};

int main() {
    ClassA ca1(1); //直接定义对象
    ClassA* ca2 = new ClassA(2); //使用new关键字
    return 0;
}
复制代码

输出结果:ios

ClassA(1)
ClassA(2)
~ClassA(1)
复制代码

  直接定义对象的方式会将对象内存分配在栈上,所以main函数退出后会执行ClassA的虚构函数,该对象被回收。而使用new实例化的对象内存分配在堆上,对象在main函数退出后不会执行虚构函数。
  C++中,内存能够被分配到栈上或者堆内存中。
  那么java是否也是这样呢,若是java在必要的时候也是把对象分配到栈上,从而自动销毁对象,那必然能减小一些垃圾回收的开销(java的垃圾回收须要进行标记整理等一系列耗时操做),同时也能提升执行效率(栈上存储的数据有很大的几率会被虚拟机分配至物理机器的高速寄存器中存储)。虽然,这些细节都是针对JVM而言的,对于开发者而言彷佛不太须要关心。
  然而,我仍是很好奇。算法

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

public class ClassA{
     public int arg;
     public ClassA(int arg) {
         this.arg = arg;
     }

     @Override
     protected void finalize() throws Throwable {
         System.out.println("对象即将被销毁: " + this + "; arg = " + arg);
         super.finalize();
     }
 }
 
 
 public class TestCase1 {
     public static ClassA getClassA(int arg) {
         ClassA a = new ClassA(arg);
         System.out.println("getA() 方法内:" + a);
         return a;
     }
 
     public static void foo() {
         ClassA a = new ClassA(2);
         System.out.println("foo() 方法内:" + a);
     }
 
 
     public static void main(String[] args) {
         ClassA classA = getClassA(1);
         System.out.println("main() 方法内:" + classA);
 
         foo();
     }
 
 }
复制代码

输出结果:bash

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()来通知系统进行垃圾回收,结果如何?ide

public static void main(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虚拟机是否有对对象作逃逸分析呢?优化

  答案是否。ui

  关于逃逸分析的论文在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来查看分析结果。

相关文章
相关标签/搜索