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

上一篇:搭建一个简单的Struts2框架


下一篇:javascript-函数的参数和return语句