| 泠岚's profile雪汐斋PhotosBlogLists | Help |
|
|
May 30 java 传值 vs. 传引用java程序的函数调用到底是传值呢还是传参呢?这可是个难缠的问题,如果搞不清楚还是挺容易出错的:P对于这个问题,最经典的解释莫过于“java函数是传值的,java函数传递的参数是对象的引用”
这两句话好像初听上去有点绕,不过意思倒是表达得蛮精确的。 我看到过几个解释这个问题的例子,不过个人感觉看过例子之后还是只知道是什么不知道为什么,停留在照猫画虎的水平上还是挺容易出问题的。所以举例子之前,先从jvm的实现原理上有个了解应当是不无裨益的。jvm的结构图前一阵子贴到blog上了,那可是从“深入java虚拟机”这本巨牛的书上看来的,绝对有权威性。从jvm的结构图上可以看出来,jvm在实现的时候将属于它的内存分为五部分,其中程序代码(严格的说应当是字节码)是放在java栈的栈帧中,而对象是从堆中分配的,堆这个东西我看可以理解成“对象池”。程序和程序中需要用到的对象放在两个相对独立的区域中,那么程序怎么使用对象呢?答案是程序中真正使用对象的地方其实只是声明了一个对象的引用,也就是把堆中分配了的相应对象的地址放到引用中,栈和堆之间就是通过一个一个的引用来联系的。引用嘛,我理解就是一个指针常量,指针常量又是个什么东西呢?说白了,就是一个无符号整数,这个整数所表达的是引用对象的地址。好了,这下清楚了,不管是基本类型变量(int,float,double什么的)还是对象,相应的内存地址中存放的都是一个数(无符号整数,整数,浮点数等)。传递参数的时候传递的就是相应内存地址中的数,所以说“java函数是传值的”。当然,这个数对于基本类型和对象类型来说意义是不一样的,对于基本类型这个数就是其值本身,传递值的结果就是,改变新的变量的值不影响旧的变量的值;而对于对象来说这个数是它的地址,传递这个值就相当于传递了真实对象的引用,传递了引用或者说是地址的结果就是变化会全局可见,所以又可以说“java函数传递的参数是对象的引用”。
唔,松口气啦。经过上面这一小堆讨论,不难理解为什么java在传递参数时对于基本类型和对象表现不同:)
现在开始举例了,举网上搜来的例子,看看是不是比原来没有上面的解释的时候好理解一点?
public class TestRef
{ public static void main(String[] args) { ValueObject vo1 = new ValueObject("A", 1); System.out.println("after vo1: " + vo1.getName()); //=A changeValue1(vo1); System.out.println("after changeValue1: " + vo1.getName()); //=A1, changed changeValue2(vo1); System.out.println("after changeValue2: " + vo1.getName()); //=A1, changeValue2内部的赋值不会影响这里。 } /** * 使用vo1自身的函数对其内部数据进行改变是有效的,函数外可反映出来, * 因为这是对对象本身的操作
* 这种object称为可变的(mutable) * @param vo1 */ private static void changeValue1(ValueObject vo1) { vo1.setName("A1"); } /** * 在函数内给vo1重新赋值不会改变函数外的原始值,因为这种改变了引用的指向 * @param vo1 */ private static void changeValue2(ValueObject vo1) { vo1 = new ValueObject("B", 2); System.out.println("inside changeValue2: "+ vo1.getName()); //=B,赋值操作引起的结果变化仅在changeValue2内部有效 } } class ValueObject { public ValueObject() {} public ValueObject(String name, int id) { this.name = name; this.id = id; } private String name; private int id; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } java中对象的每个实例(就是对象)内存地址是唯一的,它一旦被创建,能够对这个地址进行操作的就是其本身,如果ValueObject类中没有public void setName之类的方法对这个类的实例中的数据进行修改的话,程序是没有任何别的方法可以修改ValueObject类的实例中的数据,这个就是java的封装特性。对于不提供修改内部数据的方法的类,我们称为不可变(immutable)的类。在函数中对传入的参数变量进行赋值操作,只能在函数范围内改变局部变量指向的引用地址,但是不会改变原始地址的内容。因此,在changeValue2(...)函数内部的vo1和函数外的vo1虽然名字相同,但是实际上是不同的实例变量,只不过指向了和函数外的vo1同样的地址,所以当我们用vo1=... 对其进行赋值的时候,只不过是把函数内的临时变量指向了新的地址,并没有改变原始vo1内存地址中的内容。这就是在运行changeValue2(...)之后,vo1的值在main范围内仍然没有被修改的原因。而changeValue1里面是调用的ValueObject本身的function来更改其内容,因此是原始内存地址中的数据被更改了,所以是全局有效的. 总结: 对于引用类型的传参也是传值的,传的是引用类型的值,其实就是对象的地址。 1. java参数传递值的。 2. java所有对像变量都是对像的引用。 不知道我解释清楚没,要是还是不能理解java参数传递的原理,强烈推荐看看“深入java虚拟机”一书的第五章,中文翻译的不是特别出色,可以看看英文原版,应该比中文版好懂:) May 26 约瑟夫问题的java实现讲讲这个Josephus游戏的故事先:据说著名的犹太史学家Josephus有过以下故事,在罗马人占领乔塔帕特后,39个犹太人与Josephu和他的朋友躲到一个洞中,39个犹太人决定宁愿死夜不愿被敌人俘虏,于是决定了一个自杀的方式,41个人排成一个圆圈,由第1个人开始报数,每报到第3人该人就必須自杀,然后再由下一个人重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假裝遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
今天又看到josephus问题觉了,总觉得会有一种简单的办法来解决这个问题,可以既不使用链表又不用struct和多维数组什么的,呵呵,结果真的找到了,下面这个就是了
public class Josephus
{ public static void main(String args[]) { if(args.length != 2) //处理参数数目不正确情况 { System.out.println("Please Input a number!"); return; } int i, j, n, m; n = Integer.parseInt(args[0]); m = Integer.parseInt(args[1]); if (n <= 0 || m <= 0) //处理参数值不正确的情况 { System.out.println("Paramter must bigger than zero!"); return; } int a[] = new int[n]; for (i = 0; i < n; i++) a[i] = i + 1; int k = 1; //标识处理第k个离开的人 i = -1; //数组下标,下一个为0,即第一个人 while (true) //k等于n表示只剩下一个人了 { for (j = 0; j < m;) //在圈中数m个人 { i = (i + 1) % n; if (a[i] >0) j++; //a[i] >0表示第i个人还没有离开 } if(k==n) break; System.out.println("No." + a[i] + " is out!"); a[i] = -1; //表示该人离开 k++; } System.out.println("No." + a[i] + " is the winner!"); } } 输入:
C:\Documents and Settings\circle\桌面>java Josephus 9 4
输出结果:
No.4 is out! No.8 is out! No.3 is out! No.9 is out! No.6 is out! No.5 is out! No.7 is out! No.2 is out! No.1 is the winner! 运行速度比较快,输入java Josephus 1,000,000 9(也就是说有一百万个人排成一圈,每隔9个出局一个),程序只需要两三秒钟就可以得到最后的winner:No. 963295,当然,要是输出"No." + a[i] + " is out!"这句就很花时间了。
ps:刚才在网上搜到一个更强的,c程序,只有8行!!!暴快!!!汗一下……
用的是倒推法,而不是向上面那样模拟游戏的方法。
#include <stdio.h> main() { int n, m, i, s=0; printf ("N M = "); scanf("%d%d", &n, &m); for (i=2; i<=n; i++) s=(s+m)%i; printf ("The winner is %d\n", s+1); } 求X个的质数,嗯,这个算法好简洁 算法P:
求X个不同的质数,PRIME[]为一个数组,存储所有求得的质数
P1. [初始化]PRIME[1]<-2, N<-3, J<-1(N跑遍所有奇数,因为只有奇数可以成为质数 的候选者;J用来标记至今已找到多少个质数)
P2. [N是质数]置J<-J+1, PRIME[J]<-N P3. [找到X个?]若J=X,则全部要求的质数找到,退出程序 P4. [增加N]置N<-N+2 P5. [K<-2]置K<-2(PRIME[K]将跑遍N的所有可能的质因子) P6. [K/PRIME[K]?]以PRIME[K]除K;设Q是商,R是余数,若R=0(因此N不是质数),转 向P4
P7. [PRIME[K]足够大了?]若Q<=PRIME[K],则转P2(在这种情况下,N必然是质数) P8. [增加K]K加1,并转P6 算法来自The Art of Programming,的确是大师,窥一斑而见全豹,就这么个小东西都能看出来人家的厉害之处,我以为求N是不是质数就要一个一个挨个除到N的平方根呢,这个方法可快多了。 May 25 交换变量a和b不引入变量c算法Change:
变量a和b交换,一般要引入第三个变量c:
c=a;
a=b;
b=c;
其实变量c没有什么实质性的作用,只是作为一个“中转站”出现。如果不想引入变量c而完成交换,可以通过以下方法实现: a = a + b; b = a - b; a = a - b; 或者使用位运算符“异或” a = a ^ b; b = a ^ b; a = a ^ b; May 22 Java虚拟机结构图《深入java虚拟机》看完了,挺难理解的,不是很懂,嘿嘿,这个自然了。虽然他的大部分思想还是很自然也挺符合人们的思维习惯的,但是毕作为一个先进的技术它的深层原理和具体技术还是不那么显而易见的,远远超出了我仅有的理解能力:P不懂的实在比不懂的多得多,不过上面那幅图倒是让我明白了点JVM底层的实现机制,受益匪浅。很多以前很诧异理解不了的语法和概念在仔细研究了这幅图之后变得清晰起来。
执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本的CPU运算,例如,把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。
由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为: 第一个字节*256+第二个字节字节码。
指令流一般只是字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。
未完,待续 May 21 多处理器系统中Cache一致性解决方案PART 1 软件解决方案
软件解决方案是由编译器和操作系统解决一致性问题。这种解决方案将开销由运行时转到了编译时,设计复杂性由硬件转到软件。然而,软件方法倾向于使用保守判定,因为软件方法通过分析代码确定缓存共享变量的安全时段,任何共享变量只要处于非安全期就不允许任何存储器缓存它。当然这样就造成了不必要的开销,这种开销的大小是通过高速缓存利用率差来反映的。
PART 2 硬件解决方案
硬件解决方案在运行时动态识别潜在问题,提高高速缓存的利用率,并且硬件方案可以确保高速缓存一致性的问题对程序员透明。
硬件解决方案通常有两种:目录协议和监听协议。
在目录协议中, 所有的请求都对照目录进行检查,需要执行某些传输操作。而存放目录的硬件设备就有可能成为整个系统的瓶颈。通常这种协议在采用复杂互联方案的大型系统中有效。 监听协议把高速缓存一致性责任分布到每个高速缓存控制器上,每个高速缓存通过特定得方式识别被共享的数据块,并且将其更新过的操作通知到其他高速缓存。这种协议适合于居于总线的多处理器系统。
MESI协议属于监听协议中的写无效协议。MESI协议MESI协议是“修改(modified)、排它(exclusive)、共享(shared)、无效(invalid)”四个功能的简称,每个缓存模块必须按照MESI协议完成这4个独立的功能。
● 修改:如果某一内存数据区记录只存在于一个CPU缓存中,那么此CPU可以对此数据进行修改,而无需通知其他CPU。 ● 排它:同一时间只能有一个CPU对同一内存数据区进行修改或者更新。 ● 共享:如果某一内存数据区记录存在于多个CPU缓存中,那么CPU对此数据修改后,必须通知其他CPU。 ● 无效:一旦CPU对缓存数据访问失效,那么就必须重新从内存中读取数据。 这种协议要求在任何时刻都可以有多个读者,但至多有一个写者。当请求写操作时,其他缓存中的这个数据块被置为失效状态。在其他处理器访问该数据块之前,执行写操作的处理器以独占的方式访问该数据块。
另外还有一种写更新的协议,多个处理器读,多个处理器写,更新的内容被分发到所有其他处理器。这种方式的代价比较高。 垃圾收集的算法Java的堆是一个运行时数据区,类的实例(对象)从中分配空间。Java虚拟机(JVM)的堆中储存着正在运行的应用程序所建立的所有对象,这些对象通过new、newarray、anewarray和multianewarray等指令建立,但是它们不需要程序代码来显式地释放。一般来说,堆的是由垃圾回收来负责的,尽管JVM规范并不要求特殊的垃圾回收技术,甚至根本就不需要垃圾回收,但是由于内存的有限性,JVM在实现的时候都有一个由垃圾回收所管理的堆。垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。
垃圾收集的意义
我最不喜欢“意义”这两个字了,可能是政治背多了,一见“意义”脑袋都大成俩了,垃圾收集当然是有意义的,没意义设计JVM的人也就不会多次一举了。不过说说这个垃圾回收是干吗使的也有点用,我勉强东拼西凑了这点东西,凑合看看。
在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象(C++是我的nightmare,写C的程序的时候我就经常因为这类问题搞得狼狈不堪,我始终对会用C++的人高山仰止);而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾收集意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾收集也可以清除内存记录碎片。由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。垃圾收集能自动释放内存空间,减轻编程的负担。GC使Java 虚拟机具有一些优点。首先,它能使编程效率提高。在没有垃圾收集机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾收集机制可大大缩短时间。其次是它保护程序的完整性, 垃圾收集是Java语言安全性策略的一个重要部份。 当然,使用这么省事的功能是需要付出代价滴,垃圾收集本身就挺耗费资源的,它的开销影响程序性能(鱼和熊掌不能兼得,反正一般情况下也不用Java做实时性很强的程序,慢一点我忍了)。Java虚拟机必须追踪运行程序中有用的对象, 而且最终释放没用的对象。这一个过程需要花费处理器的时间。其次垃圾收集算法的不完备性,早先采用的某些垃圾收集算法就不能保证100%收集到所有的废弃内存。随着垃圾收集算法的不断改进以及软硬件运行效率的不断提升,这些问题没准一夜之间迎刃而解。
垃圾收集的算法分析
Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾收集算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就量正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。
几个常用的算法
1、 引用计数法(Reference Counting Collector) 引用计数法是唯一没有使用根集的垃圾回收得法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1。当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量 ,计数器加1,而每次现有对象出了作用域生,计数器减1。 2、 tracing算法(Tracing Collector) tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器. 3、 compacting算法(Compacting Collector) 为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来 的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。 4、 coping算法(Coping Collector) 该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于coping算法的垃圾收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。 5、 generation算法(Generational Collector) stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代(generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。 6、 adaptive算法(Adaptive Collector) 在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。
几点补充
嘿嘿,下面这特点总结得够拽,我是写不出来滴,这要相当的总结能力,文学水平。人家说中国的科研人员不一定能成为科学家,但肯定能成为文学家。
(1) 垃圾收集发生的不可预知性:由于实现了不同的垃圾收集算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的垃圾收集一样,等到内存消耗出现极限时发生,这与垃圾收集器的选择和具体的设置都有关系。 (2) 垃圾收集的精确性:主要包括2 个方面:(a)垃圾收集器能够精确标记活着的对象;(b)垃圾收集器能够精确地定位对象之间的引用关系。前者是完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。而后者则是实现归并和复制等算法的必要条件。所有不可达对象都能够可靠地得到回收,所有对象都能够重新分配,允许对象的复制和对象内存的缩并,这样就有效地防止内存的支离破碎。 (3) 现在有许多种不同的垃圾收集器,每种有其算法且其表现各异,既有当垃圾收集开始时就停止应用程序的运行,又有当垃圾收集开始时也允许应用程序的线程运行,还有在同一时间垃圾收集多线程运行。 (4) 垃圾收集的实现和具体的JVM 以及JVM的内存模型有非常紧密的关系。不同的JVM 可能采用不同的垃圾收集,而JVM 的内存模型决定着该JVM可以采用哪些类型垃圾收集。现在,HotSpot 系列JVM中的内存系统都采用先进的面向对象的框架设计,这使得该系列JVM都可以采用最先进的垃圾收集。 (5) 随着技术的发展,现代垃圾收集技术提供许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这就使得根据不同的应用环境获得最优的应用性能成为可能。
写程序时要注意的东西,这个才是我最关心的东西,吼吼: (1) 不要试图去假定垃圾收集发生的时间,这一切都是未知的。比如,方法中的一个临时对象在方法调用完毕后就变成了无用对象,这个时候它的内存就可以被释放。 (2) Java中提供了一些和垃圾收集打交道的类,而且提供了一种强行执行垃圾收集的方法--调用System.gc(),但这同样是个不确定的方法。Java 中并不保证每次调用该方法就一定能够启动垃圾收集,它只不过会向JVM发出这样一个申请,到底是否真正执行垃圾收集,一切都是个未知数。 (3) 挑选适合自己的垃圾收集器。一般来说,如果系统没有特殊和苛刻的性能要求,可以采用JVM的缺省选项。否则可以考虑使用有针对性的垃圾收集器,比如增量收集器就比较适合实时性要求较高的系统之中。系统具有较高的配置,有比较多的闲置资源,可以考虑使用并行标记/清除收集器。 (4) 关键的也是难把握的问题是内存泄漏。良好的编程习惯和严谨的编程态度永远是最重要的,不要让自己的一个小错误导致内存出现大漏洞。 (5) 尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null,暗示垃圾收集器来收集该对象,还必须注意该引用的对象是否被监听,如果有,则要去掉监听器,然后再赋空值。结束语 一般来说,Java开发人员可以不重视JVM中堆内存的分配和垃圾处理收集,但是,充分理解Java的这一特性可以让我们更有效地利用资源。同时要注意finalize()方法是Java的缺省机制,有时为确保对象资源的明确释放,可以编写自己的finalize方法 Java程序内存泄露问题1. java中的“ 垃圾收集器”的原理 GC的功能是当发现一个对象不再需要被使用时,将它删除,同时将其占有的内存归还给JVM或系统。那么,GC又是如何来判断一个对象已经不再需要呢?它的标准是当该对象没有被引用(Reference)就表明是不再被需要的。GC会从根节点开始遍历被Java应用程序的整个生命周期中始终存在的那些类,所有被引用的对象将被标记,没有标记的当作垃圾被收集。所以,当内存足够时,JVM 是不会启动GC 的,也不会暴露内存泄露问题的严重程度。
2. 内存是如何被泄露的 例:Class_A首先生成了一个实例A,并且该实例在整个程序中都将被使用。然后程序生成了Class_B的一个实例,该实例可能只是被使用过一次两次。如下: Class_A = new Class_A(); … Class_B = new Class_B(); A.referB = B; …
如果A使用完B以后没有清空该引用,则将一直保持对B的引用,即使以后再也不需要B了。在这种情况下B是不可能被删除的。 解决问题十分简单:在应用程序中增加清除引用B的语句:A.referB = null;,即可。现在GC当然会轻松地找到泄露内存的B并收集由它占着的许多重复内存,从而解决异常终止的问题。
3 以上的话总结起来就是“概括地说,内存托管语言中的内存泄漏产生的主要原因:保留下来却永远不再使用的对象.
嘿嘿,鼓励一下自己,很努力的写这篇帖子,尤其是那个小图,画的很辛苦的说,感觉是把问题说清楚了,鼓掌鼓掌^_^ java upcastingabstract class HumanBeing { String name; public HumanBeing() { show(); } public abstract void show(); } class Man extends HumanBeing { public Man(String name) { this.name=name; } public void show() { System.out.println("I am a man..."); System.out.println("My name:"+this.name); } } class Woman extends HumanBeing { public Woman(String name) { this.name=name; } public void show() { System.out.println("I am a woman..."); System.out.println("My name:"+this.name); } } public class UpCasting { public static void showCharacteristics(HumanBeing human) { human.show(); } public static void main(String[] args) { showCharacteristics(new Man("liansen")); showCharacteristics(new Woman("shorwan")); } }
执行结果: >java UpCasting I am a man... My name:liansen I am a woman... My name:shorwan April 25 还有弱引用这个估计也是用不到了啊。java的东西高深呵。
Java 理论与实践: 用弱引用堵住内存泄漏
虽然用 Java™ 语言编写的程序在理论上是不会出现“内存泄漏”的,但是有时对象在不再作为程序的逻辑状态的一部分之后仍然不被垃圾收集。本月,负责保障应用程序健康的工程师 Brian Goetz 探讨了无意识的对象保留的常见原因,并展示了如何用弱引用堵住泄漏。 全局 Map 造成的内存泄漏 无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object)相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息,如清单 1 中的 SocketManager 类所示: 清单 1. 使用一个全局 Map 将元数据关联到一个对象 public class SocketManager { SocketManager socketManager; 这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket 和 User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket 和 User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。 找出内存泄漏 程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。 有工具可以利用 GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具(请参阅 参考资料)。观察 GC 之后堆大小的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用分为两部分:baseline 使用和 current load 使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的内存使用,current load 使用是在处理请求过程中使用的、但是在请求处理完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一个稳定的内存使用水平。如果在应用程序已经完成了其初始化并且负荷没有增加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了生成的对象。 清单 2 展示了一个有内存泄漏的程序。MapLeaker 在线程池中处理任务,并在一个 Map 中记录每一项任务的状态。不幸的是,在任务完成后它不会删除那一项,因此状态项和任务对象(以及它们的内部状态)会不断地积累。 清单 2. 具有基于 Map 的内存泄漏的程序 public class MapLeaker { private enum TaskStatus { NOT_STARTED, STARTED, FINISHED }; private class Task implements Runnable { public void run() { public Task newTask() {
确信有了内存泄漏后,下一步就是找出哪种对象造成了这个问题。所有内存分析器都可以生成按照对象类进行分解的堆快照。有一些很好的商业堆分析工具,但是找出内存泄漏不一定要花钱买这些工具 —— 内置的 hprof 工具也可完成这项工作。要使用 hprof 并让它跟踪内存使用,需要以 -Xrunhprof:heap=sites 选项调用 JVM。 清单 3 显示分解了应用程序内存使用的 hprof 输出的相关部分。(hprof 工具在应用程序退出时,或者用 kill -3 或在 Windows 中按 Ctrl+Break 时生成使用分解。)注意两次快照相比,Map.Entry、Task 和 int[] 对象有了显著增加。 请参阅 清单 3。 清单 4 展示了 hprof 输出的另一部分,给出了 Map.Entry 对象的分配点的调用堆栈信息。这个输出告诉我们哪些调用链生成了 Map.Entry 对象,并带有一些程序分析,找出内存泄漏来源一般来说是相当容易的。 清单 4. HPROF 输出,显示 Map.Entry 对象的分配点 TRACE 300446: 弱引用来救援了 SocketManager 的问题是 Socket-User 映射的生命周期应当与 Socket 的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从 JDK 1.2 开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏 —— 利用弱引用。 弱引用是对一个对象(称为 referent)的引用的持有者。使用弱引用后,可以维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable)。) WeakReference 的 referent 是在构造时设置的,在没有被清除之前,可以用 get() 获取它的值。如果弱引用被清除了(不管是 referent 已经被垃圾收集了,还是有人调用了 WeakReference.clear()),get() 会返回 null。相应地,在使用其结果之前,应当总是检查 get() 是否返回一个非 null 值,因为 referent 最终总是会被垃圾收集的。 用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样 —— 如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。 弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合 —— 这就是 SocketManager 类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能被回收,WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个对象被垃圾收集。清单 5 给出了 WeakHashMap 的 get() 方法的一种可能实现,它展示了弱引用的使用: 清单 5. WeakReference.get() 的一种可能实现 public class WeakHashMap<K,V> implements Map<K,V> { private static class Entry<K,V> extends WeakReference<K> public V get(Object key) { 调用 WeakReference.get() 时,它返回一个对 referent 的强引用(如果它仍然存活的话),因此不需要担心映射在 while 循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法 —— 一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到解释。 在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回 null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。 用 WeakHashMap 堵住泄漏 在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清单 6 所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。 清单 6. 用 WeakHashMap 修复 SocketManager public class SocketManager { 引用队列 WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry 和值对象也不会被收集。 可以通过周期性地扫描 Map,对每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映射。但是如果 Map 有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列 的作用。 引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是 referent)就在引用清除后加入 到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 —— polled、timed blocking 和 untimed blocking。) WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。清单 7 展示了 expungeStaleEntries() 的一种可能实现。用于存储键-值映射的 Entry 类型扩展了 WeakReference,因此当 expungeStaleEntries() 要求下一个失效的弱引用时,它得到一个 Entry。用引用队列代替定期扫描内容的方法来清理 Map 更有效,因为清理过程不会触及活的项,只有在有实际加入队列的引用时它才工作。 清单 7. WeakHashMap.expungeStaleEntries() 的可能实现 private void expungeStaleEntries() { Entry<K,V> prev = getChain(hash); 结束语 弱引用和弱集合是对堆进行管理的强大工具,使得应用程序可以使用更复杂的可及性方案,而不只是由普通(强)引用所提供的“要么全部要么没有”可及性。下个月,我们将分析与弱引用有关的软引用,将分析在使用弱引用和软引用时,垃圾收集器的行为 软引用看“java虚拟机”没看懂什么叫软引用,郁闷到了,网上搜了一篇。这玩艺估计一时半会儿我是用不到了。
软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候才回收这类内存,因此在内存足够的时候,他们通常不被回收。另外,这些引用对象还能保证在Java 抛出OutOfMemory异常之前,被设置为null。他可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory异常。
下面是软引用的实现代码:
1/** *//** 2 * 该类演示了Soft Reference的应用 3 * 版权 本文版权属Java天下 4 */ 5package cn.javatx; 6 7import java.lang.ref.SoftReference; 8 9/** *//** 10 * @author ajie 11 */ 12 13public class softReference { 14 15 /** *//** 16 * @param args 17 */ 18 public static void main(String[] args) { 19 // TODO Auto-generated method stub 20 A a = new A(); 21 22 //使用a 23 a.test(); 24 25 //使用完了a,将它设置为soft引用类型,并且释放强引用 26 SoftReference sr = new SoftReference(a); 27 a = null; 28 29 //下次使用 30 if (sr != null) { 31 a = (A)sr.get(); 32 a.test(); 33 } else { 34 //GC由于低内存,已释放a,因此需要重新装载 35 a = new A(); 36 a.test(); 37 a = null; 38 sr = new SoftReference(a); 39 } 40 } 41 42} 43 44class A { 45 public void test() { 46 System.out.println("Soft Reference test"); 47 } 48} 49
软引用技术的引进使Java应用可以更好的管理内存,稳定系统,防止系统内存溢出,避免系统崩溃。因此在处理一些占用内存大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。但事物总带有两面性的,有利也有弊,在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。
|
|
|