==和equals,栈中句柄和主类型的生命周期

其实这一章我感觉都比较简单明了,就是一个equals这个比较难懂,所以干脆就详细看一看这个问题。

一、==和equals

之前只是大概的知道这两个东西的区别,基本上不会用错。

equals 方法是 java.lang.Object 类的方法,有两种用法说明:

(1)对于字符串变量来说,使用“==”和“equals()”方法比较字符串时,其比较方法不同

“==”比较两个变量本身的值,即两个对象在内存中的首地址。

“equals()”比较字符串中所包含的内容是否相同。

这里一般不会弄错。

String类中重写了equals方法,比较的是值而不是地址,所以这里的equals结果是true

这个StringBuffer并不是String类,StringBuffer没有重写equals这个方法,那就是继承Object中的equals方法,而Object中的equals方法是比较这两个对象的堆中地址是否同一个,因为new了两个对象,所以他们是两块不同的地址,因此结果都是false。

严格来说,这个==和equals都起到一样的效果。

(2)对于基本类型的比较,不能使用equals,只能使用==

根本就没有equals这个方法。

(3)对于主类型的包装类,和上文的String一样,equals是比较值的,而不是地址

对于基本类型的包装类型,比如Boolean、Character、Byte、Shot、Integer、Long、Float、Double等的引用变量,==是比较地址的,而equals是比较内容的。

和上面的String一样。

如果以上都分辨得很清楚了,那么理解这个应该不成问题才对:

关键是new发生了什么,声明String的时候句柄创建在哪里…应该不难理解。

(4)最后的一个例子可能比较难懂

为什么都是false呢?原因跟上文中的StringBuffer是完全一样的…

Value是我自定义的类,没有复写equals方法,所以就继承了Object中的equals方法,而Object中的equals方法是比较内存地址的,因为v1和v2都是new的,在堆中是两个不同的地址,所以就返回false了。

二、关于栈中的句柄等生命周期

我在上一篇文章 think in java 第二章 一切都是对象 第二部分中提到一个问题就是:

这里说java对象不具备与主类型一样的存在时间,之前好像提到过java中的主类型和句柄都是放在java栈里的,难道栈里的这些东西都有一定的存在时间吗?所以GC不需要在栈中进行回收吗?这个就需要好好研究下jvm了…

现在回顾一下感觉其实有点蠢,感觉我当时是没有理解好变量作用域那个部分。

变量作用域中有这么一个概念:

那么句柄s会在作用域的终点处消失。

栈中的句柄超出作用域之后就释放了,而不是有一定的存在时间。因为超出作用域就被释放掉,自然就不需要GC啦。

所以现在我觉得,栈中的主类型和句柄都有其作用域,超过即释放。而堆中的对象则需要通过GC来回收。

比如说之前这段代码

这里的s是句柄,放在栈中,而new则在堆中开辟了一块区域来放置这个对象,然后s句柄再去指向这块区域。

现在s超过作用域了,栈中的句柄s就被释放了,不存在s这个句柄了。但是堆中的这块内存区域依然存在,等待着被GC回收。

如果我再次执行这样一段代码,就会在栈中新分配内存放句柄,然后又在堆中新分配内存放对象…然后再超过作用域,再释放…一直这样运作。

三、那么我觉得问题来了

看到这里的:

我很自然就联想到了

这两种创造方式有什么不同?下面的这种方式有没有在堆中创建对象?

关于这点,我个人认为当然在堆中开辟了空间,用于放这个”a string”对象。然后栈中存放s这个引用,指向堆中的对象。

如果这个是正确的,那么下面这种情况要怎么解释呢?

这里又声明了一个String,那么这里需要在堆中新开辟空间吗?然后引用k再去指向这个新的空间?

我一开始以为是这样的,后来才知道,这里并没有在堆中又新创建一个”a string”对象,而是应用k直接指向上一个对象,实现了已有对线的重用(这个实现我觉得太合理了,很大程度上减轻了jvm的负担,不用过多浪费内存去处理相同的对象)。

那么再回顾一下:

这里的情况和上面一样吗?

很遗憾,因为new的关键词存在,虽然这两个对象的值是一样的,但是在堆中确实两个完全不同的对象。然后s和k分别指向他们。

这里有很多非常好的回答,我也是从中得到我现在的观点的,网址在此:

https://www.zhihu.com/question/29884421

四、那么我觉得问题又来了

这里有这样一个解释:

Java栈内存以帧的形式存放本地方法的调用状态(包括方法调用的参数,局部变量,中间结果等)。每调用一个方法就将对应该方法的方法帧压入Java栈,成为当前方法帧。当调用结束(返回)时,就弹出该帧。

也就是说:在方法中定义的一些基本类型的变量和对象的引用变量都在方法的栈内存中分配。

当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作它用。

在这里我感觉,情况可能有所不同。

比如说像下面这样:

那么,这里的变量i的作用域只是到for为止,那么i在这个方法帧里是什么时候释放的呢?是在出了for的作用域之后立即释放,还是等整个方法都结束时一起释放呢?

因为我当前水平有限,不知道怎么去验证,但是看别人的回答应该是在方法结束时才释放的:

所有局部变量在方法调用的时候已经分配好,只有方法调用结束后,整个方法的栈帧才会被废弃,这个变量的空间才会被回收。

当调用一个java方法时,产生一个帧,帧里面包括操作数栈、局部变量区和帧数据区。

这个局部变量区的大小在编译的时候就已经确定了,在运行时是通过索引来访问的,类似于数组,所以即使出了作用域,这个局部变量区还是那么大,不会被compact的,直到方法调用结束将整个帧discard。

现在我似乎有点明白了,方法栈里的资源应该是整个方法结束时才进行统一回收的。

但是如果是类,一出类的作用域,栈里的资源就会被回收了。(这里还是存疑的,回头再来修改)

五、总结

总之这一章的逻辑和控制部分用得很多,功能上我已经比较熟练了,但是可惜的是不知道这些控制是怎么在jvm里面实现的,也没有详细的解释,感觉有些可惜。但是更加巩固了我对jvm创建对象的流程的认识。

发表评论

电子邮件地址不会被公开。 必填项已用*标注