目录
1 程序计数器(Program Counter Register)
一、所有变量
(注意:实例就是被 new 出来的对象)
1 成员变量
- 成员变量声明在一个类中,但在方法、构造方法和语句块之外。
- 成员变量包括:实例变量(没有static修饰的)、类变量(有static修饰的)。
- 成员变量无需显式初始化,系统会自动对其进行默认初始化。
1.1 实例变量
- 实例化对象存在,则该对象的实例变量存在,就可以访问实例变量;实例化对象被销毁,则该对象的实例变量销毁。
- 当一个对象被实例化之后,每个实例变量的值就跟着确定。
- 每创建一个某类的实例化对象,系统将再次为实例变量分配内存,并执行初始化。
- 实例变量则属对象私有,某一个对象将其所包含的实例变量的值改变,不影响其他对象中实例变量的值。
- 实例变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是false,引用类型变量的默认值是null。
- 访问方式:对象.实例变量(只要对象存在,实例就可以访问实例变量)。
- 存放位置:实例变量随着对象的建立而存在于堆内存中。
- 生命周期:实例变量生命周期随着对象的消失而消失。
- 程序可以在3个地方对实例变量执行初始化:
- 定义实例变量时指定初始值
- 非静态初始化块中对实例变量指定初始值
- 构造器中对实例变量指定初始值
1.2 类变量(实例变量)
- 类存在,则类变量存在;类被销毁,则类变量被销毁。
- 只要类存在,类就可以访问类变量。
- 类优先于对象存在,所以类变量的初始化时机总是处于实例变量的初始化时机之前。
- 类变量是所有该类的实例化对象所共有的资源,其中一个对象将它值改变,其他对象得到的就是改变后的结果。
- 当类初始化完成之后,类变量也随之初始化完成,以后不管程序创建多少个该类的对象,系统也不再为类变量分配内存。
- 访问方式:类.类变量(只要类存在,类就可以访问类变量)。
- 存放位置:类变量随着类的加载而存在于方法区中。
- 生命周期:类变量生命周期最长,随着类的消失而消失。
- 注意1:静态方法只能访问静态成员,不可以访问非静态成员。因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员。
- 注意2:静态方法中不能使用this,super关键字。因为this代表对象,而静态在时,有可能没有对象,所以this无法使用。
- 程序可以在2个地方对类变量执行初始化:
- 定义类变量时指定初始值
- 静态初始化块中对类变量指定初始值。
pubulic class Duck {
int size;
static int shape;
}
//size是实例变量,shape是类变量
2 局部变量
- 局部变量声明在一个方法中。
- 局部变量包括:形参(形式参数)、方法局部变量 (方法内定义)、代码块局部变量 (代码块内定义)。
- 局部变量除了形参外,都必须显示初始化,也就是要指定一个初始值,否则不能访问。
- 定义了局部变量以后,系统并没有给局部变量进行初始化,直到程序给这个局部变量赋给初值时,系统才会为这个局部变量分配内存空间,并将初始值保存到这块内存中。
- java允许局部变量和成员变量重名,局部变量会覆盖成员变量的值。如果需要在这个方法中引用被覆盖成员变量,则可使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。
- 形参:在整个方法内有效。
- 方法局部变量: 从定义这个变量开始到方法结束这一段时间内有效。
- 代码块局部变量 :从定义这个变量开始到代码块结束这一段时间内有效。
public void foo(int x){
int i = x + 3;
boolean b = ture;
}
//参数x和变量i,b都是局部变量
总结:
尽管三者的本质都是变量,可是使用时却有相当大的区别,稍不留神就可能陷入陷阱。且先记住:在一个类中,如果一个变量能够用来描述一个类的属性,那就定义为成员变量,否则,它就应该定义为局部变量。而如果一个变量在全局中都能使用(某个内容是被所有对象所共享),那么我们就可以把这个变量用static来修饰,即为静态变量。
二、内存分配
1 程序计数器(Program Counter Register)
- 程序计数器,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器来完成。
- 这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部。但是寄存器的数量极其有限,所以寄存器由编译器根据需求进行分配,无法直接人为控制,也不能在程序中感觉到寄存器存在的任何迹象。
2 虚拟机栈(VM Stack)
- 位于位于通用 RAM 中,通过堆栈指针可以从处理器获得直接支持。
- 创建程序时候,JAVA编译器必须知道存储在堆栈内所有数据的确切大小和生命周期,因为它必须生成相应的代码,以便上下移动堆栈指针。堆栈指针向下移动,则分配新的内存;向上移动,则释放那些内存。
- 存储方式速度仅次于寄存器。
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(用于存放局部变量表、操作数栈、动态链表(动态链接的作用主要还是提供栈里面的对象在进行实例化的时候,能够查找到堆里面相应的类地址,并进行引用。这一整个过程,我们称之为动态链接)、方法出口信息(某个子方法执行完毕之后,需要回到主方法的原有位置继续执行程序,方法出口主要就是记录该信息)等)。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表(通常说的栈其实是栈里面的局部变量表):基本类型的变量(int,long,short,byte,float,double,boolean,char)对象的引用变量。
- 栈是先进后出。
- 栈内存的数据用完就释放(当在代码块中定义一个变量时,java栈就为这个变量分配适当的内存空间,当该变量退出作用域时,java栈会释放该变量的内存空间。)
- 每个线程包含一个栈区(stack),每个栈中的数据都是私有的,其他栈不能访问。
- 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)
- 如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
- 三种释放对象引用变量的方式:
1、引用变量永久性的离开它的范围:
void go() {
Life z = new Life();
}
//Z会在方法结束时消失
2、引用变量被赋值到其他的对象上:
Life z = new Life();
Z = new Life();
//第一个对象会在Z被赋值到别处时挂掉
3:直接将引用设定为null:
Life z = new Life();
Z = null;
//第一个对象会在Z被赋值为null时挂掉
3 本地方法栈(Native Method Stack)
- 本地方法栈与虚拟机的作用相似,不同之处在于虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。
- 会抛出StackOverflowError和OutOfMemoryError异常。
4 堆(heap)
- 位于通用 RAM 当中,其中存放的数据由JVM自动进行管理。
- Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。
- 堆不同于堆栈的好处是:编译器不需要知道要从堆里分配多少存储区域,也不必知道存储的数据在堆里存活多长时间。
- 当创建一个对象的时候,只需要 new 写一行简单的代码,当执行这行代码时,会自动在堆里进行存储分配。
- 存放 new 出来的数组或对象。
- jvm 只有一个堆区(heap),被所有线程共享。
- 在 java 堆中产生了一个数组 or 对象后,可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组 or 对象在堆内存中的首地址,栈中的这个变量就成了数组 or 对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。
- !!!!需要注意的是我们通过 new 关键字创建出来的对象在堆内存中只有该对象的成员变量,而不包括成员方法。因为每一个类的对象都有各自的成员变量存储在各自的堆内存中,但是它们共享该类的成员方法,所以并不是每一次 new 都会创建成员方法!!!!
- 对象使用方法的时候,方法才被压入栈,方法不使用则不占用内存。
- 即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存也不会被释放,只有数组和对象在没有引用变量指向它的时候才会变成垃圾,但是并没有立即回收,仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉,这个也是Java比较占内存的主要原因。
- 如果堆中没有内存完成实例分配,并且堆也无法完成扩展时,将会抛出OutOfMemoryError异常。
5 方法区(Method Area)
- 方法区被所有的线程共享。
- 方法区存放已被虚拟机加载的类信息(包括方法!!!!)、常量、静态变量、即时编译器编译后的代码等数据。
- 方法区中存放的都是在程序中永远的唯一的元素。
- 运行时常量池:是方法区的一部分,它用于存放编译期生成的各种字面量(如文本字符串,声明为final的常量值等。)和符号引用(类和接口的全限定名、字段的名称和描述符、方法和名称和描述符。)。(常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。)
- 当方法区无法满足内存分配需要时,将抛出OutOfMemoryError异常。
(静态变量和常量的区别: 静态变量本质是变量,是整个类所有对象共享的一个变量,其值一旦改变对这个类的所有对象都有影响;常量一旦赋值后不能修改其引用,其中基本数据类型的常量不能修改其值。)
例题1:
public class Test {
public static int a1=0;
public static void Demo(){
int a2=1;
Test test2=new Test();
}
}
// 变量 a1 和方法 Demo()存放在方法区
// 变量 a2 和对象的引用 test2 存放在栈区
// 而通过 new Test()创建的对象将存放在堆区
例题2:JVM如何管理内存,分成几个部分?分别有什么用途?说出下面代码的内存实现原理:
Foo foo = new Foo();
foo.f();
参考答案:
JVM内存分为“堆”、“栈”和“方法区”三个区域,分别用于存储不同的数据。
堆内存用于存储使用new关键字所创建的对象;
栈内存用于存储程序运行时在方法中声明的所有的局部变量;
方法区用于存放类的信息;类的各种信息(包括方法)都在方法区存储。
Java程序运行时,首先会通过类装载器载入类文件的字节码信息,经过解析后将其装入方法区。
Foo foo = new Foo();
foo.f();
以上代码的内存实现原理为:
1.Foo类首先被装载到JVM的方法区,其中包括类的信息,包括方法和构造等。
2.在栈内存中分配引用变量foo。
3.在堆内存中按照Foo类型信息分配实例变量内存空间;然后,将栈中引用foo指向foo对象堆内存的首地址。
4.使用引用foo调用方法,根据foo引用的类型Foo调用f方法。
三、构造器
默认格式:
[修饰符] 类名 () { }
有参数格式:
[修饰符] 类名 (形参列表) { }
- 构造器与类同名,包括大小写。
- 构造器没有返回值,但也不能写void,也不能写return。
- 构造可以有0、1或多个参数;
- 每个类可以有一个以上的构造器,但参数列表必须不同(即重载)。
- 构造器总是伴随着new操作一起调用。
- 如果一个类中没有构造方法,那么编译器会为类加上一个默认的构造方法(无参数,方法体为空),且默认构造器的访问权限随着所属类的访问权限变化而变化。
- 构造器的作用:初始化成员属性(即实例变量)。
- 构造器的访问权限可以为任意,但是一般情况下使用public,不能被static、final、synchronized、abstract和native修饰。
- 构造器是可以被private修饰的,作用:其他程序无法创建该类的对象。
- 类一定有构造器!
- 默认构造器是看不到的,一旦自己写上构造器则默认构造器就没有了,自己写的叫自定义构造器,即便自己写的是空参数的构造器,也是自定义构造器,而不是默认构造器。
- 如果类声明了构造器,Java编译器将不再提供默认构造器。若没手动写出无参构造器,但却调用了无参构造器,将会报错!
- 构造方法不能被子类继承。
- 构造器在执行的时候,第一件事是去执行它的父类的构造器,这会连锁反应到Object这个类为止。
- 子类继承父类,那么子类型构造器默认调用父类型的无参数构造器。(任何我们自己定义的类,都应该显式的加上默认的构造器:因为类中显式的声明了一个带参数构造器,所以默认的构造器就不存在了,但是你在子类的构造器中并没有显式的调用父类的构造器(创建子类对象的时候,一定会去调用父类的构造器,这个不用问为什么),没有显式调用的话,虚拟机就会默认调用父类的默认构造器,但是此时你的父类的默认构造器已经不存在了,这也就是为什么父类中必须保留默认构造器的原因。)
- 创建子类对象会调用器构造方法但不会创建父类对象,只是调用父类构造器初始化父类成员属性。
- 有了构造方法之后可以对对象的属性进行初始化,那么还需要对应的set和get方法吗?
- 答:需要相应的set和get方法,因为对象在创建之后需要修改和访问相应的属性值时,在这时只能通过set或者get方法来操作。
- 构造方法和一般方法区别:
- 构造方法在对象创建时就执行了,而且只执行一次。
- 一般方法是在对象创建后,需要使用时才被对象调用,并可以被多次调用。
- 注意事项:sper()、sper.和 this()、this.
- this或super调用构造器只能出现在构造器中,而且必须出现在第一行。
- 一个构造方法中第一行只能为this或super调用构造方法,两者不能同时调用构造方法出现。
- this:在运行期间,哪个对象在调用this所在的方法,this就代表哪个对象,隐含绑定到当前“这个对象”。
- this():调用本类的其他构造器,按照参数调用构造器,必须在构造器中使用,必须在第一行使用,this() 与 super() 互斥,不能同时存在。
- this.是访问当前对象,本类对象的引用,在能区别实例变量和局部变量时,this可省略,否则一定不能省!
- super():调用父类无参构造器,一定在子类构造器第一行使用!如果没有则是默认存在super()的!这是Java默认添加的super()。
- super.是访问父类对象,父类对象的引用,与this.用法一致
- 如果子父类中出现非私有的同名成员变量时,子类要访问本类中的变量用this. ;子类要访问父类中的同名变量用super.