一、对象和类的存储

根据java虚拟机规范第七版的规定,Java虚拟机所管理的内存将包括以下几个运行时数据区域:程序计数器方法区虚拟机栈本地方法栈。(详见深入理解java虚拟机)

clipboard.png

1. 程序计数器(Program Counter Register)

  程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2. Java虚拟机栈

  与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。
  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型),它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

3. 本地方法栈

  本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,也是线程私有的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

4. Java堆

  对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
  Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java堆中的上述各个区域的分配和回收等细节将会是下一章的主题。
 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展
时,将会抛出OutOfMemoryError异常。

5. 方法区

方法区的存储内容

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(例如运行时常量池(Runtime Constant Pool)、字段和方法数据构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法<init>(§2.9))、常量静态变量即时编译器编译后的代码等数据,。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
    对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory来实现方法区的规划了。
  Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

    运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)这部分内容将在类加载后存放到方法区的运行时常量池中。即由从class常量池转到了运行时常量池当中。
    字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。对于字面量而言,在Java语言特性的里面的定义是六种基本类型(int、long、char、boolean、float、double)、字符串类型、null类型的值的源码(即.java里的)表示形式,这里的基础类型少了byte和short,是因为整型字面量只有int和long两种类型(An integer literal is of type long if it is suffixed with an ASCII letter L or l (ell);otherwise it is of type int)。那么会进入Class常量池也就是后来会进入运行时常量池的字面量有何要求,经javap测试得到了下面的结论:

  • 首先null是肯定不会进入Class常量池的
  • 对于给包装类型赋值的字面量而言,结果有限的字面量如bool、char、小于3232768的int其值不会被放入到Class常量池当中,加上final或者static也不行。
  • 但对于给基础类型变量赋值的结果有限字面量而言,一旦加上final之后上面那些也都会加入到Class常量池当中。其中char会将对应的int值放进去、bool型则是false放入0,true放入1。
  • 对于结果无限的字面量,如浮点数字面量而言,只要字面量不是0.0或者1.0,那么一定会进入Class常量池,相反则否;字符串字面量则均会进入Class常量池。

符号引用则属于编译原理方面的概念,包括以下三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

    Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。 既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

二、基本类型与引用类型

基本类型

类型 默认值 大小
byte 0 1字节
short 0 2字节
int 0 4字节
long 0L 8字节
float 0.0f 4字节
double 0.0d 8字节
char u0000' 2字节
String (or any object)   null 不定大小
boolean false

  关于boolean类型的大小:涉及到boolean类型的值在编译后都是用int数据类型来代替(即4字节),但是在boolean数组中采用的是byte(即1字节)当将boolean值放入boolean数组时,是要把int类型的值转换成byte再存储,当从boolean数组中取出boolean元素时,则会将byte类型的值扩展为int类型。所有的基本类型都有对应的包装类型

引用类型

  引用类型:Class类型、interface类型、数组类型,其默认值均是null。
  对于数组类型而言,当创建一个数组对象时,实际上就是创建了一个引用数组,并且每个引用都会自动被初始化为一个特定值,该值拥有自己的关键字null。
  重点理解:基本类型变量的值就直接保存在变量中,引用类型变量中保存的只是实际对象的地址。引用指向实际对象,实际对象中保存着内容。
  注意:声明字段时并不总是需要分配值。声明但未初始化的字段将由编译器设置为合理的默认值。一般来说,此默认值将为零或null取决于数据类型。然而,依赖于这样的默认值通常被认为是糟糕的编程风格。局部变量略有不同; 编译器永远不会为未初始化的局部变量分配默认值。如果未在声明局部变量的同时进行初始化,请确保在尝试使用它之前为其赋值。否则如访问未初始化并且之后也未赋值的局部变量将导致编译时错误。

数字类型的高精度计算

  Java提供了两个用于高精度计算的类,BigInteger和BigDecimal。能作用于int和float的操作,也同样能作用于BigInteger和BigDecimal。只不过必须以方法调用方式取代运算符方式来实现。这么做复杂了许多,运算速度也会比较慢,可以说是以速度换取了精度。

  • BigInteger支持任意精度的整数。也就是说,在运算中,可以准确地表达任何大小的整数值,而不会丢失任何信息。
  • BigDecimal支持任何精度的浮点数。

三、 方法、参数和返回值

方法

  方法由下列部分组成,其中只有加粗部分是必须要有的。

  1. Modifiers
  2. The return type
  3. The method name
  4. The parameter list in parenthesis
  5. An exception list
  6. The method body, enclosed between braces

返回值

  在下列三种情况,任何一种先发生时,方法将返回到调用它的代码处

  • 执行方法中的所有语句,
  • 执行了return语句
  • 抛出异常

 
  当方法使用类名作为其返回类型时,返回对象的类型类必须是返回类型的子类或就是该返回类型。在继承时,可以覆盖方法并定义它以返回父类原始方法的子类型,这种技术被称为协变返回类型。此外还可以使用接口名称作为返回类型。在这种情况下,返回的对象必须实现指定的接口。

参数及变量

  • 实例变量/Instance Variables(非静态字段/Non-Static Fields): 类中不加static修饰的成员变量,当从同一个类中创建出多个对象时,每个对象拥有独立的实例变量副本。
  • 类变量/Class Variables(静态字段/Static Fields): 类中加static修饰符的成员变量,所有的对象都能共享该变量,类变量是与类相关联,而不是与任何对象相关联。
  • 局部变量/Local Variables: 在方法中或者块中声明的变量,其作用范围也仅限于方法或块中。
  • 参数/Parameters: 方法声明(method declarations)中的变量,其作用范围仅限于方法中。参数也是变量,但不是类的成员变量,因此不能叫做字段。

 
  类由字段(成员变量)方法(成员函数)组成,成员变量可以分为实例变量和类变量,方法中则包括参数和局部变量。类变量是在类初始化后就存在的,使用类名是访问类变量的首选方式,但也可以使用值为或者不为null的class类型引用变量来访问。如

class StaticTest{
    static int i=47;
    public static void main(String[] args){//main函数当中的args是用来存储命令行参数的。

        StaticTest st=null;
                System.out.println(st.i);//成功打印
    }
}

相关扩展

  • 方法签名方法的名称参数类型列表,参数类型可以更进一步分为形参参数列表泛型方法类型参数列表
  • 方法参数:在方法声明中的变量叫做parameters,在调用方法时传入的实际值叫做arguments ,但两者在中文中都叫做参数,为了更好地区分,将parameters称为形式参数简称形参(列表),将arguments称为实际参数,简称为实参
  • 传递参数:在Java中的方法调用中,无论是传递基础类型实际参数,还是传递引用类型实际参数,都是按值传递的,如何理解这个按值传递,实际上就是等同于赋值操作符“=”的效果

橡皮擦
41 声望4 粉丝