• Java进阶2 数组内存和对象的内存管理知识


    Java进阶2 数组内存和对象的内存管理知识 20131028

    前言:

             在面试的时候,如果是Java的编程语言,也许你认为没有什么可以问的,只能够说明你对于Java了解的太浅了,几乎就是两个星期的节奏速成,没有在底层掌握Java编程语言。那么面试的时候,你就会发现很多的不会,所以在这个时候切记说你懂Java。

             还有有些人面试Java认为就是面试SSH框架,其实个人理解方面,除了那种很小型的公司还有不懂技术的什么什么类型的企业,就会拿SSH器标准你。说一下自己的情况:

             我的第一编程语言是C++,同时Java是自己的辅助,可以算的上是本科生中学习Java最好的之一(谦虚点了),但是我自己真的对于SSH没有掌握,因为为了面试去学习SSh框架感觉很不值,自己不喜欢为了学习框架而去学习框架。对于Java中的框架,没有1000也有2000的样子,这么多的框架怎么学啊,所以当有需要的时候才可以去学习。我自己掌握Spring的IOC机制,因为在暑假期间的时候确实需要,还有就是数据接口的框架,我自己掌握的是Mybatis框架技术,所以没有去学习Hibernate框架。其实学习Java的关键不是说你会使用多少的框架,而是对于Java编程语言的真正意义上的掌握,而现在大多数人掌握的Java水平只是出于一种简单的语法,根本不了解Java低层次的更深层次的知识,这样面试的时候,你就会暴露出来,因为面试官问你的问题基本在教材中找不到答案,其实Java是一门轻松入门,但是如果想学懂得话,那就真的需要下点苦功夫了。

    Chapter 1 Java数组内存分配

    1.Java是一种静态编程语言,对应的Java数组也是静态的即,数组被初始化之后,数组占用的空间和数组的长度是不变的。数组初始化的方式有两种:静态初始化和动态初始化。

             静态初始化:程序员显示的指定每一个元素的初始值,有系统决定数组的长度;

             动态初始化:程序员指定数组的长度,由系统初始化数组的值,数组还可以使用length访问数组的长度。

             数组中的所有元素实质上都保存在内存的堆中,数组的名字保存在栈中。

             对于字符串数组的话,其实使用的是string pool 实现的,所以在堆中的内存中存放的知识字符串的地址。

    2.数组一定要初始化吗

             了解Java中数组的内存分配,其实java数组的名字是保存在栈中的,他本身不是数组对象,而是对数组对象的引用,只要让数组的名字指向有效的数组对象即可使用数组变量。这里的数组变量只是一个引用变量,类似C的指针,数组的初始化其实不是对数组变量执行初始化,而是在堆中创建数组对象,在堆中分配一块连续的内存空间。

             int []arr = null;

        System.out.println(arr);这一段代码是没有任何问题的,因为访问的是arr变量而不是arr的成员方法或者是属性

        arr.length就会报错,抛出NullPointerException,因为通过引用变量访问一个还未引用的有效的对象的时候,就会出现这种异常。

    public class TestMain {

        public static void main(String[] args) {     

           Person [] students;

           students = new Person[2];

           //students[0].printInfo(); // error NullPointerException

           //students[1].printInfo(); // error NullPointerException

           Person a = new Person(10,12.0);

           Person b = new Person(138,24.9);

           students[0] = a;

           students[1] = b;

           System.out.println("before change : ");

           students[0].printInfo();

           a.age = 100;

           a.height = 50.9;

           System.out.println("after changed : ");

           students[0].printInfo();

           /*

            * 实际上students[0] 和 a 执行的是同一个对象,当修改了a 的时候,对应的students[0]也会随之修改

            * 数组内容同样只是对于对象的一个引用,其中的指向的内容才是实际的对象。

            */

        }

    }

    class Person{

        public int age;

        public double height;

        public Person(int a, double height){

           this.age = a; this.height = height;

        }

        public void printInfo(){

           System.out.println("age:" + this.age + ", height:" + this.height);

        }

    }

    Chapter 2 对象及其内存管理

             虽然Java是有JVM管理内存的,但是作为程序员,也必须了解Java内存管理机制,我们编写源代码不能够仅仅停留在代码层面上,需要考虑每一行代码对于系统的内存影响。Java的内存管理机制比较那一理解,所以可能会感觉Java内存管理和实际开发距离比较远。这是一种错误的理解,虽然JVM会关心程序的内存回收,但是并不意味着我们程序员可以随意的使用系统的内存。

             Java内存管理分为两个方面:内存分配和内存回收。内存分配指的是创建Java对象是JVM为该对象在对内存中分配内存空间;内存回收指的是当该Java对象失去引用的时候,变成垃圾,JVM的垃圾回收机制自动清理该对象,并且回收对象占用的内存空间。

             JVM内存回收机制是由一条后台线程维护的,而且该线程也是十分消耗资源的,如果我们在程序中肆无忌惮的创建新对象,让系统分配内存,那么这些分配的内存都将有GVM的垃圾回收机制完成回收,这样做的不好的地方是:

             不断的分配内存空间是操作系统的内存空间减少,会降低程序的性能;同时大量已经分配内存的回收是的来及回收的负担加重,降低程序的性能。这一章主要介绍内存管理中的内存分配的知识。

    2.1实例变量和类变量

    成员变量和局部变量

             对于局部变量的话存在三种情况:

             形参:在方法签名中定义的局部变量,有放大调用者负责为其赋值,随着方法的结束而消亡。

             方法内的局部变量:在方法中定义的局部变量,必须在方法内部显示的初始化,从初始化开始生效,并且随着方法的结束失效;

             代码块的局部变量:在代码块中显示的初始化,在代码块结束的时候,变量消亡。

             成员变量有两种:静态成员变量和普通的成员变量。静态成员变量也就是说成员属于该类而不是Class中的某一个对象。静态变量的初始化,也就是类变量的初始化是在编译的时候,随着class的初始化而得到初始化的,所以静态变量会造编译阶段的时候就已经完成初始化,所以在普通的成员变量可以使用它,无论是在静态 变量之前还是在静态变量之后。但是对于静态成员变量和静态成员变量就会存在一个先后的问题:

    public class ErrorDef{

             static int num1 = num2 + 3;

             static int num2 = 10;

    }

    但是对于下面的情况是对的

    public  class  RightDef{

             int num1 = num2 + 10;

             static int  num2 = 10;

    }

    public class TestMain {

        int num1 = num2 + 10;

        static int num2; //default 0

        public static void main(String[] args) {

           TestMain main  = new TestMain();

           System.out.println(main.num2); //0

           System.out.println(main.num1);// 10

        }

    }

        在JVM中每一个Class对应一个对象,每一个Class可以创建多个Java对象。所以静态变量只会有一份。在某种意义上来所其实Class也是一个对象,所以的类都是Class的实例。每一个类初始化之后,系统会为该类创建一个对应的Class实例,程序可以通过反射来获得某个类所对应的Class实例: Person.class ,或者是Class.forName(“Person”)即可。

        普通的成员变量的初始化时机:对于实例变量来说,他说与Java对象本身,每一次创建一个Java对象,都会需要为实例变量分配内存空间,并且实例变量执行初始化。在程序中可以在三个地方初始化成员变量:

        在声明成员变量的时候初始化;非静态的代码块儿中初始化;构造函数中初始化。前两种方式比后一种方式更早的执行。前两种的话,取决于器在程序中代码的位置。仅限于Java编程语言。我们整理一段Java代码:

     

    public class A {

        {

           a=2;// 创建A的对象的时候,会执行这一段代码,没创建一个对象都会调用这一段代码,执行当然是在构造函数执行之前,而且可以提前初始化值,但是不可以右值,只可以左值。

        }

        public int a; //只是一个引用

        {

           System.out.println("code block a = " +a  );

        }

        static {//静态代码块,在加载类的时候执行,切只会执行一次

           System.out.println("static A ");

        }

        public A(){

           System.out.println("A.A()");

           System.out.println("in A.A() before change  a = " + a);

           a = 3;

        }

    }

    public class Base {

        public A objA;//不会调用构造函数 当然如果我们在这里显示初始化的话,就会调用,在Base构造函数之前调用A的构造函数,执行一系列操作。

        static {

           System.out.println("Base static code");

        }

        {

           System.out.println("Base code ");

        }

        public Base(){

           System.out.println("Base.Base()");

           objA = new A();

        }

    }

    Main{

        Base b= new Base();

    }

    main start

    Base static code

    Base code

    Base.Base()

    static A

    code block a = 2

    A.A()

    in A.A() before change  a = 2

    在Java中,成员变量在声明的时候初始化的底层实现:

    double weight = 23.45;其实是分为两部分实现的,当床架Java对象的时候,根据该语句会为其分配内存空间,但是没有初始化值,weight = 23.45;这一句代码会被提取出来到Java的构造器中执行,但不是构造函数。

    对于Java编译的知识我们如果想要了解的更详细的话,可以将源代码编译之后生成class 然后使用 javap –c ClassName输出,查看编译的情况。

             对于类的变量初始化时机:定义的时候直接初始化;或者使用静态代码块初始化变量。两种方式的执行顺序按照其在代码中声明的顺序执行。

    下面看一段代码:

    public class Price {

        final static Price INSTANCE = new Price(2.9);

        static double initPrice = 20;

        public double currPrice;

        public Price(double discount){

           this.currPrice  = this.initPrice - discount;

        }

    }

    public class TestMain {

        public static void main(String[] args) throws ClassNotFoundException {

           System.out.println(Price.INSTANCE.currPrice);

           Price p = new Price(2.9);

           System.out.println(p.currPrice);

        }

    }

    //在第一次使用Price类的时候,静态变量调用类的构造函数进行初始化,但是这个时候声明的initPrice没有进行初始化,默认是0,而不是20,所以在调用构造函数的时候会产生复数。

           String a = "yang";

           System.out.println(System.identityHashCode(a));

           String b = "yang";

           System.out.println(System.identityHashCode(b));

           String c = new String("yang");

           System.out.println(System.identityHashCode(c));

    2.2继承的执行顺序

    public class Base {

        static {

           System.out.println("Base static code");

        }

        {

           System.out.println("Base not static code ");

        }

        public Base(){

           System.out.println("Base.Base()");

        }

        public Base(int a){

           System.out.println("Base.Base(int )");

        }

    }

    public class Mid extends Base{

        static{

           System.out.println("Mid static code");

        }

        {

           System.out.println("Mid not static code");

        }

        Mid(){

           super();

           System.out.println("Mid.Mid()");

        }

        Mid(int a){

           super(a);

           System.out.println("Mid.Mid(int)");

        }

    }

    public class Sub extends Mid {

        static {

           System.out.println("Sub static code");

        }

        {

           System.out.println("Sub not static code");

        }

       

        Sub(){

           super(4);

           System.out.println("Sub.Sub() ");

        }

    }

    public static void main(String[] args) {

        Sub sub = new Sub();

    }

    Base static code

    Mid static code

    Sub static code

    Base not static code

    Base.Base(int )

    Mid not static code

    Mid.Mid(int)

    Sub not static code

    Sub.Sub()

    执行顺序的理解,其实首先执行的是父类的非静态代码区域,然后是父类的构造函数,但是super默认会执行默认的构造函数,当我们不显示的super执行父类的构造函数类型的时候,需有默认的构造函数,否则会直接报错。其实在super就是指明执行哪一个父类的构造函数。

             只要在程序中创建Java对象,系统总是调用最顶层的父类的初始化操作,包括初始化块和构造函数,然后依次向下调用所有的类的初始化操作,最终执行的是本类的初始化操作,返回本类的实例,至于父类中调用哪一个构造函数,分为如下几种情况:1.子类的构造函数中使用super显式的调用父类中的构造函数,系统会根据super的参数列表匹配父类的构造函数,这个是静态绑定,也就是在编译阶段就已经确定了。注意一点如果使用super的话,必须在构造函数中的第一句使用super指明父类的构造函数。2.子类的构造函数中执行体中的第一行代码使用this关键字现实的调用该类中的重载的构造函数,系统图会根据this调用里传入的实参列表来确定该类中的另一个构造器,执行该类的另一个构造函数。3.既没有super关键字,也没有this关键字调用,系统将会在执行子类的构造器之前,隐式的调用默认的父类构造函数。

    2.3 访问子类对象中的实例变量

             子类中的方法可以访问父类中的实例变量,这是因为子类继承父类就会获得父类的成员变量和方法;但是父类的方法不能够访问子类的实例变量,因为父类不知道他被那个子类继承,他的子类会增加那些变量。

    下面分析一段代码:父类 Base ,子类Sub extends Base , 在main中创建一个子类的对象。

    public class Base {

        private int val = 2;

        public Base(){

           System.out.println("Base().val =" + this.val);

           System.out.println("Base.Base()");

           this.display();

           System.out.println(this.getClass());

        }

        public void display(){

           System.out.println("Base.val = " + val);

        }

        public Base(int a){

           System.out.println("Base.Base(int )");

        }

    }

    public class Sub extends Base {

        private int val= 22;

        Sub(){

           System.out.println("Sub.Sub() ");

           val = 222;

        }

        public void display(){

           System.out.println("Sub val="+val);

        }

    }

    public static void main(String[] args) throws ClassNotFoundException {

        Sub sub = new Sub();

    }

    输出结果是:

    Base().val =2首先调用父类的构造函数,其中使用this输出的变量val是在子类中的声明变量val初始化的2

    Base.Base()//首先调用父类的构造函数

    Sub val=0//这里我们就有点凌乱了,为什么是0,整理一下,首先是初始化类的构造函数,因为集成,所以首先是执行的父类的初始化,所以在上一条中我们输出的结果是在父类中初始化代码块的2,之后我们在父类中使用this直接调用成员变量的话,那么是调用的Base类的成员变量所以显示的是2,但是我们在后面使用的是调用函数,那么就会有多态问题的出现,这个时候调用函数就是更具具体对象的类型去调用函数。所以这里使用this.display()会根据类的多态调用的是子类中的函数。但是在这个时候,我们子类对象知识分配了内存空间,而没有初始化内存,所以这个时候没有执行到子类的成员变量的初始化,但是我们调用子类的成员变量当然是没有初始化的值0.

    class yang.main.Sub//我们在程序父类中输出类的值,会发现this指针实际上是子类。

    Sub.Sub()

             在这里我们在整理一个概念:Java对象在内存中的空间并不是有构造代码块实现的内存分配,在构造代码块执行之前,其实对象在内存中的空间已经分配,构造代码块完成的是对内存区域的初始化工作。但是在分配内存空间的时候,没有初始化,默认值都是0.,对于应用类型的变量则是NULL。

    总结:当变量编译时的类型和运行时类型是不同的,通过变量访问它的而引用对象的实例变量的时候,该实例变量的值是由声明该变量的类型决定的。但是通过该变量弟阿勇他引用对象的成员函数的时候,则会根据他实际的类型确定的。

    2.4父类实例的内存控制

    public class Base {

        public int val = 2;

        public void display(){

           System.out.println("Base.val = " + val);

        }

    }

    public class Sub extends Base {

        public  int val= 22;

        public void display(){

           System.out.println("Sub val="+val);

        }

    }

    public static void main(String[] args) throws ClassNotFoundException {

        Base b = new Base();

        System.out.println(b.val); //2

        b.display(); // 2

       

        Sub sub = new Sub();

        System.out.println(sub.val);//22

        sub.display();//22

       

        Base btod = new Sub();

        System.out.println(btod.val);//2

        btod.display();//22

       

        Base btod2 = sub;

        System.out.println(btod2.val);//2

        btod2.display();//22

    }

    总结:不管声明对象是哪一种类型的,只要他们实际指向的是一个子类,那么他调用方法就会将多态体现出来;但是如果调用的是成员变量,那么变量的值总是和声明这些对象的类型一致。

    对于继承的话,其实继承了父类中的所有的函数和成员变量,但是因为在访问权限上会做一些限制,其实子类在内存中仅仅有一个对象,但是对于父类中的内容是被隐藏掉。

    Java程序中允许出现return this的语句但是不会允许出现 return super,因为在Java中不允许直接将super当成一个引用变量使用。

        如果在子类中定义了父类中已经定义的变量,这样在Java是允许的,但是会在子类中隐藏掉,我们可以使用super关键字访问

    class  Parent{public String tag = “yang”;}

    class Derived extends Parent{ public String tag = “teng”;}

    Main:

    Derived d = new Derived();

    System.out.println(d.tag);// complie error

    System.out.println(((Parent)d).tag); right  yang ;

    2.4final 修饰符

    2.4.1final 修饰变量

    被final修饰的实例变量必须显示的指定初始化值,而且只能够在三个位置指定初始化的值

        定义final实例变量的时候指定初始值;在非静态代码块中为final实例变量指定初始化值;在构造函数中初始化值。对于final实例变量JVM无法默认初始化值,因此必须有程序员初始化。

    对于final 静态变量,就是是使用static声明的变量的话,那么只能在两个地方进行初始化,一个是静态代码块中,一个是声明的地方。

    2.4.2执行宏替换

        使用final声明的变量在编译阶段的话会执行宏替换,类似C中的define

        再有就是Java会缓存所有的字符串常量,如执行String a = “yang”; String b = “yang”; a==b  is true ,因为Java的字符串缓冲池的作用,其实指向的是同一个对象的地址。字符串的话如果可以在编译阶段 就可以确定的字符串,那么就会直接进行编译优化,进行替换,前提是在表达式中不存在变量,全部都是常量。

    2.4.3final方法是不可以被重写的

    class A{ final void funA(){}}

    class B extends A {void funA(){} //error}

    追梦的飞飞

    于广州中山大学图书馆 20131028

    HomePage: http://yangtengfei.duapp.com

  • 相关阅读:
    Linux学习——操作文件与目录
    链表去重
    Android主题换肤 无缝切换
    android 换肤模式总结
    NotificationListenerService不能监听到通知
    判断app是否在后台
    模拟接听电话的方法,兼容华为android5.0以上设备
    Windows下安装破解JIRA6.3.6
    一些as的配置
    Android BLE开发——Android手机与BLE终端通信初识
  • 原文地址:https://www.cnblogs.com/hbhzsysutengfei/p/3409555.html
Copyright © 2020-2023  润新知