Java内存管理分为两个方面即内存分配和内存回收。内存分配特指创建Java对象时jvm为该对象在堆内存中所分配的内存空间;内存回收指的是当该Java对象失去引用变成垃圾时,jvm的垃圾回收机制自动清理该对象,并回收该对象所占用的内存。不能因为jvm内置了垃圾回收机制而认为java不存在内存泄漏,因此不能肆无忌惮的创建对象。
一、实例变量和类变量
1.java程序变量可分为成员变量和局部变量。
局部变量分为三类:形参、方法内的局部变量、代码块内的局部变量。局部变量的作用时间很短,它们被存储在方法的栈内存中。
成员变量即在类内定义的变量,如果定义该成员变量时没有使用static修饰符则该成员变量叫做非静态变量即实例变量,即该成员变量属于类的实例;若使用了static修饰符则该成员变量叫做静态变量即类变量,即该成员变量属于类本身。
“static”在Java中作用就是从实例成员变为类成员,它只能修饰类里的成员,不能修饰外部类,不能修饰局部变量、局部内部类。
类变量初始化时机总是早于实例变量,当一个类初始化完成时,类变量也初始化完成了。java要求定义变量时必须采用合法的前向引用,即变量必须先定义再使用。
比如下面就是错误的,因为num2还没定义就先被引用了:
但是,如果定义num2为类变量,就不会出错,因为类变量总是比市里变量先初始化完成,如下就不会报错:
2.实例变量和类变量的属性
实例变量属于类的实例,类变量属于类本身。在同一个jvm中,每个类对应一个class对象,但是每一个class可以创建很多歌java对象,即在同一个jvm中,一个类的类变量只需要一块内存空间,但是 对于实例变量而言,该类每创建一个实例对象就需要为实例变量分配一块内存空间,程序里有几个实例变量就需要几块内存空间。
有一个person类
class person{
String name;
int age;
static int eyenum;
public void info(){
System.out.println("我的名字是:"+name+",我的年龄是:"+age);
}
}
(1)person.eyenum=2:因为eyenum是类变量,因此类初始化完成后就可以使用该类变量,对该累变量赋值完成后,内存分配如下:
(2)person p=new person();p.name="ltt";p.age=18;p.eyenum:定义一个person对象p,并用p去访问类变量eyenum,内存分配如下:
可以看到虽然p调用了eyenum,但是系统不再为eyenum分配内存空间,这更说明当类初始化后类变量也初始化结束,无论类创建多少的实例对象,系统都不再为类变量分配空间,但是没创建一个实例对象就会为实例变量创建一块内存。
(3)person q=new person();q.name="pl";q.age=18;q.eyenum=6:再添加一个person对象q,并对类变量eyenum重新赋值,这样实际修改的是person类的eyenum,此时内存分配如下:
3.实例变量初始化时机
程序可以在三个地方对实例变量进行初始化:(1)定义实例变量时指定初始值(2)非静态初始化块中对实例变量指定初始值(3)构造器中对实例变量指定初始值
需要注意的是前两种方法总是在第三种方法之前执行,而前两种方法的执行顺序和它们在程序里面书写的顺序一致。而且值得一说的是,经过编译后前两种方法的赋值语句都被提到构造器中,且总是位于构造器中的所有语句之前。例子如下:
class cat{
String name;
int age;
public cat(String name,int age){
System.out.println("执行构造器!");
this.name=name;
this.age=age;
}
{
System.out.println("执行非静态代码块!");
weights=2.0;
}
double weights=2.3;//定义时指定初始值
public String getString(){
return "name="+name+",age="+age+",weights="+weights;
}
}
public class tt{
public static void main(String[] args){
cat cat=new cat("aa", 2);
System.out.println(cat.getString());
}
}
结果:
最后执行构造器,按照程序里的顺序先执行静态代码块里的weigthts=2,再执行定义时指定的weights=2.3,所以最后的结果也是weights=2.3
4.类变量初始化时机
每个jvm队一个Java类只初始化一次,因此系统只为类变量分配一次内存空间,执行一次初始化,程序可以在两个地方对类变量进行初始化(1)定义变量时指定初始值(2)静态初始化块中队类变量指定初始值。同样,这两种方法的执行顺序和它们在源程序中的排列顺序一致。
public class tt{
static int count=2;
static{
System.out.println("静态初始化块!");
name="java";
}
static String name="c++";
public static void main(String[] args){
System.out.println(tt.count);
System.out.print(tt.name);
}
}
结果如下:
对类初始化(1)系统为类变量分配内存空间,此时它们的初始值为0或者null(2)按照初始化代码块的排列顺序对类变量进行初始化。
二、父类构造器
1.程序总是会依次先调用每个父类的非静态初始化块、父类构造器,最后才调用本类的非静态初始化块、构造器。在调用父类的构造器时,既可以用super()显示调用,也可以隐式调用。super()调用用于显示的调用父类的构造器,this()调用用于显示调用本类中另一个重载的构造器,系统会根据super和this里面调用传入的实参列表确定调用哪一个构造器,super和this只能在构造器中使用,而且作为构造器的第一行。若没有super或this,则隐式的调用父类无参构造器。当this出现在构造器中,this代表正在初始化的java对象。
2.访问子类变量的实例变量
子类的方法可以访问父类的实例变量,但是父类方法不能访问子类的实例变量,但是在一些情况下,会出现父类访问子类的例子,如下:
class Base{
private int i=2;
public Base(){
this.display();
}
public void display(){
System.out.println(i);
}
}
class sub extends Base{
private int i=22;
public sub(){
i=222;
}
public void display(){
System.out.println(i);
}
}
public class tt{
public static void main(String[] args){
new sub();
}
}
结果是0。
new sub()时会去执行sub的构造器,构造器会隐式的去执行父类的无参数构造器,里面只有this.display(),这个this指的是正在初始化的java对象即sub,则this调用的display方法即sub类的display,此时的i并没有 被初始化,因此为0,所以结果为0。
3.调用被子类重写的方法
在访问权限允许的情况下,子类可以调用父类的方法,但是父类不能调用子类的方法,但是当子类重写了父类的方法后,也会出现父类调用子类方法的例子。
class animal{
private String desc;
public animal(){
this.desc=getDesc();
}
public String getDesc(){
return "animal";
}
public String toString(){
return desc;
}
}
class wolf extends animal{
private String name;
private double weights;
public wolf(String name,double weights){
this.name=name;
this.weights=weights;
}
public String getDesc(){
return "name:"+name+" ,weights="+weights;
}
}
public class tt{
public static void main(String[] args){
System.out.println(new wolf("lkk", 28));
}
}
结果:name:null ,weights=0.0。
new wolf()会调用wolf的构造器,构造器隐式的调用父类无参构造器,父类里面的getDesc表面上是父类的,实际是子类的getDesc,此时name和wights还没有被赋值,因此为null和0.
因此为了避免这种结果,应该避免在父类构造器中调用被子类重写过的方法。
三、父子实例的内存控制
class Base{
int count=2;
public void display(){
System.out.println(this.count);
}
}
class sub extends Base{
int count=20;
public void display(){
System.out.println(this.count);
}
}
public class tt{
public static void main(String[] args){
Base a=new Base();
System.out.println("Base a=new Base():"+a.count);
System.out.print("Base a=new Base():");
a.display();
sub b=new sub();
System.out.println("sub b=new sub():"+b.count);
System.out.print("sub b=new sub():");
b.display();
Base c=new sub();
System.out.println("Base c=new sub():"+c.count);
System.out.print("Base c=new sub():");
c.display();
Base d=b;
System.out.println("Base d=b:"+d.count);
System.out.print("Base d=b:");
d.display();
}
}
结果如下:
对于上面的结果,需要注意的如下:
(1)首先,子类父类中定义的同名的实例变量,子类中会保留父类的实例变量,即在子类对象中,不仅保存了子类的实例变量而且保存了父类的实例变量,但是如果子类重写了父类方法,就相当于子类完全覆盖了父类里的同名方法,即系统不能把父类的方法转移到子类中去。上面子类sub的内存如下:
可以看到sub中不仅保存了自己的count,也保存了父类Base的count.
(2)当变量的编译类型和运行类型不一致时,通过该变量访问它所引用的对象的实例变量时,实例变量的值由声明该变量的类型决定,但是通过该变量调用它引用的对象的实例方法时,由它实际所引用的类型对象来决定。
所以上面的例子Base c=new sub():声明了一个Base变量c,但是把sub对象赋值给c,调用c.count就和声明类型Base有关,因此输出的是Base里面的count2,而c.dispaly()和实际类型sub有关,因此调用的是sub里面的display(),,即输出20。
四、final修饰符
1.final修饰过的变量被赋值后,不能再重新赋值;被final修饰过的方法不能被重写;被final修饰过的类不能派生子类。
2.final修饰过的变量在编译时就确定了值,则这个final变量不再是变量而是一个确定的,因此系统会把它当做宏变量来处理。
对于final修饰的实例变量来说只有在定义该变量时指定初始值才会有宏变量的效果,在非静态初始化块和构造器中不会有宏变量的效果,类变量也只有在定义时指定初始值才会有宏变量的效果。
3.如果程序需要再内部类中使用局部变量,则这个局部变量必须是final修饰的。