面向对象
面向对象编程的英文缩写是 OOP,全称是 Object Oriented Programming。对应地,面向对象编程语言的英文缩写是 OOPL,全称是 Object Oriented Programming Language。
面向对象编程中有两个非常重要、非常基础的概念,那就是类(class)和对象(object)。
面向对象编程从字面上,按照最简单、最原始的方式来理解,就是将对象或类作为代码组织的基本单元,来进行编程的一种编程范式或者编程风格。
⾯向对象 : ⾯向对象易维护、易复⽤、易扩展。 因为⾯向对象有封装、继承、多态性的性质,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是, ⾯向对象性能⽐⾯向过程低。
类
具有相同或相似性质的一组对象的抽象就是类,类是对一类事物的描述,是抽象的、概念上的定义;对象是实际存在的该类事物的个体。
对象的抽象是类,类的具体化就是对象。
对一个定义类而言,可以包含三种最常见的成员:构造器、成员变量和方法,三种成员都可以定义零个或多个。
类的加载
加载->连接->初始化->使用->卸载
加载
- 通过全限定类名来获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
连接
验证
这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。 - 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
第二阶段,保证不存在不符合 Java 语言规范的元数据信息。 - 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
- 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。
准备
为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化
到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 <clinit>()
方法的过程。
<clinit>()
方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。(不包括构造器中的语句。构造器是初始化对象的,类加载完成后,创建对象时候将调用的 <init>()
方法来初始化对象)
<clinit>()
方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>()
方法。
使用
卸载
类的唯一性和类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
双亲委派模型
类加载器在加载Java类时会采用双亲委派模型:当类加载器在加载java类时,会先委派给父类加载,层层上传,最后会把加载任务委派给启动类加载器。只有父类类加载器无法完成这个加载任务时(搜索范围中没有找到对应的类),子加载器才会尝试自己去加载。 所以是一个向上传递再向下传递的过程。
双亲委派模型对于保证 Java 程序的稳定运作很重要,例如类 java.lang.Object
,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。
对象
对象的创建过程
- 类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始化过。如果没有,那必须先执行相应的类加载过程。
- 内存分配:在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种。
指针碰撞
- 假设java堆中的内存分配是绝对规则的。指针作为已分配内存和未分配内存的分界线;
- 给变量分配的内存的过程将作为分界线的指针向未分配空间挪动一个变量的大小。
空闲列表
- 如果java堆中的内存不是 规整的,虚拟机就会维护一个空闲列表,用来记录剩余的可用的内存空间;
- 每次为变量分配内存后会动态的维护这个列表。
- 初始化零值:内存分配完成后,虚拟机需要将分配到的空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在java代码中可以不赋初始值直接使用,程序能访问到的这些字段的数据类型所对应的零值。
- 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 执行init方法:在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始, <init> ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ <init> ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。
对象的内存布局
对象头
- 用于存储对象自身的运行时数据(Mark Word)。如:哈希码(hashcode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 类型指针,即对象指向它的类型元数据的指针,java虚拟机通过这个指针来确定该对象是哪个类的实例。
实例数据
我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。
对齐填充
这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
对象的访问方式
句柄: 如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池, reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;
直接指针: 如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。
堆
存储实例对象
优势:存取速度快,仅次于寄存器,栈数据可以共享
缺点:存在栈的数据大小和生存期是确定的,缺乏灵活性
栈
存储基本类型、引用类型变量、方法