02-方法传参和初始化与垃圾回收清除
Posted likejiu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了02-方法传参和初始化与垃圾回收清除相关的知识,希望对你有一定的参考价值。
1.方法参数传值
1.1 方法传参
方法参数分为三种:1,基本类型; 2,String类型;3,引用类型。
实例如下:
public void changeParam(int i,String str,StringBuilder sb,StringBuilder sb2){ i=1; str="1"; sb.append("1"); sb2=new StringBuilder("1"); } @Test public void change(){ int i = 0; String str = "0"; StringBuilder sb = new StringBuilder("0"), sb2 = new StringBuilder("0"); changeParam(i, str, sb,sb2); System.out.format("i:%-3d str:%-3s sb:%-3s sb2:%-3s", i, str, sb, sb2); } 结果: i:0 str:0 sb:01 sb2:0
执行change方法后,i依然是0,str依然是0,sb则变成了"01",原因如下:
- 基本类型方法传参是以复制值的形式,即change中的i变量复制给changeParam的i,但实际上changeParam的i拥有change的i的值,但changeParam中的i无论怎么变则不会改变change的i的值;
- String虽然是引用类型,但是String自己本身的特性是不能被改变,所以要改变String的值的话,则在java里面是生成一个新的String,再把此值赋值给str,所以str这种参数和方传参并没有什么关系,就算是在一个方法内也是如此。
- 引用类型参数情况分为2种:
- sb引用,在changeParam中sb修改属性后,在change中的sb也一同跟着改变,原因是因为change方法和changeParam方法中的sb引用指向的是同一个对象,所以它们修改对象时,自然会在该对象的所有引用中体现。
- sb2虽然调用changeParam方法时也是传递的是StringBuilder的引用,但是在changeParam中sb2重新指向新的对象,所以此时changeParam和change中的引用指向的不再是同一个对象了,所以它们之间的改变是互不影响的。
1.2 可变参数可会发生的问题
在java中方法传值使用不当可能会出问题,如可参数可能会发生的问题,先了解下方法的基本概念,在java中调用方法时,通过类型下的方法名+参数类型来区分调用的是哪个方法,如果同一个类下的方法名一样,那么方法参数和参数数量则就成了判断方法的区别;调用时后台实现方式是通过RTTI机制,关于RTTI以后会有笔记会将具体什么情况。
可变参数方法是接收同一种类型参数但没有固定具体数量,如xx(Integer ... args),Integer就是可以传入多个,接收时以数组形式接收。如下:
//参数类型后面是3个省略后就是可变参数 public void changeParam(String ... args){ System.out.println(Arrays.deepToString(args)); System.out.println(args[0]); } @Test public void changeParamTest(){ changeParam("a"); changeParam("a","b","c"); } 结果: [a] a [a, b, c] a
可变参数可能会出现的问题如下:
/** * 可变参数可能会发生的问题 * 当changeParam方法参数一个是Integer args一个是Integer ... args,编译器是通过的 * 当程序调用changParam方法但只有一个参数,而我此时是需要调用的是带有Integer ... args方法 * 但是程序会认为是我调用的Integer args方法,并执行该方法 * @param args */ public void changeParam(Integer args){ System.out.println("Integer:"+args); } public void changeParam(Integer ...args){ System.out.println("Integer...:"+args); } //解决方式 public void changeParam(Long type,Integer ...args){ for (Integer val:args) { System.out.println("int:"+val); } } @Test public void changeParamRun(){ changeParam(1L,1); }
本示例changeParam(Integer args)和changeParam(Integer ...args)方法是不同的方法,但原本要调用(Integer ...args)方法,但只传1个值的情况下就会变成执行(Integer args)方法,这就是很严重的问题,执行的结果和目的方法不一致。最佳的解决方案是多设定参数,设定表示区分。如(Long type,Integer ...args),通过参数类型区分方法。
2.程序初始化流程
public class HelloA { public HelloA() {System.out.println("HelloA");} { System.out.println("I‘m A class"); } static { System.out.println("static A"); } } public class HelloB extends HelloA { public HelloB() { System.out.println("HelloB"); } { System.out.println("I‘m B class"); } static { System.out.println("static B"); } } public class Test { public static void main(String[]args){ new HelloB(); } } 结果: static A static B I‘m A class HelloA I‘m B class HelloB
结果流程如下:
父类静态区域(静态变量和静态块内容)》》当前类的静态区域(静态变量和静态块内容)》》父类{}代码块和成员变量》》父类构造方法》》子类{}代码块和成员变量》》子类构造方法
静态区域只执行初始化一次,无论创建多少对象都只执行一次。
3.java垃圾回收机制
程序员都了解初始化的重要性,同时也应该了解清理工作,java的一大优点就是帮助我们处理初始化和运行时给程序分配资源,并且在程序资源吃紧时清理资源,保证程序能够健康的运行。庆幸的时这些事情java已经帮我们做了,这可是“帮了大忙",我们在刚上手java代码时不需要考虑如何分配内存,如何清理内存,只需要关注我们自身的程序的内容即可。当然为了以后写出更优秀的代码。了解内存分配和清理内存还是很有必要的。
3.1 finalize方法的作用
垃圾回收器会回收通过new生成放在堆里的对象,但对于一些特殊生成的对象资源始终都不会被清理,例如静态区域之类,那么可以通过finalize进行清理,finalize方法是垃圾回收器执行时先调用finalize方法,然后在下次垃圾回收进行垃圾回收。在finalize方法中我们可以将本来不会被清理的东西手动打上标记(例如静态变量对象不会被垃圾回收器回收掉,可以在finalize中把此变量置为null),或者将一些强引用的对象设置为null,让垃圾回收器再下次执行时把这些资源回收掉。
代码 模拟实现方式如下:
class HandlerTest{ static HandlerTest staticBl=new HandlerTest();//设置静态变量 boolean checkedOut=false; /** * 覆写finalize方法 * @throws Throwable */ @Override protected void finalize() throws Throwable { //控制清理条件,且清理内容 if(checkedOut){ System.out.println("执行清理"); /** * java垃圾回收器只回收new出来的对象放在堆里面, * 而静态区域是不做处理,那么可以通过finalize方法将其作为处理, * 并再下一次垃圾回收时回收 */ staticBl=null; super.finalize(); flag=false; } } @Override public String toString() { return "staticBl"; } } static boolean flag=true; @Test public void runGC(){ while (flag){ HandlerTest handler=new HandlerTest(); handler.checkedOut=true; HandlerTest handler2=new HandlerTest(); System.out.println(HandlerTest.staticBl); System.gc(); } } 结果: staticBl staticBl staticBl staticBl staticBl staticBl 执行清理 staticBl 执行清理 执行清理 执行清理 执行清理 执行清理
如上示例正常垃圾回收时都不会处理HandlerTest的staticBl变量,通过覆写finalize()方法当垃圾回收时,使其为null,以便下次垃圾回收将其回收掉。但是finalize方法通过垃圾回收器调用,把一些正常不会被垃圾回收器回收掉且不再使用的对象放在finalize中让其清理,但是垃圾回收器是在垃圾回收器执行的时候执行的,所以finalize执行时机是不确定的;因为java虚拟机未面临内存耗尽的情况下,它是不会浪费时间去执行垃圾回收以恢复内存的。一般情况下,finalize方法尽量不用。
3.2 内存分配
生成对象就要为对象分配内存, java分配内存方式就是就像队列形式,按照《java编程思想》说法是像一个传送带,每分配一个对象,它就会向前移动一个,java的分配内存的方式和速度要比C快的多,但并非完全像传送带,当内存资源将耗尽时,java垃圾回收器则帮助整理内存,假设以一共有10个空间,假设依次生成了8个,但是8个中间有3个空间已经不用了被标识为垃圾,那么如果还继续生成的话,只能再生成2个,那么最多只有7个有效的(8-3+2),这3个已经是无用的空间了,空间越多浪费的空间越多。
那么整理传送带的工作也是由java垃圾回收器实现的,目的是为了垃圾回收会将无用的回收掉,就是把上面那3个标识为垃圾的空间,并整理堆中对象使其紧凑排列,使其分配的内存更多,分配的效率更高。所以说垃圾回收器对于提高对象的创建速度,具有显著的效果。
3.3 垃圾回收如何工作
垃圾回收最重要的是判断哪些对象是垃圾对象(不再使用的对象),哪些对象是正在使用对象,把垃圾对象清理掉,把还在使用的对象保留下来。判断依据就是对象释放还被引用,如果对象没有被引用绑定,则说明此对象已经没有可能再被使用了,则就是垃圾对象。下面只是简单说明下,java的垃圾回收器是非常复杂的功能,以后有机会再深入了解。
3.3.1 引用记数技术
java并不是使用引用记数技术,但可以简单了解下,什么是引用记数技术?一个对象可能被多个引用绑定,每绑定一个引用则引用计数器加1,对象的引用离开作用域或者被设置为null时引用减1,当为0时表示该对象已经没有任何引用绑定了。等同于垃圾对象了。所以当记数值为0时,立即释放对象。
虽然管理引用记数的开销不大,但每次生成引用绑定对象都有执行,这种开销在整个程序生命周期中将持续发生。但整个方式有个缺陷,如果对象之间存在循环引用,就可能出现“对象应该被回收,但引用计数却不为零”,那么就不能回收掉。
3.3.2 遍历引用链条追踪对象
思路是这样的:任何“活”的对象,一定能够追溯到其存活在堆栈或静态存储区之中的引用,因此,如果从堆栈和静态存储区开始,遍历所有引用,就能找到其绑定的所有对象,例如找到某个引用绑定的对象后,再追踪该对象包含的所有引用的对象。等同于树状的金字塔,从上面一层一层的遍历,找到所有的“活的”对象。既然找到的都是活的对象,反过来没有找到的就是死的对象,因此就可以被自动回收掉了
3.3.3 区分活的对象和死的对象后,垃圾回收器是如何清理的?
不同的虚拟机版本有多种处理方式,有一种做法叫做“停止-复制(stop-and-copy)”
停止-复制:
顾名思义,这种方式就是先暂停程序的运行,避免新的垃圾产生,也能避免意外问题,通过遍历引用链条能找到所有的"活"的对象,就把所有存活的对象从当前堆复制到另一堆,当对象被复制到新堆时,它们时一个挨着一个,保持紧凑排列,并且修正原本的引用重新指向新堆里面原本对应的对象地址。而没有被复制的都是垃圾,一下子清理掉。
此方式的缺点如下:
- 得有两个完全隔离的堆,得在这两个堆之间来回捣腾,即假设实际运行时需要100个空间,那么还得预留此空间单纯是用来捣腾,效率低下,
- 复制,当程序趋于稳定,没有多少垃圾产生,甚至没有垃圾。尽管如此,复制式回收器仍会将所有内存一处复制到另一处,假如有100个对象,其中有5个是垃圾对象,那么还是得把95个移动到另一个堆中,只为了清除那5个垃圾。性能浪费
为了避免复制式的少了量垃圾时的性能资源浪费,一些java虚拟机在这种没有多少垃圾时切换另一种工作模式,称为“标记-清扫(mark-and-sweep)”,一般情况下,标记-清扫方式速度很慢,但处理少量垃圾时速度很快。标记-清扫也是必须程序先暂停。
同样是通过遍历引用链条,遍历时找到所有存活的对象,每找到一个对象,就给此对象设一个标记,所有标记工作完成的时候,开始清理工作,清理时过程中,没有标记的需都被释放,不会发生任何复制动作。所以剩下的堆空间不是连续的,垃圾回收器希望得到连续空间的话,就得重新整理剩下的对象。
总结 停止-复制和标记-清扫:
举个很不恰当的例子,就比如你要打扫家里的卫生时,你让这个房间停止住人,如果房间垃圾比较多些,就把不是垃圾的东西搬到另一个房间里面并规规矩矩的排列好,然后一股脑得把这个原房间垃圾全清理掉,等新房间垃圾也多的时候再搬回来,来回折腾。这就是"停止-复制",
而如果垃圾比较少,就用不着把不是垃圾的东西把到另一个房间了,因为总共才几个垃圾,搬到其他房间那空垃圾就扫干净了,则就是把不是垃圾的和是垃圾的东西区分开,然后把垃圾清扫出去,这是“标记-清扫”,当然这个例子很不恰当。
3.3.4 自适应,分代的
堆内存是虚拟机管理内存的最大的一块,垃圾回收器执行时都要遍历堆内存的所有的对象,遍历这些对象所花费的代价太大,严重影响GC的效率。内存分代就是为了这个目的。它的作用就是把这些对象分类,哪些不用每次都遍历,哪些需要频繁遍历。
虚拟机把内存分配新生代,老年代,永久代。新建的对象会在新生代中分配内存,多次回收仍存活下来的对象存放在老年代,而静态属性和类信息存放在永久代中,新生代的对象存活时间短,在新生代区域中频繁GC,老年代对象生命周期长,内存回收频率较低,永久代一般不进行垃圾回收。根据不同的年代的特点采用不同的收集方式,提升收集效率。
4 总结:
引用和对象的关系,以及对象初始化都是java程序中非常需要注重的点,像上面所说java虚拟机判断哪些是活的对象都是通过遍历引用链条的方式,但仅仅依赖引用来判断哪些对象是需要清理,哪些对象是不需要清理是不够得。例如当内存吃紧时,而且引用都绑定着对象,那么就没有多少垃圾对象可以清理,但又内存资源紧张,那么我们需要区分强引用,软引用,弱引用和虚引用了。通过此方式清理些可以被清理的对象(例如缓存对象数据等等)。这些以后笔记会详情描述。