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",原因如下:
  1. 基本类型方法传参是以复制值的形式,即change中的i变量复制给changeParam的i,但实际上changeParam的i拥有change的i的值,但changeParam中的i无论怎么变则不会改变change的i的值;
  2. String虽然是引用类型,但是String自己本身的特性是不能被改变,所以要改变String的值的话,则在java里面是生成一个新的String,再把此值赋值给str,所以str这种参数和方传参并没有什么关系,就算是在一个方法内也是如此。
  3. 引用类型参数情况分为2种:
    1. sb引用,在changeParam中sb修改属性后,在change中的sb也一同跟着改变,原因是因为change方法和changeParam方法中的sb引用指向的是同一个对象,所以它们修改对象时,自然会在该对象的所有引用中体现。
    2. 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)”
停止-复制:
    顾名思义,这种方式就是先暂停程序的运行,避免新的垃圾产生,也能避免意外问题,通过遍历引用链条能找到所有的"活"的对象,就把所有存活的对象从当前堆复制到另一堆,当对象被复制到新堆时,它们时一个挨着一个,保持紧凑排列,并且修正原本的引用重新指向新堆里面原本对应的对象地址。而没有被复制的都是垃圾,一下子清理掉。
此方式的缺点如下
  1. 得有两个完全隔离的堆,得在这两个堆之间来回捣腾,即假设实际运行时需要100个空间,那么还得预留此空间单纯是用来捣腾,效率低下,
  2. 复制,当程序趋于稳定,没有多少垃圾产生,甚至没有垃圾。尽管如此,复制式回收器仍会将所有内存一处复制到另一处,假如有100个对象,其中有5个是垃圾对象,那么还是得把95个移动到另一个堆中,只为了清除那5个垃圾。性能浪费
标记-清扫(mark-and-sweep):
    为了避免复制式的少了量垃圾时的性能资源浪费,一些java虚拟机在这种没有多少垃圾时切换另一种工作模式,称为“标记-清扫(mark-and-sweep)”,一般情况下,标记-清扫方式速度很慢,但处理少量垃圾时速度很快。标记-清扫也是必须程序先暂停。
    同样是通过遍历引用链条,遍历时找到所有存活的对象,每找到一个对象,就给此对象设一个标记,所有标记工作完成的时候,开始清理工作,清理时过程中,没有标记的需都被释放,不会发生任何复制动作。所以剩下的堆空间不是连续的,垃圾回收器希望得到连续空间的话,就得重新整理剩下的对象。
总结 停止-复制和标记-清扫:
    举个很不恰当的例子,就比如你要打扫家里的卫生时,你让这个房间停止住人,如果房间垃圾比较多些,就把不是垃圾的东西搬到另一个房间里面并规规矩矩的排列好,然后一股脑得把这个原房间垃圾全清理掉,等新房间垃圾也多的时候再搬回来,来回折腾。这就是"停止-复制",
    而如果垃圾比较少,就用不着把不是垃圾的东西把到另一个房间了,因为总共才几个垃圾,搬到其他房间那空垃圾就扫干净了,则就是把不是垃圾的和是垃圾的东西区分开,然后把垃圾清扫出去,这是“标记-清扫”,当然这个例子很不恰当。
3.3.4 自适应,分代的
    堆内存是虚拟机管理内存的最大的一块,垃圾回收器执行时都要遍历堆内存的所有的对象,遍历这些对象所花费的代价太大,严重影响GC的效率。内存分代就是为了这个目的。它的作用就是把这些对象分类,哪些不用每次都遍历,哪些需要频繁遍历。
    虚拟机把内存分配新生代,老年代,永久代。新建的对象会在新生代中分配内存,多次回收仍存活下来的对象存放在老年代,而静态属性和类信息存放在永久代中,新生代的对象存活时间短,在新生代区域中频繁GC,老年代对象生命周期长,内存回收频率较低,永久代一般不进行垃圾回收。根据不同的年代的特点采用不同的收集方式,提升收集效率。

4 总结:

    引用和对象的关系,以及对象初始化都是java程序中非常需要注重的点,像上面所说java虚拟机判断哪些是活的对象都是通过遍历引用链条的方式,但仅仅依赖引用来判断哪些对象是需要清理,哪些对象是不需要清理是不够得。例如当内存吃紧时,而且引用都绑定着对象,那么就没有多少垃圾对象可以清理,但又内存资源紧张,那么我们需要区分强引用,软引用,弱引用和虚引用了。通过此方式清理些可以被清理的对象(例如缓存对象数据等等)。这些以后笔记会详情描述。

以上是关于02-方法传参和初始化与垃圾回收清除的主要内容,如果未能解决你的问题,请参考以下文章

python 开发 -- 02垃圾回收机制

JVM垃圾回收

CMS垃圾回收器

直通BAT必考题系列:JVM的4种垃圾回收算法垃圾回收机制与总结

直通BAT必考题系列:JVM的4种垃圾回收算法垃圾回收机制与总结

直通BAT必考题系列:JVM的4种垃圾回收算法垃圾回收机制与总结