1。例子一 我们先来看几个例子。先来Java的吧。Java最假了。一般人都认为是引用传递(基本类型值传递)。所以我们又拿C做例子。 写a,b是为了好区分开。其实写俩a都行。不好书面表达publicclassDemo{publicstaticvoidmain(String〔〕args){AanewA();System。out。println(初始化:a。name);a。System。out。println(init函数调用前:a。name);change函数传递init(a);结果?System。out。println(init函数调用后:a。name);}staticvoidinit(Ab){a被初始化了。。bnewA();}privatestaticclassA{S}} 输出:初始化:nullinit函数调用前:maininit函数调用后:main 结果是啥。如果真的是引用传递。那么为啥a。name为啥不是空呢。 我用C给大家写一下。 cincludeinclude classA{public:}; voidinit(Ab){cout未改变的地址:bA();cout改变后的地址:}intmain(intargc,charconstargv〔〕){AanewA();cout初始化:coutinit函数调用前:init(a);coutinit函数调用后:return0;} 输出初始化:init函数调用前:main未改变的地址:0xe699d0改变后的地址:0xe699d0init函数调用后: 显然C和Java的表现是不一样的。为什么C赋值可以改变值。而Java却不是呢。 我们先解释C的做法。 我们init(Ab)做了啥 首先是我们定义了一个变量AanewA(main),我没有写构造函数,假设的。 此时传递给函数init(Ab),此时ba,a0xe699d0。所以呢b0xe699d0。 后面我们的操作就是基于这个b的。此时我们将bA();,是不是将0xe699d0指针指向了A()对象呢(其实就是块内存,指向的是内存的首地址)。此时是不是修改了0xe699d0指针的指向呢。那么a也等于0xe699d0。所以a的值也被修改了。 然后我们看看Java的做法。staticvoidinit(Ab){System。out。println(未改变的地址:0xInteger。toHexString(a。hashCode()));a被初始化了。。bnewA();System。out。println(改变后的地址:0xInteger。toHexString(a。hashCode()));} 我们打印一下hashcode。因为Java的hashcode其实就是确定一个对象的唯一标识,如果你想深入了解hashcode的话介意看看JVM的源码,C和Java对象的映射关系(如果是对象地址的话,那么内存回收会改变大量的内存地址,难道还是地址吗,我们这里不考虑这个。只要知道这个是Java对象的唯一值类似于hash码)。 输出结果:初始化:nullinit函数调用前:main未改变的地址:0x6e8cf4c6改变后的地址:0x12edcd21init函数调用后:main 此时我们发现地址发生了改变。其实理解了上面C那部分聪明的就明白了。 由于Java引用类型传递如果是Hotspot虚拟机则实现的就是简单的指针传递。这个变量指向Java对象的数据区域。 由于一开始a0x6e8cf4c6(Java引用对象),然后函数赋值ba,此时b0x6e8cf4c6。 关键点在于newA()地方;执行的是,实例化一个A对象,在栈顶开辟一块空间,将A对象保存在栈顶中,此时栈顶值0x12edcd21,然后将栈顶值存入到变量b中。此时b0x12edcd21。那么改变原来0x6e8cf4c6指向的内容了吗,并没有。 我们再看看javac编译后的结果:staticvoidinit(com。jvm。reference。DemoA);descriptor:(LcomjvmreferenceDemoA;)Vflags:ACCSTATICCode:栈的深度需要3,变量表需要一个,参数一个stack3,locals1,argssize1实例化一个对象0:new2classcomjvmreferenceDemoA3:dup4:aconstnull调用构造方法5:invokespecial3MethodcomjvmreferenceDemoA。init:(LcomjvmreferenceDemo1;)V将栈顶值存入变量0中8:astore0返回9:return 准确点说Java的做法是:如下操作。voidinit(Ab){cout未改变的地址:AaA();cout改变后的地址:} 其实各种做法也有各种做法的好处。没有绝对的好坏。2。例子二 其实你理解上面这个例子。你就明白了为啥值传递了。我们继续拿C说话。Java查看地址不方便。voidswap(inta,intb){couta:a,b:}intmain(intargc,charconstargv〔〕){intx1;inty2;coutx:x,y:swap方法。swap(x,y);coutx:x,y:codereturn0;} 输出x:1,y:2a:2,b:1x:1,y:2 我们发现为啥函数内部。a和b成功交换了地址。所以a和b的值就互换了。 但是为啥呢。 我们再次打印一下地址coutx:x,y:输出:x:0x61fefc,y:0x61fef8 x0x61fefc,y0x61fef8, 执行swap函数。此时a0x61fefc,b0x61fef8, 然后经过一番操作,此时a0x61fef8,b0x61fefc。然后输出a2,b1,所以成功了。 但是为啥x和y没变呢。是不是发现x和y依旧着原来的地址呢。 正确的swap操作,必须修改指针指向的值。voidswap(inta,intb){}3。Java的引用类型 创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图22所示。如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图23所示。 本文第三节引用自深入理解Java虚拟机。这两种对象访问方式各有优势: 使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。 所以句柄访问方便管理,直接指针访问效率高,但是不方便管理。