(一)Java 的发展与指针引用概念
Java 作为一种广泛应用的编程语言,在发展过程中借鉴了许多其他语言的特性,同时也进行了创新和改进。其中,与 C++ 的关系尤为密切。虽然 Java 没有像 C++ 那样明确的指针定义,但在底层逻辑上,Java 的引用实际上与 C++ 的指针有着相似之处。
Java 的设计目标之一是提高程序的安全性和可靠性。与 C++ 相比,Java 摒弃了一些容易导致错误和安全问题的特性,如指针的直接操作。然而,这并不意味着 Java 中完全没有指针的概念。实际上,Java 的引用可以看作是一种受限的指针。
在 C++ 中,指针是一个存储内存地址的变量,可以直接访问和操作内存中的数据。而在 Java 中,引用是指向对象的一种方式,它并不直接操作内存地址,但在功能上与指针有很多相似之处。例如,通过引用可以访问对象的成员变量和方法,就像在 C++ 中通过指针访问对象的成员一样。
Java 的引用在内存管理方面也起着重要作用。Java 的垃圾回收机制依赖于引用的概念。当一个对象没有被任何引用所指向时,垃圾回收器会自动回收该对象所占用的内存空间。这与 C++ 中的手动内存管理形成了鲜明对比。在 C++ 中,程序员需要手动释放不再使用的内存,否则可能会导致内存泄漏等问题。
此外,Java 的引用在对象传递和赋值方面也与 C++ 的指针有相似之处。在 Java 中,当一个对象被赋值给另一个变量时,实际上是将对象的引用复制给了另一个变量。这就意味着两个变量指向同一个对象,对其中一个变量所指向的对象进行修改,会影响到另一个变量所指向的对象。在 C++ 中,指针的赋值也有类似的效果。
总的来说,Java 虽然没有明确的指针定义,但实际上引用在很多方面都体现了指针的概念。Java 的发展在一定程度上借鉴了 C++ 的底层逻辑,但通过对指针进行限制和封装,提高了程序的安全性和可靠性。
二、Java 中的引用详解
(一)引用的类型及特点
- 强引用:最常见的引用类型,垃圾回收器不会轻易回收被强引用的对象,除非没有引用指向它。强引用就像一条坚固的纽带,紧紧地将对象与程序中的变量联系在一起。只要这个纽带不断,垃圾回收器就不会对该对象进行回收操作。例如,Object obj = new Object();这里的obj就是一个强引用,只要程序中还存在对这个对象的强引用,它就会一直存在于内存中。
- 软引用:用于内存敏感的高速缓存,内存不足时可能被回收。软引用就像是一个较为宽松的束缚,当内存空间足够时,软引用指向的对象可以在内存中存活。但是,当内存不足时,垃圾回收器会考虑回收这些被软引用指向的对象。例如,在一些图片缓存框架中,“内存缓存” 中的图片可以以软引用的方式保存。当系统内存吃紧时,这些软引用的图片对象就会被回收,以释放内存空间。据资料显示,从 Java 1.3.1 开始引入了 jvm 参数 -XX:SoftRefLRUPolicyMSPerMB(默认为 10ms),软引用对象能够被回收需要满足一定的逻辑判断,判断公式如下:clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB。这里的clock表示上次 GC 的时间戳,timestamp表示最近一次读取软引用对象的时间戳,这两者的差值表示该软引用对象多久没被使用了,差值越大,软引用对象价值越低,负数则表示软引用对象刚刚被使用。freespace是空闲空间大小,SoftRefLRUPolicyMSPerMB表示每一 MB 的空闲内存空间可以允许软引用对象存活多久。如果SoftRefLRUPolicyMSPerMB取默认值 1ms,这意味着如果只有 10MB 可用堆内存,GC 将释放已使用超过 10 秒的引用。
- 弱引用:比软引用更弱,垃圾回收器会更积极地回收被弱引用的对象。弱引用就像是一个脆弱的联系,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收。例如,WeakReference weakReference = new WeakReference(new User("zhangsan", 24));System.gc();System.out.println("手动触发 GC:" + weakReference.get());输出结果为手动触发 GC:null,可以看见上面的例子只要垃圾回收一触发,该对象就被回收了。
- 虚引用:最弱的引用类型,主要用于监视对象是否已被垃圾回收。虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。虚引用的唯一作用就是用队列接收对象即将死亡的通知。例如,ReferenceQueue referenceQueue = new ReferenceQueue();PhantomReference phantomReference = new PhantomReference(new User("zhangsan", 24), referenceQueue);System.out.println("什么也不做,获取:" + phantomReference.get());输出结果为什么也不做,获取:null。
(二)引用的使用场景
- 缓存:软引用和弱引用可用于实现缓存,根据内存情况自动回收对象,避免内存溢出。软引用经常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样保证使用缓存的同时,不会耗尽内存。在高速本地缓存 Caffeine 中实现了软引用的缓存,当需要缓存淘汰的时候,如果是只有软引用指向那么久会被回收。弱引用的特点使其可以适用于可有可无的缓存场景。当内存充足时缓存的对象数据可以加速系统,内存紧张时又会被回收掉。
- 线程安全:ThreadLocal 引用用于存储线程本地变量,确保每个线程有独立的对象引用,避免线程间干扰。ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的 ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get () 方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。
- 数据结构:如 WeakHashMap 使用弱引用作为 key,当没有强引用指向 key 时,自动删除相关 entry。在 WeakHashMap 中,当某个键不再正常使用时,会被从 WeakHashMap 中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。WeakHashMap 的 key 是 “弱键”,即是 WeakReference 类型的;ReferenceQueue 是一个队列,它会保存被 GC 回收的 “弱键”。实现步骤是:(01) 新建 WeakHashMap,将 “键值对” 添加到 WeakHashMap 中。实际上,WeakHashMap 是通过数组 table 保存 Entry (键值对);每一个 Entry 实际上是一个单向链表,即 Entry 是键值对链表。(02) 当某 “弱键” 不再被其它对象引用,并被 GC 回收时。在 GC 回收该 “弱键” 时,这个 “弱键” 也同时会被添加到 ReferenceQueue (queue) 队列中。(03) 当下一次我们需要操作 WeakHashMap 时,会先同步 table 和 queue。table 中保存了全部的键值对,而 queue 中保存被 GC 回收的键值对;同步它们,就是删除 table 中被 GC 回收的键值对。
三、Java 指针与引用的区别
(一)操作符差异
在 C/C++ 中,指针操作符丰富多样,为程序员提供了强大的内存操作能力。其中,&操作符用于获取数据的地址,将其存储到一个指针中。例如,定义一个结构体student_t和变量stu1后,可以使用int* p_addr = &stu1;创建一个指针p_addr,此时p_addr存储了stu1的地址。->操作符可以读写一个指针所指向结构体地址的成员数据。比如int age = p_addr->age;用于读取成员数据,p_addr->age = 44;用于修改成员数据。*操作符可以读写一个指针中地址的数据,如student_t stu2 = *p_addr;将p_addr地址的数据读取到stu2,*p_addr = {2, 8};将数据写入p_addr地址。
相比之下,Java 中引用操作符只有.。它主要用于访问对象的成员变量和方法。例如,定义一个类Student和对象stu1后,可以通过Integer age = stu.age;读取成员数据,stu.age = 44;修改成员数据。但不能像 C/C++ 指针那样直接对地址的数据进行读写。
(二)功能差异
- 指针的灵活性在于它可以指向任意一个地址,甚至可以是空地址。比如student_t* p_addr = &stu1;可以指向一个具体的对象地址,也可以像student_t* p_addr = 0x12000;直接给定一个地址,或者student_t* p_addr = NULL;指向空地址。同时,指针可以对地址进行加减操作,从而修改相邻地址的数据,比如修改一个数组。例如int data[4] = {1,2,3,4};int* p_addr = data;*p_addr = 6;p_addr += 1;*p_addr = 7;p_addr += 1;*p_addr = 8;p_addr += 1;*p_addr = 9;此时数组内数据为{6,7,8,9}。
而 Java 中的引用只能指向一个确定的对象,不能直接给其赋地址,也不能为空引用。例如Student stu = new Student();Student stu = 0x12000;这种写法在 Java 中是编译不通过的。
- 指针可以随意修改所指向地址的数据,例如student_t* p_addr = &stu1; *p_addr = 24242;可以将 24242 写入stu1的地址。
而 Java 中的引用只能修改所指向对象的固定成员,或者通过所指向对象提供的固定方法来修改数据。
(三)缺陷差异
- 指针存在野指针问题。当指针在创建时未初始化,此时指向的地址是随机的,对其读写可能破坏程序运行。另外,当指针所指向地址的数据已经被释放,此时对指针读写也会破坏程序运行。原因在于指针可以指向任意一个地址,且不能自动解除指向。例如在底层驱动开发时,寄存器的地址是固定的,想要修改寄存器的数据,需创建一个指针,把寄存器地址赋给指针,然后去修改寄存器。如int* p_led_addr = 0x1233; // LED 寄存器地址是 0x1233,将其赋给指针 p_led_addr*p_led_addr = 1; // LED 亮*p_led_addr = 0; // LED 灭int state = *p_led_addr; // 读取 LED 的亮灭状态。
而 Java 中的引用在对象销毁时会自动解引用,避免了野指针问题。引用必须指向一个确定的对象,增强了内存操作的规范,从而增强了语言内存安全性,降低了对开发者的要求。
- C 语言强制类型转换可能造成内存误修改。将类型 A 的变量s,强制转换成类型 B,然后将其s的地址赋给指向类型 B 的指针p,对指针p读写。此时类型 B 的数据结构可能并不兼容类型 A,导致对变量s的误修改。原因是 C 语言强制类型转换的不严格检查,过于粗鲁。而 Java 引用避免了这种情况。
综上所述,Java 的引用和 C/C++ 的指针在操作符、功能和缺陷方面都存在明显的差异。各有各的用途,我们理解本质后,在不同的场景选择合适的工具即可。
四、Java 中对指针和引用的理解
(一)Java 中引用的创建过程
- 基本数据类型引用创建:在 Java 中,基本数据类型的引用创建相对简单。基本数据类型的值直接存储在栈内存中。当我们声明一个基本数据类型的变量时,例如 int num = 5;,这里的 num 实际上是在栈区创建的一个存储单元,它存储的是数值 5,并非地址。但从某种意义上来说,我们可以把这个变量看作是对这个数值的一种 “引用”,因为当我们再次使用这个变量时,实际上是在自动解引用获取其存储的数值。例如在进行运算 int result = num + 3;,这里直接获取了 num 的值 5 进行加法运算,得到结果 8。
- 引用数据类型引用创建:
-
- 数组:创建数组引用时,首先在栈内存中为数组引用分配一块空间。例如 int[] arr;,这里只是声明了一个数组引用,此时还没有为数组分配实际的内存空间。当使用 new 关键字创建数组对象时,例如 arr = new int[5];,在堆内存中分配一块连续的空间来存储这个包含 5 个整数的数组。数组引用 arr 存储的是这个数组对象在堆内存中的地址。
-
- 字符串:创建字符串引用可以使用直接赋值的方式,例如 String str = "Hello";。在这种情况下,JVM 会先检查字符串常量池中是否已经存在 "Hello" 这个字符串,如果存在,就将引用指向这个已经存在的字符串对象;如果不存在,就先在常量池中创建这个字符串对象,然后将引用指向它。另外,也可以使用 new 关键字创建字符串对象,例如 String str2 = new String("Hello");,这种方式会在堆内存中创建一个新的字符串对象,即使常量池中已经存在相同内容的字符串。
-
- 自定义类:创建自定义类的引用时,例如 MyClass obj;,同样只是在栈内存中为引用分配了空间。当使用 new 关键字创建对象时,例如 obj = new MyClass();,在堆内存中为这个自定义类的对象分配空间,并将对象的引用存储在栈内存中的变量 obj 中。
-
- 自定义类数组:创建自定义类数组的引用和创建普通数组引用类似,首先在栈内存中为数组引用分配空间,例如 MyClass[] arrObj;。然后使用 new 关键字创建数组对象,例如 arrObj = new MyClass[3];,在堆内存中分配一块连续的空间来存储这个包含 3 个自定义类对象的数组。每个数组元素都是一个对自定义类对象的引用,初始值为 null。
(二)引用的实际应用理解
以软件快捷方式类比 Java 中的引用,我们可以更好地理解引用变量存放位置和对象存放位置的关系。在电脑上下载一个软件,把软件安装到某一个硬盘下的某一个文件夹中,为了能够方便地打开这个软件,都会在桌面上创建一个快捷方式。快捷方式存放的位置就类似于 Java 中引用变量存放的栈内存,而软件安装的实际位置就相当于 Java 中对象存放的堆内存。通过快捷方式打开软件,就如同通过引用变量访问对象中的所有成员。快捷方式和软件的实际位置是不一样的,通过快捷方式可以快速找到软件的实际位置并打开它。同样,在 Java 中,引用变量存储的是对象在堆内存中的地址,通过引用变量可以方便地访问对象的成员。当我们需要使用一个对象时,只需要通过引用变量即可,而不需要直接去操作堆内存中的对象地址。这种方式使得 Java 的内存管理更加安全和高效。
五、总结
Java 中的引用在很多方面与 C/C++ 中的指针有着明显的区别,但它们在各自的语言体系中都扮演着重要的角色。
在 Java 中,引用虽然不像 C/C++ 的指针那样可以进行灵活的内存操作,但它在内存管理方面具有很大的优势。Java 的垃圾回收机制依赖于引用的概念,当一个对象没有被任何引用所指向时,垃圾回收器会自动回收该对象所占用的内存空间。这大大减轻了程序员的内存管理负担,提高了程序的安全性和可靠性。
同时,Java 中的引用在对象生命周期控制方面也发挥着重要作用。不同类型的引用,如强引用、软引用、弱引用和虚引用,可以根据不同的需求来控制对象的生命周期。强引用保证对象在程序运行期间的存在,软引用和弱引用可以在内存不足时被回收,虚引用则主要用于监视对象是否已被垃圾回收。
相比之下,C/C++ 中的指针虽然具有强大的内存操作能力,但也带来了很多安全隐患。野指针和强制类型转换可能导致的内存误修改等问题,都需要程序员格外小心。手动内存管理也增加了程序的复杂性和出错的可能性。
理解 Java 中的引用和 C/C++ 中的指针的差异和特点,有助于程序员更好地编写 Java 程序。在 Java 编程中,我们应该充分利用引用的优势,合理使用不同类型的引用,以提高程序的性能和安全性。同时,我们也可以从 C/C++ 的指针中借鉴一些内存管理的思想,加深对计算机内存结构和程序运行机制的理解。
标签:Java,addr,对象,指针,内存,解析,引用 From: https://blog.csdn.net/weixin_41903456/article/details/142752689