一、值类型与引用类型
在 Java 中类型可分为两大类:值类型与引用类型。值类型就是基本数据类型(如 int、double 等),而引用类型是指除了基本的变量类型之外的所有类型(如通过 class 定义的类型)。所有的类型在内存中都会分配一定的存储空间(形参在使用的时候也会分配存储空间,方法调用完成之后,这块存储空间自动消失),基本的变量类型只有一块存储空间(分配在stack中),而引用类型有两块存储空间(一块在 stack 中,一块在 heap 中)
。
1、什么是引用
引用其实就像是一个对象的名字或者别名 (alias)
,一个对象在内存中会请求一块空间来保存数据,根据对象的大小,它可能需要占用的空间大小也不等。访问对象的时候,我们不会直接是访问对象在内存中的数据,而是通过引用去访问
。引用也是一种数据类型,我们可以把它想象为类似 C++ 语言中指针的东西,它指示了对象在内存中的地址——只不过我们不能够观察到这个地址究竟是什么。
如果我们定义了不止一个引用指向同一个对象,那么这些引用是不相同的,因为引用也是一种数据类型
,需要一定的内存空间(stack,栈空间)来保存。但是它们的值是相同的,都指示同一个对象在内存(heap,堆空间)的中位置
。比如:
通过上面的代码和图形示例不难看出,a 和 b 是不同的两个引用,我们使用了两个定义语句来定义它们。但它们的值是一样的,都指向同一个对象 “This is a Text!”。但要注意String 对象的值本身是不可更改的 (像 b = “World”; b = a; 这种情况不是改变了 “World” 这一对象的值,而是改变了它的引用 b 的值使之指向了另一个 String 对象 a)。
如图,开始b 的值为绿线所指向的“Word Two”,然后 b=a; 使 b 指向了红线所指向的”Word“。
2、引用与对象
在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。
(1)创建一个引用,
引用可以独立存在,并不一定需要与一个对象关联
。
String s;
(2)通过将这个叫“引用”的标识符指向某个对象,之后便可以通过这个引用来实现操作对象了。
s = new String("abc");
System.out.println(s.toString());
二、四种引用类型
在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。
1、强引用(Strong Reference)
Java 中默认声明的就是强引用。
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出 OutOfMemoryError,不会去回收
。
如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为 null,这样一来,JVM 就可以适时的回收对象了
。
Student student = new Student(); // 只要 student 还指向 Student 对象,student 对象就不会被回收
student = null; // 手动置 null,结束引用
2、软引用(Soft Reference)
软引用是用来描述一些非必需但仍有用的对象。
在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常
。这种特性常常被用来实现内存敏感的高速缓存
,比如网页缓存,图片缓存等。只要垃圾回收器没有回收它,这个对象就能被程序使用。
SoftReference 的特点是它的一个实例保存对一个 Java 对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦 SoftReference 保存了对一个 Java 对象的软引用后,在垃圾线程对这个 Java 对象回收前,SoftReference 类所提供的 get() 方法返回Java对象的强引用。一旦垃圾线程回收该 Java 对象之后,get() 方法将返回 null。
在 JDK1.2 之后,用 java.lang.ref.SoftReference 类来表示软引用。
Student student = new Student();
SoftReference softReference = new SoftReference(student);
3、弱引用(Weak Reference)
弱引用的引用强度比软引用要更弱一些,
无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收
。
在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
Student student = new Student();
WeakReference weakReference = new WeakReference(student);
4、虚引用(Phantom Reference)
虚引用是最弱的一种引用关系,
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
。
设置虚引用的目的是为了被虚引用关联的对象在被垃圾回收器回收的时候,能够收到一个系统通知
(用来跟踪对象被GC回收的活动)。
在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个 null,也就是说将永远无法通过虚引用来获取对象。
虚引用必须要和 ReferenceQueue 引用队列一起使用
。
Student student = new Student();
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(student,queue);
5、引用类型与 GC 回收
(1)强引用
强引用表示一个对象处在【有用,必须】的状态,是使用最普遍的引用。如果一个对象具有强引用,那么垃圾回收器绝不会回收它。就算在内存空间不足的情况下,Java 虚拟机宁可抛出 OutOfMemoryError 错误,使程序异常终止,也不会通过回收具有强引用的对象来解决内存不足的问题。
(2)软引用
1)回收 Java 对象:
软引用表示一个对象处在【有用,但非必须】的状态。在内存空间足够的情况下,如果一个对象只具有软引用,那么垃圾回收器就不会回收它,但是如果内存空间不足,垃圾回收器就会回收这个对象(回收发生在 OutOfMemoryError 错误之前)。只要垃圾回收器没有回收它,这个对象就能被程序使用。
2)回收 SoftReference 对象:
作为一个Java对象,SoftReference 对象除了具有保存软引用的特殊性之外,也具有 Java 对象的一般性。所以,
当软对象被回收之后,虽然这个 SoftReference 对象的 get() 方法返回 null,但这个 SoftReference 对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量 SoftReference 对象带来的内存泄漏
。
软引用可以和一个引用队列(Reference Queue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中,以便在恰当的时机将该软引用回收。但是由于GC线程的优先级较低,通常手动调用System.gc()并不能立即执行GC,因此软引用所引用的对象并不一定会被马上回收。
(3)弱引用
弱引用表示一个对象处在【可能有用,但非必须】的状态。类似于软引用,但是强度比软引用更弱一些:只具有弱引用的对象拥有更短暂的生命周期。GC线程在扫描它所管辖的内存区域的过程中,一旦发现只具有弱引用的对象,就会回收掉这些被弱引用关联的对象。也就是说,无论当前内存是否紧缺,GC都会回收被弱引用关联的对象。不过,由于GC是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用同样可以和一个引用队列(Reference Queue)联合使用,如果弱引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
(4)虚引用
虚引用表示一个对象处在【无用】的状态。这意味着虚引用等同于没有引用,在任何时候都可能被GC回收。设置虚引用的目的是为了被虚引用关联的对象在被垃圾回收器回收的时候,能够收到一个系统通知(用来跟踪对象被GC回收的活动)。
虚引用和弱引用的区别在于:
虚引用的使用必须和引用队列(Reference Queue)联合使用
。
GC在回收一个对象时,如果发现该对象具有虚引用,那么在回收之前就会首先将该对象的虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入虚引用来了解被引用的对象是否将要被GC回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
(5)引用队列
作为一个Java对象,SoftReference 对象除了具有保存软引用的特殊性之外,也具有 Java 对象的一般性。所以,
当软对象被回收之后,虽然这个 SoftReference 对象的 get() 方法返回 null,但这个 SoftReference 对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量 SoftReference 对象带来的内存泄漏
。
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
在 java.lang.ref 包里还提供了 ReferenceQueue 类。
比如在创建 SoftReference 对象的时候,可以使用一个 ReferenceQueue 对象作为参数提供给 SoftReference 的构造方法。
Student student = new Student();
ReferenceQueue referenceQueue = new ReferenceQueue();
SoftReference ref = new SoftReference(student, referenceQueue);
当这个 SoftReference 所软引用的实例对象被垃圾收集器回收的同时,ref 所强引用的 SoftReference 对象也会被列入 ReferenceQueue 中。也就是说,ReferenceQueue 中保存的对象是 Reference 对象,而且是已经失去了它所软引用的对象的 Reference 对象
。另外从 ReferenceQueue 的名字也可以看出,它是一个队列,当我们调用它提供的 poll() 方法的时候,如果这个队列不是空队列,那么将返回队列前面的哪个 Reference 对象。
在任何时候,我们都可以调用 ReferenceQueue 的 poll() 方法来检查它所关心的非强可及对象被回收。如果队列为空,将返回一个 null,否则将返回队列中前面的一个 Reference 对象。利用这个方法,我们可以检查哪个 SoftReference 所软引用的对象已经被回收。
于是我们可以把这些失去所软引用对象的 SoftReference 对象清除掉。常用的方式为:
SoftReference ref = null;
while ((ref = (EmployeeRef) queue.poll()) != null) {
// 清除ref
}
三、数据存储方式
1、局部变量/方法参数
对于局部变量和方法传递的参数在 jvm 中的存储方式是相同的,都是存储在栈上开辟的空间中
。方法参数空间在进入方法时开辟,方法退出时进行回收。以 32 为 JVM 为例,boolean、byte、short、char、int、float以及对应的引用类型都是分配4字节大小的空间,long、double分配8字节大小空间。对于每一个方法来说,最多占用空间大小是固定的,在编译时就已经确定了。当在方法中声明一个 int 变量 i=0 或 Object 变量 obj=null 时,此时仅仅在栈上分配空间,不影响到堆空间。当 new Object() 时,将会在堆中开辟一段内存空间并初始化Object对象
。
2、引用类型的数组和对象
(1)一维数组
数组也是对象,当声明数组 int[] arr=new int[2] 时,
在堆中开辟相应大小空间进行存储数组内容,数组名 arr 实际上是引用,指向堆中的数组内容,然后在栈上占用4个字节大小的存储空间来存放 arr 引用
。
(2)二维数组
当声明一个二维数组时,如 int[][] arr2=new int[2][4],arr2 同样在栈中占用4个字节,
在堆内存中开辟长度为 2,类型为 int[] 的数组对象,然后 arr2 指向这个数组
。这个数组内部有两个引用类型(大小为4个字节),分别指向两个长度为 4 类型为 int 的数组
。内存分布如图:
所以当传递一个数组给一个方法时,数组的元素在方法内部是可以被修改的,但是无法让数组引用指向新的数组
。
(3)普通对象
栈中存储的是对象的地址(即引用)
,而对象本身不存放在栈中,而是存放在堆中
,使用时通过栈中的引用地址找到堆中的实际对象
,这里的引用地址类似于C/C++中的指针。
注意:Object obj=null 时,此时仅仅在栈上分配空间,不影响到堆空间;当 new Object() 时,将会在堆中开辟一段内存空间并初始化 Object 对象
。
(4)String 类型对象
对于 String 类型,其对象内部需要维护三个成员变量,char[] chars,int startIndex, int length。chars 是存储字符串数据的真正位置,在某些情况下是可以共用的,例如:String str=new String(“hello”),内存分布如下:
实际上 String 类型是不可变类型,所以
当传递一个 String 字符串给一个方法时, String 字符串在方法内部是不可以被修改的,但是可以让 String 引用指向新的字符串
。
四、变量的初始化与赋值
1、变量初始化
int num=10;
String str="hello"
从上图可以显而易见:
(1)num 是 int 基本类型变量,值就直接保存在 stack 中的变量中
。
(2)str 是 String 引用类型变量,stack 变量中保存的只是 heap 中实际对象对应的地址信息,而不是实际对象数据
。
2、变量赋值
num=20;
str="java";
(1)
对于基本类型变量 num,赋值运算符将会直接修改 stack 中变量的值
,原来的数据将被覆盖掉,被替换为新的值。
(2)对于引用类型变量 str,赋值运算符只会改变 stack 中变量中所保存的对象的地址信息
,原来对象的地址被覆盖掉,重新写入新对象的地址数据。但heap 中原来的对象本身并不会被改变,只是不再被任何引用所指向,即“垃圾对象”,后续会被垃圾回收器回收
。
五、函数传参
1、基本数据类型:按值传递
public void func(int value) {
value = 100;
}
参数类型是基本数据类型的时候,是按值传递的。对于基本类型参数,虽然在方法中改变了传进来的参数的值,但对这个参数原始变量本身并没有影响。
以参数形式传递简单类型的变量时,实际上是将参数的值做了一个拷贝再传进方法函数的,那么在方法函数里再怎么改变其值,其结果都是只改变了拷贝的值,而不是源值
。
2、引用数据类型:按地址传递
(1)对于提供修改自身的成员方法的引用类型,原始的数据会被更改
StringBuilder sb = new StringBuilder("test");
public void func(StringBuilder sBuilder) {
sBuilder.append("aa");
}
(2)对于没有提供修改自身的成员方法引用类型,原始不会被更改
String sb = "test";
public void func(String sBuilder) {
sBuilder = "111";
}
因为 String 类型的字符串是一个常量,不能被改变,所以在函数中 sBuilder = “111” 并
没有改变原来值,而是重新指向了新的 String 类型的字符串
。