Java 初始化与清理

Posted 莫西里

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 初始化与清理相关的知识,希望对你有一定的参考价值。

一、介绍

程序在运行过程中,可能因为开发人员忘记给变量进行初始化导致程序出现错误,也可能因为无法释放内存造成内存泄露最终导致大量内存被占用,程序被动终止。因此在Java类或者对象的生命期间,变量(包括静态变量)的初始化以及对象不在使用时的内存回收也决定这程序的健壮性等多个方面。
初始化:初始化只是类或者对象在使用前,对其属性/变量进行初始化一个值。
清理:是指当一个对象使用后,对其占用的资源进行清理,及时释放所占用的资源。

二、详情

1、构造器

构造器从定义上来说,其实就是类的构造函数。构造器的出现是为了能够使对象再被使用前,对象被正确的初始化而出现的。也即是构造器的主要作用就是:初始化类的对象,初始化对象的属性。
构造器有两种,默认构造函数/无参构造函数和用户自定义的构造函数。
(1)无参构造函数
无参构造器只是没有参数的构造函数,当用户定义一个类,但是没有声明构造函数时,编译器就会自动各给类添加一个无参构造函数,当用户定义了构造函数时,编译器就不会自动添加构造器。

public class Fifth1 
    private int level;
    public Fifth1()
        this.level=0;
    

(2)用户定义的构造器

public class Fifth1 
    public int level;
    public Fifth1(int level)
        this.level=level;
    

构造器有几点要弄清楚:
(1)构造器函数类的名称是相同的,大小写也是相同的,构造器函数不遵循驼峰法的规则
(2)构造函数运行完成后,不返回任何值(不是void),这个意思是指,不能使用return关键字
(3)在初始化对象时,返回对象地址的是new关键字,而不是构造函数本身。
(4)默认构造器是在用户没有定义的情况下才会有,如果用户定义了非无参构造器,当使用无参构造器时,就必须再继续定义一个无参构造器。

2、构造函数的重载

重载:存在一定函数,当对这个函数重新定义一组新的参数列表后形成的新的函数,也就是说两个函数只有参数列表是不相同的,这种行为叫做重载。
构造器的重载则是对构造函数参数列表的更改。

public Fifth2()
    this.level=0;

public Fifth2(int level)
    this.level=level;

public Fifth2(byte line)
    this.line = line;

public Fifth2(short s)
    this.s=s;

1、基本类型的重载

这节主要是讲,当出现基本类型重载的情况时,基本类型的转换情况,其主要的情况为
(1)小类型向大类型转换
当输入的参数类型小于函数参数的类型时,会自动向上转换成大类型的参数。当出现多个大类型重载时,则选择最小的那个。其顺序为:

这里写代码片

char > byte > short > int > long > float > double

 public static class Test
        public Test(int s)
        
            System.out.println("run the int");
        
        public Test(long s)
        
            System.out.println("run the long");
        
        public Test(float s)
        
            System.out.println("run the float");
        

    

    public static void example1()
    
        int a = 1;
        new Test(a);
        //
    

结果为

run the int

(2)char的特殊情况:char向上转型是int类型,无法转型为byte或者short

    public Fifth2(double level)
        this.level=level;
    
    public Fifth2(byte line)
        this.line = line;
    
    public Fifth2(short s)
       this.s=s;
    
    //
    char c = 'c';
    Fifth2 f3 = new Fifth2(c);
    System.out.println(f3.line); //0
    System.out.println(f3.s);    //0
    System.out.println(f3.level);//99

从这段代码中可以看到,char类型向上转型时,没有转型到离他更近的byte和short类型,而是直接转为int类型,再有int类型转型到double类型
(3)大类型向小类型转型
当输入的参数的类型大于函数参数的类型时,则需要将大类型的参数转换成小类型的参数,因此会造成精度损失,为了减少损失,java规定,当大类型向小类型转换时,先向较大的小类型进行转换。其转换顺序为:

double > float > long > int > short > byte > char
public static class Test

        public Test(int s)
        
            System.out.println("run the int");
        
        public Test(long s)
        
            System.out.println("run the long");
        
        public Test(float s)
        
            System.out.println("run the float");
        

    

    public static void example1()
    
        double d =1d;
        new Test((float) d); //float是编译器自动生成的。
    

结果为:

run the float

3、this关键字

this关键字的含义只是在对象内部时,特指对象本身,也就是说this关键字就代表了整个对象。因此可以在对象内部可以使用this来调用对象内的属性或者方法。
代码:

public Fifth4(int i,String s,Object o)
    System.out.println("Fifth4(int i.String s,Object o)");
    this.i =i;
    this.s =s;
    this.o =o;

在构造函数内使用this来使用对象的属性,相同使用对象的方法是使用同样的方式就可以。this关键字除此之外还有其他几个使用方式:

1、在构造器中调用构造器

有时候类中会实现多个构造函数,我们可以通过在构造器中调用其他构造器的方式来节省代码量。

    public Fifth4(int i,String s,Object o)
    
        System.out.println("Fifth4(int i.String s,Object o)");
        this.i =i;
        this.s =s;
        this.o =o;
    

    public Fifth4(int i,String s)
    
        this(i,s,null);
        System.out.println("Fifth4(int i,String s)");
    

    public Fifth4(int i)
    
        this(i,null);
        System.out.println("Fifth4(int i)");

    

    public Fifth4()
    
        this(0);
        System.out.println("Fifth4()");
    

例如上端代码,当我们在构造函数中调用另一个构造函数时,可以通过this进行代替,这是this的含义就不再是“对象本身”,而是构造函数的一种代表,this与参数列表的两个条件可以确定调用的哪一个构造函数。
规律:使用this关键词当做构造函数调用时,尽量遵循参数少的函数调用参数多的函数,并且对参数列表中无法赋值的参数初始化一个默认值,同时尽量保证参数列表之间的差别尽量小。尽量使得所有的构造函数形成一条链。

2、static关键字

static关键字,将修饰的属性或者方法定义为类的属性或者方法(注意是类的方法或者属性,而不是对象的方法或者属性)。需要注意的是,一个类在jvm中只有一份,但是一个类的对象在jvm中会有很多,一个类的所有对象都是从类结构中初始化而来,因此对象可以调用类的方法。但是类无法调用对象的方向或者属性。

    public static void example1()
    
        Fifth4 fifth4 = new Fifth4();
        fifth4.sayHello();
    

注意点:
(1)因为类在jvm中只存在一份的原因,因此类的属性也只会有一份,当多个对象同时使用类的属性的时候,一定要注意线程安全问题。

4、清理:终结处理和垃圾回收

在jvm中生成的对象不可能一直在jvm中存在,因此需要对jvm中已经不需要的对象进行清理,此时就会涉及到java中的垃圾回收机制,来将不再需要使用的对象进行清楚,释放占用的内存,以保证这个接下来jvm有空余的内存继续产生新的对象。清理概念我们需要牢记以下几点:
(1)对象可能不被垃圾回收
A、jvm可能并不会发生垃圾回收动作,垃圾回收动作只有在jvm内存即将使用完时才会进行垃圾回收,因为垃圾回收会停用整个jvm虚拟机,并且垃圾回收也会使用一定运算资源,因此在不必要时,垃圾回收动作不会进行。
B、垃圾回收动作回收的是不需要的对象,如果在jvm整个生命中其中,对象一直被需要(使用),因此来及回收可能不会对该对象进行回收。
(2)垃圾回收不等于“析构”
析构:讲对象使用的内存资源进行清空,释放资源。
一个对象的垃圾回收往往需要至少两次垃圾回收动作,这是因为在第一次垃圾回收时,并不会真的将对象进行垃圾回收,而是执行对象的finalize方法,进行用户定义的清理方式(finalize函数的作用会他谈到);当第二次(或者第N次)垃圾动作被出发后,jvm虚拟机则才会该对象进行垃圾清理(前提是该对象仍为不需要的,如果在两次垃圾回收之间,重新被使用,则无法进行回收)。
注意点:对象的finalize函数在jvm中只会被调用一次。也就是对象的在经历第一次垃圾回收动作时,finalize执行,此后及时对象重新使用且需要进行垃圾回收时都不会在调用finalize函数。
(3)垃圾回收至于内存有关
垃圾回收的过程就是清空不需要的对象,释放内存,因此在垃圾回收过程中使用的finalize函数也必须跟内容相关。

1、finalize函数的用处

(1)回收特殊内存
jvm的垃圾回收机制是对new关键字产生的对象进行回收,也就是说java语言产生的对象,jvm虚拟机都会进行管理,但是总会有一些特殊的方式创建内存,导致jvm虚拟机无法直接管理这部分内存,例如使用本地方法native方法调用c/c++的malloc函数使用内存块,当进行垃圾回收动作时,jvm无法对这部分内存进行释放,因此可以通过finalize函数调用c/c++ 的delete函数进行内存释放,减少内存泄露。
(2)终结条件
finalize 可以用作对象进行垃圾回收前的一次状态判断,判断对象是否满足已经回收的条件,根据结果对对象进行处理,保证每一个对象都能够被正确的进行垃圾回收。

    public static void example3()
    
        new Book(1).check();
        new Book(2);
        //
        System.gc();
    

    public static class Book

        private int id;
        private boolean check;

        public Book(int id)
        
            this.id = id;
            this.check=false;
        

        public void check()
        
            this.check=true;
        

        @Override
        protected void finalize() throws Throwable 
            if(!check)
            
                System.out.println("Book("+this.id+") is not checked");
            
        
    

上述代码中,当书本的内存信息被销毁时,都会检查对象是否执行了checkin函数,如果没有的话则提示。这样就利用finalize函数对对象终结条件进行判断。当我们还可以进行其他动作:将对象放到指定容器中,使其重新被使用,从而不被下次的垃圾回收进行回收,并为用户提供操作对象的接口。

4、垃圾回收过程

(1)当jvm内存达到处罚垃圾回收的条件时,一般为jvm的内存即将耗尽,触发垃圾回收动作。
(2)判断jvm内存中的对象是否有效,无效对象则需要进行清理
(3)判断对象是否第一次垃圾回收,如果是第一次经历垃圾回收动作,则直接调用对象的finalize函数。
(4)如果不是第一次经历垃圾回收动作,则进行内存回收

5、垃圾回收如何工作

我们大致了解了垃圾回收动作触发过程中的先关过程与函数执行,但是还有一些垃圾回收时工作内容并未说明,这里说明两点:无效对象的判断以及垃圾回收的算法

1、无效对象的判断

无效对象判断的最终目标是:确认该对象是否需要。判断一个对象是否需要时,则判断对象的引用是否有效(并非判断引用是否为0)
(1)引用计数
       引用计数法是根据对象上的引用个数来判断对象是否有效。当一个引用指向对象时,则对象的引用数量加1,如果一个引用置为null时,引用数量减1。当一个对象的引用数量为0时,则认为该对象为无效对象。当一个对象应用技术为0时,则立刻清空该对象。
       缺陷:当两个对象相互持有,且引用数量为1,也就是说没有他俩之外的其他对象进行使用时,这两个对象可能已经是无效的,但是因为相互持有的关系,导致jvm虚拟机认为这两个对象一直为有效。因此无法回收这两个对象。
       引用计数法到现在并没有某个jvm虚拟机实现过,这可能与引用计数法的缺陷以及效率有关。
(2)引用树(自己起的名字,具体名字我也不清楚)
判断条件:从堆栈或者静态数据开始,遍历所有的引用,就能得到有效对象。遍历对象的方式为:引用的对象中含有其他引用时,则对这些自引用继续进行递归判断,因此可以将从堆栈或者静态数据开始的引用而触发的引用网络遍历完后,那些在网络之外的对象则为无效的对象。

2、回收算法

(1)stop-and-copy算法
       该算法要求jvm需要维护两个大小一样的内存堆,当进行内存回收时,将有效的内存复制到另一个堆内存中,修改引用中的内存地址,然后清空现有堆的所有内存。
       缺点
       A、内存消耗:jvm需要维护两份同样大小的两个堆,但是只能使用其中一个,造成50%的内存的浪费
       B、对象移动:需要移动所有的有效对象,在某些情况下会造成很大的性能浪费。
(2)mark-and-sweep算法(标记-清除算法)
       该算法使用对象树的方式,查找所有的有效对象并进行标记,当所有的对象完成遍历后开始清除没有被标记的对象,然后将所有的对象向堆的一端(不知道是末端还是开始端)移动,最后所有的对象都连续的排序在堆栈内存中的一端。
       缺点:
       A、需要遍历整个引用树,效率比较低。

3、Stop The Word(概念普及)

这段只是做一个概念普及,在jvm在进行内存清理时,一般都需要将jvm中所有正在执行的线程停止,然后在进行内存清理,也就是说在整个清理过程中,整个jvm中的所有任务都是停止的,所以叫做Stop The World.

6、属性初始化

Java属性主要有两种:对象的属性和类的静态属性。而这一节来讲述这两种属性初始化的过程。
(1)java会保证所有的变量在使用前都会得到相应的初始化值

1、类的静态属性

定义:java文件中,被static关键字修饰的非局部变量属性。
特征:静态属性在内存中只存在一份,可以通过类直接访问,对象也可以直接访问(this也可以)。
初始化时间:第一次使用前或者类的第一个对象创建时。
初始化方式
(1)编译器赋值
编译器为了保证每一个变量在使用前都能得到相应的初始化的值,会对类的静态属性再编译过程中初始化一个默认值。

    public static class Coder
    
        public static int count ;
    

    public static void example3()
    
        System.out.println(Coder.count);
    

(2)定义时直接指定
在定义一个类的静态变量时,我们就可以直接对该静态属性直接赋值。

public static class Coder
    
        public static int count =10;
    

    public static void example3()
    
        System.out.println(Coder.count);
    

(3)静态代码块赋值
如果不想采用直接赋值的方式进行初始化时,可以通过静态代码块进行赋值。静态代码块是指类中被static直接修饰的代码块(并非函数)。

public static class Coder
    
        public static int count ;

     static
         count = 11;
     
    

    public static void example3()
    
        System.out.println(Coder.count);
    

初始化顺序:
(1)当该静态变量第一次被使用,或者类的第一个对象被创建时,开始进行初始化
(2)首先JVM会自动为该变量初始化一个默认是,一般为0,false,null,”“,”等,第一次
(3)执行声明时的赋值函数,将声明时定义的变量值赋值,进行第二次赋值
(4)执行静态代码块,运行其中的赋值代码,对变量进行第三赋值、
因此一个类的静态代码被初始化要经历过三次赋值,而该静态变量最终的值则是最后一次赋值的值。

2、对象的属性

定义:java文件中,函数体之外的,没有被static 修饰的属性。
初始化时间:对象被创建时,该对象的属性被初始化
初始化方式
(1)jvm/编译器赋值
根据java中任何变量使用之前都应该得到相应的初始化原则,对象的属性在对象初始化时会被给出一个默认值。

public static void example1()
    
        Coder coder = new Coder();
        System.out.println(coder.name); //coder
        System.out.println(coder.age);
        System.out.println(coder.email);
    

    public static class Coder
    
        private String name ="coder";

        private int age = getAge();

        private String email = getEmail(this.name);

        public int getAge()
        
            return 11;
        

        public String getEmail(String name)
        
            return name+"@qq.com";
        

    

(2)定义时初始化
当对象属性在被定义时,可直接进行初始化,初始化的方式有两种:直接赋值和函数赋值
直接赋值

 public static class Coder
    
        private String name ="coder";
    

函数赋值(没有确定是否可以调用静态函数对静态变量进行赋值)
函数赋值指可以直接调用对象函数,利用返回值进行赋值。注意点:如果在调用的函数中使用了对象的属性变量,必须保证该属性变量的正确性,例如字符串不能为null等条件。

 public static class Coder
    
        private String name ="coder";

        private int age = getAge();

        private String email = getEmail(this.name);

        public int getAge()
        
            return 11;
        

        public String getEmail(String name)
        
            return name+"@qq.com";
        
    

(3)代码块赋值
代码块赋值和静态代码块赋值相似,只不过代码块没有了static关键字修饰。

 public static class Coder
    
        private String name ;

        
            this.name = "coder";
        
    

(4)构造函数赋值
构造函数赋值也就是我们常用的一种方式,在构造函数中对变量进行初始化。

public static class Coder
    
        private String name ;

        public Coder()
            this.name = "coder";
        
    

初始化过程
(1)当对象被创建时,立刻开始初始化属性变量
(2)jvm对对象的变量属性进行默认赋值,第一次
(3)执行用户声明变量时的赋值,第二次赋值
(4)执行代码快,对变量属性进行第三次赋值
(5)执行构造函数,进行变量属性的第四次赋值

3、类的静态属性和对象的属性初始化过程

静态属性以及属性赋值分为两种情况:
1、父类无变量与静态变量
在这种情况下,需要初始化的变量和静态变量只有该类中的,因此该所有变量的初始化过程为:
(1)判断该类的静态属性是否初始化,如果初始化,进行(2),否则执行(3)
(2)执行静态属性初始化过程,jvm -> 声明时初始化 -> 静态代码快
(3)执行属性初始化过程,jvm -> 声明时初始化 -> 代码块 -> 构造函数

2、父类有对象和静态变量时
当父类与子类同时拥有静态变量和属性变量时其,其初始化的顺序为
(1)从现有子类开始,开始向上查找,一直到Object类,形成类链
(2)从Object开始,到现有子类,依次对每一个类进的静态变量进行初始化,jvm -> 声明初始化 -> 静态代码初始化
(3)静态变量完成初始化以后,从新从Object开始,依次对每一个类的生成一个对象,并进行初始化,jvm ->声明初始化 -> 代码块初始化 -> 构造器初始化
(4)当所有的属性变脸和静态变量完成初始化以后,则完成该对象的初始化。

三、总结

对象的创建、初始化以及想垃圾回收总结如上,如果有什么不正确的地方请直接留言指出,共勉。

以上是关于Java 初始化与清理的主要内容,如果未能解决你的问题,请参考以下文章

Java 编程思想 第五章 初始化与清理 上

初始化与清理

Java初始化与清理

Java的构造器

Java的构造器

Java编程思想(初始化与清理)