解析Java程序中对象内存的分配和控制的基本方法

一、对象与内存控制的知识点

1.java变量的初始化过程,包括局部变量,成员变量(实例变量和类变量)。
2.继承关系中,当使用的对象引用变量编译时类型和运行时类型不同时,访问该对象的属性和方法是有区别的。
3.final修饰符特性。

二、java变量的划分与初始化过程

java程序的变量大体可以分为成员变量和局部变量,成员变量可以分为实例变量(非静态变量)和类变量(静态变量),一般我们遇到的局部变量会在下列几种情况中出现:
(1)形参:在方法签名中定义的局部变量,由调用方为其赋值,随着方法结束消亡。
(2)方法内的局部变量:在方法内定义的局部变量必须在方法内显示的初始化(赋初始值),随着变量初始化完成开始,到方法结束而消亡。
(3)代码块内的局部变量:在代码块内定义的局部变量必须在代码块内显示的初始化(赋初始值),随着初始化完成开始生效,随着代码块的结束而消亡。

package com.zlc.array;

public class TestField {
  {
    String b ;
    //如果不初始化,编译器就会报The local variable b may not have been initialized
    System.out.println(b);
  }
  public static void main(String[] args) {
    int a ;
    //如果不初始化,编译器就会报The local variable a may not have been initialized
    System.out.println(a);
  }
}

使用static修饰的成员变量是类变量,属于类本身,没有用static修饰的成员变量是实例变量,属于该类的实例,在同一个JVM里面,每个类只能对应一个Class对象,但每个类可以创建多个java对象。(也就是说一个类变量只需一块内存空间,而该类每创建一次实例,就需要为实例变量分配一块空间)
   
实例变量的初始化过程:从语法角度来说,程序可以在三个地方对实例变量执行初始化:
(1)定义实例变量时指定初始值。
(2)非静态块中对实例变量指定初始值。
(3)构造器中对实例变量指定初始值。
其中(1)和(2)这两种方式初始化时间都比(3)在构造器中要早,(1)和(2)两种初始化顺序是按照他们在源码中的排列顺序决定的。

package com.zlc.array;

public class TestField {
  public TestField(int age){
    System.out.println("构造函数中初始化 this.age = "+this.age);
    this.age = age;
  }
  {
    System.out.println("非静态块中初始化");
    age = 22;
  }
  //定义的时候初始化
  int age = 15;
  public static void main(String[] args) {
    TestField field = new TestField(24);
    System.out.println("最终 age = "+field.age);
  }
}

运行结果为:非静态块中初始化
构造函数中初始化 this.age = 15
最终 age = 24
如果会使用javap的话,可以通过javap -c XXXX(class文件)看下改java类是如何编译的。
定义实例变量时指定初始值,初始化块中为实例变量指定初始值语句地位是平等的,当经过编译器编译处理之后,他们都被提到构造器中,上面所说的 int age = 15;会划分下面两个步骤执行:
1)int age;创建java对象时系统根据该语句为该对象分配内存。
2)age = 15;这条语句会被提取到java类的构造器中执行。
   
类变量的初始化过程:从语法角度来说,程序可以从两个地方对类变量进行初始化赋值。
(1)定义类变量时指定初始值。
(2)静态块中对类变量指定初始值。
两种执行顺序和他们在源码中的排列顺序相同,我们举个小变态的例子:

package com.zlc.array;

 class TestStatic {
  //类成员 DEMO TestStatic实例
  final static TestStatic DEMO = new TestStatic(15);
  //类成员 age
  static int age = 20;
  //实例变量 curAge
  int curAge;

  public TestStatic(int years) {
    // TODO Auto-generated constructor stub
    curAge = age - years;
  }
}
 public class Test{
  public static void main(String[] args) {
    System.out.println(TestStatic.DEMO.curAge);
    TestStatic staticDemo = new TestStatic(15);
    System.out.println(staticDemo.curAge);
  }
}

输出结果有两行打印,一个是打印TestStatic类属性DEMO的实例变量,第二个通过java对象staticDemo输出TestStatic的实例属性,根据我们上面分析的实例变量和类变量的初始化流程可以进行推断:
1)初始化第一阶段,加载类的时候为类变量DEMO、age分配内存空间,此时DEMO和age的默认值分别是null和0。
2)初始化第二阶段,程序按顺序依次给DEMO、age赋初始值,TestStatic(15)需要调用TestStatic的构造器,此时age = 0 所以打印结果为 -15,而当staticDemo被初始化的时候,age已经被赋值等于20了,所以输出结果为5。

三、在继承关系中继承成员变量和继承成员方法的区别

当创建任何java对象时,程序总会先调用父类的非静态块、父类构造器,最后才调用本类的非静态块和构造器。通过子类的构造器调用父类的构造器一般分为两种情况,一个是隐式调用,一个通过super显示调用父类的构造器。
子类的方法可以调用父类的实例变量,这是因为子类继承了父类就会获取父类的成员变量和方法,但父类的方法不能访问子类的实例变量,因为父类不知道它将被哪个类继承,它的子类将会增加什么样的成员变量,当然在一些极端的例子里面还是可以实现父类调用子类变量的,比如:子类重写了父类的方法,一般都会打印出默认值,因为这个时候子类的实例变量还没有初始化。

package com.zlc.array;
class Father{
  int age = 50;
  public Father() {
    // TODO Auto-generated constructor stub
        System.out.println(this.getClass());
        //this.sonMethod();无法调用
    info();
  }
  public void info(){
    System.out.println(age);
  }
}
public class Son extends Father{
  int age = 24;
  public Son(int age) {
    // TODO Auto-generated constructor stub
    this.age = age;
  }
  @Override
  public void info() {
    // TODO Auto-generated method stub
    System.err.println(age);
  }
  public static void main(String[] args) {
    new Son(28);
  }
    //子类特有的方法
    public void sonMethod(){
         System.out.println("Son method");
    }
}

按照我们正常推断,通过子类隐式的调用父类的构造器,而在父类的构造器中调用了info()方法(注意:我这里没有说调用父类的),按道理来说是输出了父类的age实例变量,打印结果预计是50,但实际输出的结果为0,分析原因:
1)java对象的内存分配不是在构造器中完成的,构造器只是完成了初始化赋值的过程,也就是在调用父类的构造器之前,jvm已经给这个Son对象分类好了内存空间,这个空间存放了两个age属性,一个是子类的age,一个是父类的age。
2)在调用new Son(28)的时候,当前的this对象代表着是子类Son的对象,我们可以通过把对象.getClass()打印出来就会得到class com.zlc.array.Son的结果,但是当前初始化过程又是在父类的构造器中进行的,通过this.sonMethod()又无法被调用,这是因为this的编译类型是Father的缘故。
3)在变量的编译时类型和运行时类型不同时,通过该变量访问它的引用对象的实例变量时,该实例变量的值由声明该变量的类型决定,但通过该变量调用它引用的对象的实例方法时,该方法的行为由它实际引用的对象决定,所以这里调用的是子类的info方法,所以打印的是子类的age,由于age还没来得急初始化所以打印默认值0。
通俗的来说也就是,当声明的类型和真正new的类型不一致的时候,使用的属性是父类的,调用的方法是子类的。
通过javap -c我们更能直接的体会为什么继承属性和方法会有很大的区别,如果我们把上面例子里面,子类Son的info重写方法去掉,这个时候调用的会是父类的info方法,是因为在进行编译的时候会把父类的info方法编译转移到子类里面去,而声名的成员变量会留在父类中不进行转移,这样子类和父类拥有了同名的实例变量,而如果子类重写了父类的同名方法,则子类的方法会完全覆盖掉父类的方法(至于为什么java要这么设计,个人也不太清楚)。同名变量能同时存在不覆盖,同名方法子类会彻底覆盖父类同名方法。
总的来说对于一个引用变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型,当通过该变量访问它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型。
最后拿个小case复习下:

package com.zlc.array;
class Animal{
  int age ;
  public Animal(){

  }
  public Animal(int age) {
    // TODO Auto-generated constructor stub
    this.age = age;
  }
  void run(){
    System.out.println("animal run "+age);
  }
}
class Dog extends Animal{
  int age;
  String name;
  public Dog(int age,String name) {
    // TODO Auto-generated constructor stub
    this.age = age;
    this.name = name;
  }
  @Override
  void run(){
    System.out.println("dog run "+age);
  }
}
public class TestExtends {
  public static void main(String[] args) {
    Animal animal = new Animal(5);
    System.out.println(animal.age);
    animal.run();

    Dog dog = new Dog(1, "xiaobai");
    System.out.println(dog.age);
    dog.run();

    Animal animal2 = new Dog(11, "wangcai");
    System.out.println(animal2.age);
    animal2.run();

    Animal animal3;
    animal3 = dog;
    System.out.println(animal3.age);
    animal3.run();
  }
}

想要调用父类的方法:可以通过super来调用,但super关键字没有引用任何对象,它不能当做真正的引用变量来使用,有兴趣的朋友可以自己研究下。
上面介绍的都是实例变量和方法,类变量和类方法要简单多了,直接使用类名.方法就方便了很多,也不会遇到那么多麻烦。

四、final修饰符的使用(特别是宏替换)

(1)inal可以修饰变量,被final修饰的变量被赋初始值之后,不能对他重新赋值。

(2)inal可以修饰方法,被final修饰的方法不能被重写。

(3)inal可以修饰类,被final修饰的类不能派生子类。

被final修饰的变量必须显示的指定初始值:
对于是final修饰的是实例变量,则只能在下列三个指定位置赋初始值。
(1)定义final实例变量时指定初始值。
(2)在非静态块中为final实例变量指定初始值。
(3)在构造器中为final实例变量指定初始值。
最终都会被提到构造器中进行初始化。
对于用final指定的类变量:只能在指定的两个地方进行赋初始值。
(1)定义final类变量的时候指定初始值。
(2)在静态块中为final类变量指定初始值。
同样经过编译器处理,不同于实例变量的是,类变量都是提到静态块中进行赋初始值,而实例变量是提到构造器中完成。
   被final修饰的类变量还有一种特性,就是“宏替换”,当被修饰的类变量满足在定义该变量的时候就指定初始值,而且这个初始值在编译的时候就能确定下来(比如:18、"aaaa"、16.78等一些直接量),那么该final修饰的类变量不在是一个变量,系统就会当成“宏变量”处理(就是我们常说的常量),如果在编译的时候就能确定初始值,则就不会被提到静态块中进行初始化了,直接在类定义中直接使该初始值代替掉final变量。我们还是举那个年龄减去year的例子:

package com.zlc.array;

 class TestStatic {
  //类成员 DEMO TestStatic实例
  final static TestStatic DEMO = new TestStatic(15);
  //类成员 age
  final static int age = 20;
  //实例变量 curAge
  int curAge;

  public TestStatic(int years) {
    // TODO Auto-generated constructor stub
    curAge = age - years;
  }
}
 public class Test{
  public static void main(String[] args) {
    System.out.println(TestStatic.DEMO.curAge);
    TestStatic static1 = new TestStatic(15);
    System.out.println(static1.curAge);
  }
}

这个时候的age 被final修饰了,所以在编译的时候,父类中所有的age都变成了20,而不是一个变量,这样输出的结果就能达到我们的预期。
特别是在对字符串进行比较的时候更能显示出

package com.zlc.array;

public class TestString {
  static String static_name1 = "java";
  static String static_name2 = "me";
  static String statci_name3 = static_name1+static_name2;

  final static String final_static_name1 = "java";
  final static String final_static_name2 = "me";
  //加final 或者不加都行 前面兩個能被宏替換就行了
  final static String final_statci_name3 = final_static_name1+final_static_name2;

  public static void main(String[] args) {
    String name1 = "java";
    String name2 = "me";
    String name3 = name1+name2;
    //(1)
    System.out.println(name3 == "javame");
    //(2)
    System.out.println(TestString.statci_name3 == "javame");
    //(3)
    System.out.println(TestString.final_statci_name3 == "javame");
  }
}

用final修饰方法和类没有什么好说的,只是一个不能被子类重写(和private一样),一个不能派生子类。
用final修饰局部变量的时候,Java要求被内部类访问的局部变量都是用final修饰,这个是有原因的,对于普通局部变量而言,它的作用域就停留在该方法内,当方法结束时,该局部变量也就消失了,但内部类可能产生隐式的“闭包”,闭包使得局部变量脱离他所在的方法继续存在。
有时候在会在一个方法里面new 一个线程,然后调用该方法的局部变量,这个时候需要把改变量声明为final修饰的。

五、对象占用内存的计算方法
使用system.gc()和java.lang.Runtime类中的freeMemory(),totalMemory(),maxMemory()这几个方法测量Java对象的大小。这种方法通常使用在需要对很多资源进行精确确定对象的大小。这种方法几乎无用等生产系统缓存的实现。这种方法的优点是数据类型大小无关的,不同的操作系统,都可以得到占用的内存。

它使用反射API用于遍历对象的成员变量的层次结构和计算所有原始变量的大小。这种方法不需要如此多的资源,可用于缓存的实现。缺点是原始类型大小是不同的不同的JVM实现对应有不同的计算方法。
JDK5.0之后Instrumentation API提供了 getObjectSize方法来计算对象占用的内存大小。
默认情况下并没有计算到引用对象的大小,为了计算引用对象,可以使用反射获取。下面这个方法是上面文章里面提供的一个计算包含引用对象大小的实现:

public class SizeOfAgent {

  static Instrumentation inst;

  /** initializes agent */
  public static void premain(String agentArgs, Instrumentation instP) {
    inst = instP;
  }

  /**
   * Returns object size without member sub-objects.
   * @param o object to get size of
   * @return object size
   */
  public static long sizeOf(Object o) {
    if(inst == null) {
      throw new IllegalStateException("Can not access instrumentation environment.\n" +
                    "Please check if jar file containing SizeOfAgent class is \n" +
                    "specified in the java's \"-javaagent\" command line argument.");
    }
    return inst.getObjectSize(o);
  }

  /**
   * Calculates full size of object iterating over
   * its hierarchy graph.
   * @param obj object to calculate size of
   * @return object size
   */
  public static long fullSizeOf(Object obj) {
    Map<Object, Object> visited = new IdentityHashMap<Object, Object>();
    Stack<Object> stack = new Stack<Object>();

    long result = internalSizeOf(obj, stack, visited);
    while (!stack.isEmpty()) {
      result += internalSizeOf(stack.pop(), stack, visited);
    }
    visited.clear();
    return result;
  }        

  private static boolean skipObject(Object obj, Map<Object, Object> visited) {
    if (obj instanceof String) {
      // skip interned string
      if (obj == ((String) obj).intern()) {
        return true;
      }
    }
    return (obj == null) // skip visited object
        || visited.containsKey(obj);
  }

  private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) {
    if (skipObject(obj, visited)){
      return 0;
    }
    visited.put(obj, null);

    long result = 0;
    // get size of object + primitive variables + member pointers
    result += SizeOfAgent.sizeOf(obj);

    // process all array elements
    Class clazz = obj.getClass();
    if (clazz.isArray()) {
      if(clazz.getName().length() != 2) {// skip primitive type array
        int length = Array.getLength(obj);
        for (int i = 0; i < length; i++) {
          stack.add(Array.get(obj, i));
        }
      }
      return result;
    }

    // process all fields of the object
    while (clazz != null) {
      Field[] fields = clazz.getDeclaredFields();
      for (int i = 0; i < fields.length; i++) {
        if (!Modifier.isStatic(fields[i].getModifiers())) {
          if (fields[i].getType().isPrimitive()) {
            continue; // skip primitive fields
          } else {
            fields[i].setAccessible(true);
            try {
              // objects to be estimated are put to stack
              Object objectToAdd = fields[i].get(obj);
              if (objectToAdd != null) {
                stack.add(objectToAdd);
              }
            } catch (IllegalAccessException ex) {
              assert false;
            }
            }
          }
      }
      clazz = clazz.getSuperclass();
    }
    return result;
  }
}
(0)

相关推荐

  • 深入Java对象的地址的使用分析

    在传统的Java编程中,你将不再需要从内存中处理Java对象或位置. 当你在论坛上讨论这一点,提出的第一个问题是为什么你需要知道Java对象的地址? 它是一种有效的问题. 但以往,我们保留进行试验的权利.探索未知领域的问题并没有什么错.我想出了一个使用sun公司包的实验.Unsafe是一个属于sun.misc包.对你来说可能这个包有点陌生,看看源代码和方法,你就可以知道我所指的是什么了. Java的安全管理提供了足够的隐藏来确保你并不能那么容易的摆弄内存.作为第一步,我想到了要得到一个Java对

  • Java中典型的内存泄露问题和解决方法

    Q:在Java中怎么可以产生内存泄露?A:Java中,造成内存泄露的原因有很多种.典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况.最后会生成很多重复的对象.所有的内存泄露最后都会抛出OutOfMemoryError异常,下面通过一段简短的通过无限循环模拟内存泄露的例子说明一下. 复制代码 代码如下: import java.util.HashMap;import java.util.Map; public class MemoryLeak { pu

  • Java中内存分配的几种方法

    一.数组分配的上限 Java里数组的大小是受限制的,因为它使用的是int类型作为数组下标.这意味着你无法申请超过Integer.MAX_VALUE(2^31-1)大小的数组.这并不是说你申请内存的上限就是2G.你可以申请一个大一点的类型的数组.比如: 复制代码 代码如下: final long[] ar = new long[ Integer.MAX_VALUE ]; 这个会分配16G -8字节,如果你设置的-Xmx参数足够大的话(通常你的堆至少得保留50%以上的空间,也就是说分配16G的内存,

  • 深入理解Java对象的序列化与反序列化的应用

    当两个进程在进行远程通信时,彼此可以发送各种类型的数据.无论是何种类型的数据,都会以二进制序列的形式在网络上传送.发送方需要把这个Java对象转换为字节序列,才能在网络上传送:接收方则需要把字节序列再恢复为Java对象. 把Java对象转换为字节序列的过程称为对象的序列化.把字节序列恢复为Java对象的过程称为对象的反序列化.对象的序列化主要有两种用途:1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中:2) 在网络上传送对象的字节序列.一. JDK类库中的序列化APIjava.io

  • 详细介绍Java内存泄露原因

    一.Java内存回收机制 不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址.Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的.GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请.引用.被引用.赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题.在J

  • 深入JAVA对象深度克隆的详解

    有时候,我们需要把对象A的所有值复制给对象B(B = A),但是这样用等号给赋值你会发现,当B中的某个对象值改变时,同时也会修改到A中相应对象的值!也许你会说,用clone()不就行了?!你的想法只对了一半,因为用clone()时,除了基础数据和String类型的不受影响外,其他复杂类型(如集合.对象等)还是会受到影响的!除非你对每个对象里的复杂类型又进行了clone(),但是如果一个对象的层次非常深,那么clone()起来非常复杂,还有可能出现遗漏!既然用等号和clone()复制对象都会对原来

  • JavaScipt对象的基本知识第1/2页

    JavaScript 是使用"对象化编程"的,或者叫"面向对象编程"的.所谓"对象化编程",意思是把 JavaScript 能涉及的范围划分成大大小小的对象,对象下面还继续划分对象直至非常详细为止,所有的编程都以对象为出发点,基于对象.小到一个变量,大到网页文档.窗口甚至屏幕,都是对象.这一章将"面向对象"讲述 JavaScript 的运行情况. 对象的基本知识  对象是可以从 JavaScript"势力范围&quo

  • 深入分析Java内存区域的使用详解

    Java 内存划分: 在Java内存分配中,java将内存分为:方法区,堆,虚拟机栈,本地方法栈,程序计数器.其中方法区和堆对于所有线程共享,而虚拟机栈和本地方法栈还有程序计数器对于线程隔离的.每个区域都有各自的创建和销毁时间. 程序计数器:     作用是当前线程所执行的字节吗的行号指示器.Java的多线程是通过线程轮流切换并分配处理器执行时间方式来实现的.因此,每个线程为了能在切换后能恢复到正确的位置,每个线程需要独立的程序计数器. Java 虚拟机栈: 每个放在被执行的时候都会同时创建一个

  • 基于Java内存溢出的解决方法详解

    一.内存溢出类型1.java.lang.OutOfMemoryError: PermGen spaceJVM管理两种类型的内存,堆和非堆.堆是给开发人员用的上面说的就是,是在JVM启动时创建:非堆是留给JVM自己用的,用来存放类的信息的.它和堆不同,运行期内GC不会释放空间.如果web app用了大量的第三方jar或者应用有太多的class文件而恰好MaxPermSize设置较小,超出了也会导致这块内存的占用过多造成溢出,或者tomcat热部署时侯不会清理前面加载的环境,只会将context更改

  • java 序列化对象 serializable 读写数据的实例

    序列化对象: 复制代码 代码如下: package com.chen.seriaizable; import java.io.Serializable;import java.util.List; @SuppressWarnings("serial")public class Student implements Serializable{ private String name; private String id; private int age; private List<

  • 解析Java的JNI编程中的对象引用与内存泄漏问题

    JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互--在 Java 程序中调用 native code:在 native code 中嵌入 Java 虚拟机调用 Java 的代码. JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点: 利用 native code 的平台相关性,在平台相关的编程中彰显优势. 对 native code 的代码重用. native code 底层操作

随机推荐