BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / java / #26423同步于 2013/7/21
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Java机器人发帖

为什么Java/OC不支持在栈上创建对象?

never115
2013/7/21镜像同步12 回复
为什么java和OC都不支持像C++一样在栈上创建对象呢?比如string s; 而都要用new/alloc的方式在堆上创建? 可能是语言设计层面考虑的问题,不过我觉得支持在栈上的对象,还是很方便的啊。对于java/OC不支持的原因,个人猜想有以下几种: 1、java天然没有析构函数,设计之初就没想过在栈上创建对象 2、java的栈大小有限 3、java或OC支持栈上创建对象,则相应的异常处理机制也要实现栈展开,编译器实现成本较高 4、历史原因 一般的书籍和资料,也不会涉及这方面。 不知道大牛们有没有考虑过这方面的问题的呢?
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
nuanyangyang机器人#1 · 2013/7/21
栈上,每个帧的生存周期是函数调用开始到函数返回。但是Java中,对象的生存周期是从创建对象(new)到没有任何引用指向它。换句话说,如果在函数内创建了对象,同时将这个对象的引用传递给其它对象,那么即使这个函数返回了,这个对象仍然活着,但这个函数的帧已经消失了。因为生存周期不同,所以不可能把对象分配在栈上。 Java还要保证所有的引用都必须有效。如果对象在堆上,还可以有垃圾回收保证只要有引用,对象就活着。如果对象建立在栈上,那么只要它所在的帧毁掉了,对象也就消失了。如果这时候还有别的引用指向这个对象,那么那个引用就无效了,因为对象已经消失了。所以,为了保证引用的有效性,也不能在栈上创建对象。
nuanyangyang机器人#2 · 2013/7/21
一种折中是:允许在栈上创建对象,但要求所有能够引用这个对象的变量都必须在栈上,而且不能传递出去。这样,所有这些引用的生存周期都严格地位于这个对象的生存周期内,这样也可以保证安全。 (.NET应该是这样设计struct和byref引用参数的,我不是.NET专家,不太了解) 比如: void foo() { A a = new_on_stack A(); // 在栈上创建对象 a.x = ...; bar(a); print(a.y); return; // a的生存期到此为止 } void bar(byref A a) { // 参数是byref,实际上是一个引用 // 知道a在栈上,而且保证a在bar的调用者(或者更深)的帧内。 // 不管在哪里,肯定保证在bar返回之前,参数a一定是有效的。 a.y = a.x + 1; } 这样设计可以允许在栈上分配对象,但是会引来更多的问题。 比如:这样的对象和在堆中的对象,要用不同的方式引用。于是 1. 上面代码中的A能不能在堆上分配?是否既能在栈上分配,又能在堆上分配? 2. 如果一个函数接受A作为参数(如bar),那么它能够接受栈上的A,还是堆上的A,还是都能接受?也就是说: void bar(byref A a) 和 void bar(A a) 是不是一种函数? 如何设计一种函数,能够接受各种不同位置的A? 起码如果能接受堆上的A,就必须要让垃圾回收器知道这个函数还引用着这个参数,不要把它毁掉。 如果一个函数传入了一个栈上的A,你能不能把它再传个一个接受堆上的A的函数? 具体地说: void baz(A a) { ... } void bar(byref A a) { baz(a); //这样到底行不行? } 所以,在堆上分配对象会引来各种问题。如果限制不能在堆上分配,可以避免这些问题,当然,你不总是希望这样。
nuanyangyang机器人#3 · 2013/7/21
另一方面,现代的编译器都会对局部变量进行优化。一种常用的策略是SSA(Static Single Assignment)。思想是要求所有的局部变量都只能在一个地方赋值,类似于Java的final变量。这种表示方法很容易看出变量之间的依赖关系,也很容易找出未使用的变量。而且还可以优化,使得变量不一定在内存里(也就是说,甚至不一定在栈上分配),而是直接保存在寄存器里。 但是,如果一个对象被“分配在栈上”,那么就强迫它占用内存空间,这样对于这个对象的运算就不能用一般SSA表示,而是每次读写都要看成一次“读”或“写”操作,强迫SSA表示法将其看作“存储器”和“读写动作”而不是“量”和“运算关系”,这样很多优化就无法进行。起码这样的对象是无法放到寄存器中的。这样,首先速度会受到影响。 如果不允许有引用或者指针指向栈上的内容,那么所有的程序都可以用纯粹的SSA来表示;但是一旦允许,就必须加上alloca这个机制(alloca类似于malloc,就是在栈上分配一定大小的内存,返回其指针;但是在函数返回的时候就解除分配),而且对alloca出来的内存也要像所有的指针、引用操作一样进行内存读写。紧接着一个问题就是如何保证指针的有效性(上文已说)。另一个问题就是内存读写在多处理机环境下的行为,比如:一开始a=b=0。一个线程先写了a=1,后写了b=2;另一个线程如果看到了b=2,是否它一定也看到了a=1?甚至第一个线程写了a和b之后,第二个线程到底能不能看到a和b的值变了?如果不一定,那么可以更灵活地优化性能,但是用什么来保证线程间的同步?如果一定要能看到,那么线程间可以可靠地传递数据,但是会不会引来性能上的损失(如CPU缓存之间的同步)?(参考Java的volatile)
tonyjansan机器人#4 · 2013/7/21
CLI风格的栈对象不是真正在运行栈中做分配的~而是通过gcnew放入托管堆中的(这也就是为什么能实现跨平台的内存管理机制,否则WP上的C++也就不再具有安全性了) 举例来说: // MyClass a; MyClass b = gcnew MyClass; 这两条CLI语句在MyClass没有定制析构器的情况下编译出的IL代码是完全相同的!(第一条代码看上去像是从栈中分配出的空间,但实际则是由托管堆来进行管理的)。 另外指出的生命周期问题是要看你是否做了析构器实现~例如: // ref class MyClass { public: MyClass(){} // ~MyClass() {} static MyClass^ getInstance() { MyClass ret; return %ret; } } 这里,如果不定义析构器的话,MyClass::getInstance()所返回的对象是可以指向可用内存的!但如果定义了析构器的话,内存则会被Dispose掉! 【 在 nuanyangyang 的大作中提到: 】 : 一种折中是:允许在栈上创建对象,但要求所有能够引用这个对象的变量都必须在栈上,而且不能传递出去。这样,所有这些引用的生存周期都严格地位于这个对象的生存周期内,这样也可以保证安全。 : (.NET应该是这样设计struct和byref引用参数的,我不是.NET专家,不太了解) : 比如: : ...................
nuanyangyang机器人#5 · 2013/7/21
那么struct呢? 【 在 tonyjansan 的大作中提到: 】 : CLI风格的栈对象不是真正在运行栈中做分配的~而是通过gcnew放入托管堆中的(这也就是为什么能实现跨平台的内存管理机制,否则WP上的C++也就不再具有安全性了) : 举例来说: : [code=c] : ...................
tonyjansan机器人#6 · 2013/7/23
struct在CX中要看定义形式:value struct还是ref struct(枚举类似)。 【 在 nuanyangyang 的大作中提到: 】 : 那么struct呢? :
never115机器人#7 · 2013/8/4
感谢大家的讨论。 但实际上,我想问的是为什么设计如此,而不是有了这样的设计之后,为什么不能创建。 “因为生存周期不同,所以不可能把对象分配在栈上。” 生命周期不同,是因为语言设计如此。 C/C++也有局部变量,把局部变量传出去,也是会有错误的。但那是程序员的事。让不让创建,则是语言的本意。 java如果允许栈上分配对象,如果传出去了,就认为是空引用,不也可以么? 就好比C++为什么不支持反射。这是一个设计思想的问题,而不是一个实现上的问题。 我想知道java的设计者,基于何种考虑,不支持在栈上创建对象? 【 在 nuanyangyang 的大作中提到: 】 : 栈上,每个帧的生存周期是函数调用开始到函数返回。但是Java中,对象的生存周期是从创建对象(new)到没有任何引用指向它。换句话说,如果在函数内创建了对象,同时将这个对象的引用传递给其它对象,那么即使这个函数返回了,这个对象仍然活着,但这个函数的帧已经消失了。因为生存周期不同,所以不可能把对象分配在栈上。 : Java还要保证所有的引用都必须有效。如果对象在堆上,还可以有垃圾回收保证只要有引用,对象就活着。如果对象建立在栈上,那么只要它所在的帧毁掉了,对象也就消失了。如果这时候还有别的引用指向这个对象,那么那个引用就无效了,因为对象已经消失了。所以,为了保证引用的有效性,也不能在栈上创建对象。 :
nuanyangyang机器人#8 · 2013/8/4
【 在 never115 的大作中提到: 】 : 如果传出去了,就认为是空引用 这个为什么可以?我觉得不可以啊。
nuanyangyang机器人#9 · 2013/8/4
【 在 never115 的大作中提到: 】 : 感谢大家的讨论。 : 但实际上,我想问的是为什么设计如此,而不是有了这样的设计之后,为什么不能创建。 : “因为生存周期不同,所以不可能把对象分配在栈上。” : ................... 生命周期,是语言设计的一部分;引用安全,也是语言设计的一部分。 但明显两者是冲突的:作用域是明确的生命周期;而引用安全,则必须延长这个生命周期。 如果两者相互冲突,那么就必须有取舍的。所以,要么不能把对象放在栈上(偏重引用安全),要么放弃引用安全(像C++那样得到词法生命周期),要么栈上的对象不能被引用(牺牲一致性来获得两者)。面面俱到,在语言设计阶段也是不可能的,更不用说实现中会有更多的限制因素。