JAVA的引用与C++的指针有什么区别
- [1. Java值类型与引用类型](#1. Java值类型与引用类型)
-
- [1.1 变量初始化](#1.1 变量初始化)
- [1.2 变量赋值](#1.2 变量赋值)
- [1.3 函数传参](#1.3 函数传参)
- [2. Java数据存储方式](#2. Java数据存储方式)
-
- [2.1 Java局部变量&&Java方法参数](#2.1 Java局部变量&&Java方法参数)
- [2.2 Java数组类型引用和对象](#2.2 Java数组类型引用和对象)
- [2.3 String类型数据](#2.3 String类型数据)
- [3. Java引用类型](#3. Java引用类型)
-
- [3.1 强引用](#3.1 强引用)
- [3.2 软引用](#3.2 软引用)
- [3.3 弱引用](#3.3 弱引用)
- [3.4 虚引用](#3.4 虚引用)
- [4. JAVA的引用与C++的指针有什么区别](#4. JAVA的引用与C++的指针有什么区别)
1. Java值类型与引用类型
1.1 变量初始化
java
int num=10;
String str="hello"
1.2 变量赋值
从上图可以显而易见,num是int基本类型变量,值就直接保存在变量中 。str是String引用类型变量,变量中保存的只是实际对象对应的地址信息,而不是实际对象数据。对于而这特性,如下:
java
num=20;
str ="java";
对于基本类型变量num,赋值运算符将会直接修改变量的值,原来的数据将被覆盖掉,被替换为新的值。对于引用类型变量str,赋值运算符只会改变变量中所保存的对象的地址信息,原来对象的地址被覆盖掉,重新写入新对象的地址数据。但原来的对象本身并不会被改变,只是不再被任何引用所指向的对象,即"垃圾对象",后续会被垃圾回收器回收。
1.3 函数传参
java
// 基本类型参数,原始value不会被更改
public void func(int value) {
value = 100;
}
// 对于没有提供修改自身的成员方法引用类型,原始str不会被更改
public void func(String str) {
str = "hello";
}
StringBuilder sb = new StringBuilder("test");
// 对于提供修改自身的成员方法引用类型,原始的sBuilder会被更改
public void func(StringBuilder sBuilder) {
sBuilder.append("aa");
}
// 原始的sBuilder不会被更改
public void test(StringBuilder sBuilder) {
sBuilder = new StringBuilder("111");
}
说明:对于第三种情况:
对于第四种情况:
2. Java数据存储方式
2.1 Java局部变量&&Java方法参数
对于局部变量和方法传递的参数在jvm中的存储方式是相同的,都是存储在栈上开辟的空间中。方法参数空间在进入方法时开辟,方法退出时进行回收。以32为JVM为例,boolean、byte、short、char、int、float以及对应的引用类型都是分配4字节大小的空间,long、double分配8字节大小空间。对于每一个方法来说,最多占用空间大小是固定的,在编译时就已经确定了。
当在方法中声明一个int
变量i=0
或Object
变量obj=null
时,此时仅仅在栈上分配空间,不影响到堆空间。当new Object()
时,将会在堆中开辟一段内存空间并初始化Object对象。
2.2 Java数组类型引用和对象
当声明数组时,int[] arr=new int[2]
;数组也是对象,arr实际上是引用,栈上占用4个字节大小的存储空间,而是会在堆中开辟相应大小空间进行存储,然后arr变量指向它。当声明一个二维数组时,如:int[][] arr2=new int[2][4]
,arr2同样在栈中占用4个字节,在堆内存中开辟长度为2,类型为int[]
的数组对象,然后arr2指向这个数组。这个数组内部有两个引用类型(大小为4个字节),分别指向两个长度为4类型为int的数组。内存分布如图:
所以当传递一个数组给一个方法时,数组的元素在方法内部是可以被修改的,但是无法让数组引用指向新的数组。其实,还可以声明:int [][] arr3=new int[3][]
,内存分布如下:
2.3 String类型数据
对于String类型,其对象内部需要维护三个成员变量,char[] chars,int startIndex, int length。chars是存储字符串数据的真正位置,在某些情况下是可以共用的,实际上String类型是不可变类型。例如:String str=new String("hello")
,内存分布如下:
3. Java引用类型
在JAVA中提供了四种引用类型:强引用、软引用、弱引用和虚引用。在四种引用类型中,只有强引用FinalReference
类型变量是包内可见的,其他三种引用类型均为public
,可以在程序中直接使用。
3.1 强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那么垃圾回收器绝不会回收它。
之前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。比如下面这段代码中的object和str都是强引用:
java
Object object = new Object();
String str = "StrongReference";
如果一个对象具有强引用,那就类似于必不可少的物品,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
java
public class StrongReference {
public static void main(String[] args) {
new StrongReference().method1();
}
public void method1(){
Object object=new Object();
Object[] objArr=new Object[Integer.MAX_VALUE];
}
}
运行结果:
当运行至Object[] objArr = new Object[Integer.MAX_VALUE]时,如果内存不足,JVM会抛出OOM错误也不会回收object指向的对象。不过要注意的是,当method1运行完之后,object和objArr都已经不存在了,所以它们指向的对象都会被JVM回收。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
比如ArraryList类的clear方法中就是通过将引用赋值为null来实现清理工作的
java
public void clear() {
modCount++;
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
在ArrayList类中定义了一个私有的变量elementData数组,在调用方法清空数组时可以看到为每个数组内容赋值为null。不同于elementData=null,强引用仍然存在,避免在后续调用 add()等方法添加元素时进行重新的内存分配。使用如clear()方法中释放内存的方法对数组中存放的引用类型特别适用,这样就可以及时释放内存。
强引用特点:
- 强引用可以直接访问目标对象
- 只要有引用变量存在,垃圾回收器永远不会回收。JVM即使抛出OOM异常,也不会回收强引用所指向的对象。
- 强引用可能导致内存泄漏问
3.2 软引用
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
java
import java.lang.ref.SoftReference;
public class SoftRef {
public static void main(String[] args){
System.out.println("start");
Obj obj = new Obj();
SoftReference<Obj> sr = new SoftReference<Obj>(obj);
obj = null;
System.out.println(sr.get());
System.out.println("end");
}
}
class Obj{
int[] obj ;
public Obj(){
obj = new int[1000];
}
}
当内存足够大时可以把数组存入软引用,取数据时就可从内存里取数据,提高运行效率
软引用在实际中有重要的应用,例如浏览器的后退按钮,这个后退时显示的网页内容可以重新进行请求或者从缓存中取出:
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出这时候就可以使用软引用
3.3 弱引用
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。所以被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
java
import java.lang.ref.WeakReference;
public class WeakRef {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
运行结果:
在使用软引用和弱引用的时候,我们可以显示地通过System.gc()来通知JVM进行垃圾回收,但是要注意的是,虽然发出了通知,JVM不一定会立刻执行,也就是说这句是无法确保此时JVM一定会进行垃圾回收的。
弱引用还可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
3.4 虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
java
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomRef {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
使用-Xmx2M限定堆内存,使用WeakHashMap的代码正常运行结束,而使用HashMap的代码段抛出异常:java.lang.OutOfMemoryError: Java heap space。由此可见,WeakHashMap会在系统内存紧张时使用弱引用,自动释放掉持有弱引用的内存数据。但如果WeakHashMap的key都在系统内持有强引用,那么WeakHashMap就退化为普通的HashMap,因为所有的数据项都无法被自动清理。
引用类型 | 被回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时 |
弱引用 | jvm垃圾回收时 | 对象缓存 | gc运行后 |
虚引用 | 未知 | 未知 | 未知 |
-
在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生
-
利用软引用和弱引用解决OOM问题:假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
-
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
4. JAVA的引用与C++的指针有什么区别
Java 的引用和 C++ 的指针在概念上有一些相似之处,但也存在一些重要的区别。以下是它们之间的主要区别:
-
内存管理:
- Java 引用: Java 使用自动内存管理(垃圾回收)。在 Java 中,对象的内存管理由垃圾收集器负责,程序员无需手动释放内存。引用的生命周期由垃圾收集器来监控,当没有任何引用指向一个对象时,该对象将被认为是不可达的,垃圾收集器会回收其内存。
- C++ 指针: C++ 使用手动内存管理。程序员负责为分配的内存进行释放。通过
new
运算符分配的内存必须由程序员使用delete
运算符手动释放,否则可能导致内存泄漏。
-
空引用(Null References):
- Java 引用: 在 Java 中,引用可以是空(
null
),表示不指向任何对象。对空引用进行解引用会导致NullPointerException
。 - C++ 指针: C++ 中的指针可以为
nullptr
(在 C++11 之前通常使用NULL
),表示不指向任何地址。对空指针进行解引用会导致未定义行为。
- Java 引用: 在 Java 中,引用可以是空(
-
类型安全:
- Java 引用: 引用是类型安全的,编译器会在编译时执行类型检查,确保引用只能指向与其声明类型相符的对象。
- C++ 指针: 指针可以进行类型转换,并且在编译时不进行类型检查。这可能导致类型不匹配的错误,需要程序员自己负责确保类型安全性。
-
指针算术:
- Java 引用: Java 不支持指针算术,不允许直接对引用进行指针运算。
- C++ 指针: C++ 允许指针进行算术运算,可以通过指针进行地址运算。
-
多级引用(Reference Levels):
- Java 引用: Java 中只有一级引用,垃圾收集器负责处理对象之间的引用关系。
- C++ 指针: C++ 允许多级指针,即指针的指针。这种情况在一些特定的应用场景中可能会更灵活,但也需要谨慎处理。
总体而言,Java 的引用更加高级和抽象,提供了更简单的内存管理和类型安全性,而 C++ 的指针更灵活但也更容易引入一些潜在的错误。选择使用引用还是指针取决于编程语言的设计哲学以及特定的应用需求。