1 简述栈、堆和方法区的用法
通常我们定义一个基本数据类型的变量(非成员变量,对象的成员变量和对象一起放在堆中)、一个对象的引用、还有就是函数调用的现场保存都使用JVM中的栈(stack)空间。
通过new关键字和构造器创建的对象则放在堆(heap)空间,没有被引用的对象就成为“垃圾”,因此堆是垃圾收集器(GC)管理的主要区域。由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老年代,再具体一点可以分为Eden、Survivor、Tenured。堆是线程共享的内存区域。
方法区(method area)用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。程序中的字面量,如直接书写的100、“hello”和常量都是放在常量池中,常量池是方法区的一部分。
栈空间操作起来最快但是栈空间很小,通常大量的对象都放在堆空间,栈和堆的大小都是可以通过JVM的启动参数进行调整。栈空间用完了会引发StackOverflowError,而堆和常量池空间不足会引发OutOfMemoryError。
String str = new String(“Hello”);
上面语句中变量str是一个引用,放在栈空间上的,用new创建出来的字符串对象是放在堆空间上的,而“Hello”这个字面量是放在方法区的。栈空间中存放的是new对象在堆空间存放的地址值。
Person p = new Person(1);
Class Person{
int a=1;
public Person(int a){
this.a=a;
}
}
上面语句中p是一个指向Person类的实例(对象)的一个引用,放在栈空间中。真正创建对象的语句是new Person(1),这个对象放在堆空间中,对应栈空间p中存放的是这个Person对象在堆空间中的地址。其中a是成员变量,存放在堆空间中的。
若声明了一个int类型的非成员变量a,然后对a进行赋值为1,因为a为基础数据类型直接保存在栈空间中,因此栈空间中a对应存储的就是1。
注意:
引用《java编程思想(第四版)》P23页的一句话:
对于这些基本类型,java采取与C和C++相同的方法,也就是说,不用new来创建变量,而是创建一个并非是引用的“自动”变量。这个变量直接存储“值”,并置于堆栈中,因此更加高效。
明白了上面关于对象和变量的储存特点,现在我们来说说什么是java值传递。
2 值传递
2.1 什么是值传递?
按值传递指的是在方法调用时,传递的参数是把值的拷贝传递过去的。由于传递的是值的拷贝,因此按值传递的特点就是传递过后两个参数互不相干。
示例如下:
public class Test {
public static void main(String[] args) {
Test test=new Test();
int a=1;
test.sum(a);
System.out.println("main方法里的a="+a);
}
public void sum(int a){
a=2;
System.out.println("sum方法里的a="+a);
}
}
运行结果:
sum方法里的a=2
main方法里的a=1
2.2 java值传递过程分析
(1)当运行到main方法第2行代码时:
首先在栈空间中创建一个变量为a,然后将1存入变量a所在内存空间中。
(2)当运行到sum方法中时:
main方法将a变量的拷贝传给sum方法,此时在栈内存中会为sum方法中的变量a开辟一块空间,存入由main方法传过来的值1。通过a=2操作过后将sum方法中a的值改变成2。因此sum方法中的变量a更改不会影响main方法中的变量a。
图(1)
3 引用传递
3.1 什么是引用传递?
引用传递是C语言里面的一种参数传递方式,就是在方法调用时,传递给另一个方法的是对象的引用,是同一个引用。但是在java语言中,并不存在像C语言这样的引用传递,java语言中只存在值传递。之所以很多人把java传递对象误以为是引用传递,是因为没有深入的理解对象创建的内存分配规则。在java中传递对象,其实是传递的对象引用的值的拷贝,这个值也就是对象在堆内存中的地址。
Java中对象引用值的传递的特点是:两个引用指向的是同一个对象,即同一个内存空间(试想,如果是引用传递,则就不会存在两个引用了,只会有一个引用)。
示例代码如下:
public class Test {
public static void main(String[] args) {
Test test=new Test();
Person p;
p=new Person();
p.setAge(5);
test.handle(p);
System.out.println("main方法里:"+p.toString());
}
public void handle(Person p){
p.setAge(10);
System.out.println("handle方法里:"+p.toString());
}
}
class Person{
int age=0;
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [age=" + age + "]";
}
}
运行结果:
handle方法里:Person [age=10]
main方法里:Person [age=10]
3.2 Java“引用传递”过程分析
(1)当运行完main方法第4行时:
此时程序在栈空间中开辟了一块空间存放Person的一个引用p,并且在堆内存里面创建了一个对象。
图(2)
(2)当运行完main方法第5行的时:
此时main方法将引用p地址值的一个拷贝传递给handle方法,handl方法首先创建一个新的引用p,并且将传过来的地址值赋值给新引用p。此时两个引用都指向同一个内存空间。
图(3)
(3)当运行完handle方法的第1行时:
此时handle方法中的引用p,修改了地址值为0000的Person对象的age=10,由于两个引用指向的是同一个对象,因此main方法中Person对象的age也等于10,因为是同一个Person对象。
图(4)
假如将handle方法修改成如下代码:
public void handle(Person p){
p=new Person();
p.setAge(10);
System.out.println("handle方法里:"+p.toString());
}
运行结果为:
handle方法里:Person [age=10]
main方法里:Person [age=5]
分析过程为:
运行到新handle方法第一行之前,运行过程和上面(1)(2)一样,只不过当运行完第1行后放生了点变化。handle中重新new了一个Person对象,并且将handle方法中的引用p指向这个对象。
图(5)
运行完handle方法第2行代码后,handle里面的引用p修改了Person对象的age,此时由于main方法和handle方法中的引用指向了各自的Person对象,因此此次修改操作不会影响main方法中的Person对象,因此:main方法里:Person [age=5]。
图(6)
26.4 总结
(1)Java当中参数传递都是值传递!
(2)对于这些基本类型,java采取与C和C++相同的方法,也就是说,不用new来创建变量,而是创建一个并非是引用的“自动”变量。这个变量直接存储“值”,并置于堆栈中,因此更加高效。
(3)对于大家理解的引用传递,其实传递的是引用的值,即传递引用对象的地址值。
注意:如有问题请批评指正!
【四川乐山程序员联盟,欢迎大家加群相互交流学习5 7 1 8 1 4 7 4 3】