面向对象的特性

封装

  • 将数据和行为组合在一个包中, 并对使用者隐藏具体的实现细节
  • 关键在于不能让别的类调用到当前类的实例字段

继承

  • 实现了IS-A的关系, 子类可以获得父类private的属性和方法
  • 应该要遵循里氏替换原则
  • 里氏替换原则: 子类必须能够替换掉所有的父类对象, 父类引用指向子类对象称为向上转型

举例

  • CatAnimal就是一种IS-A关系, 所以Cat可以继承Animal, 并且获得Animal的所有非private的属性和方法
  • Cat可以当做Animal使用, 所以Animal可以引用Cat对象: Animal a = new Cat(), 这就是向上转型

类常见的关系

  • 依赖 USES-A: 只要一个类需要使用或操作另一个类, 就说明前者依赖后者. 需要尽可能减少相互依赖的类, (减少耦合)
  • 聚合 HAS-A: 类A的对象包含类B的对象
  • 继承 IS-A

多态

  • 分为编译时多态和运行时多态
  • 编译时多态: 指方法的重载
  • 运行时多态: 指对象引用的具体类型在运行期间才能确定
    • 运行时多态具有三个条件: 继承、覆盖(重写)、向上转型
  • 重载指的是多个方法具有相同的方法名, 以及不同的参数(或者参数顺序). 编译器查找匹配更适合的方法的过程是重载解析
    • 所以如果需要完整的描述一个方法, 则需要他们的方法名, 参数类型, 称为方法的签名
    • 返回类型不是签名的一部分, 所以重载不允许存在(方法名相同, 参数类型相同, 但返回值不同的多个函数)

JVM

功能

  1. 解释运行: javac编译源代码, 得到class字节码, JVM将其实时解释成机器码, 让计算机执行, 这是为了实现跨平台
  2. 内存管理: 自动为对象方法分配内存, 自动垃圾回收不再使用的对象
  3. 即时编译JIT:对热点代码进行优化, 提升执行效率

组成

  1. 类加载器
  2. 运行时数据区域(JVM管理的内存)
  3. 执行引擎(即编译器, 解释器, 垃圾回收等)
  4. 本地接口

字节码文件

  • jclasslib可以打开字节码文件.class
  • javap -v 路径名/类名.class > 输出文件命令同样可以反编译.class文件, 并输出到指定文件中
  • jar -xvf可以解压jar
  • Arthas可以使用java -jar arthas-boot.jar命令启动
    • dashbord -i <time (ms)> -n <num> 可以查看服务器各项指标, -i后面指定刷新的时间(ms), -n后面指定刷新的次数
    • dump -d <dir> 包名.类名 可以将指定类名的字节码文件导出到指定的dir
    • jad 包名.类名 可以将字节码反编译成源代码, 然后可以确定代码是否可以满足要求. 比如可以确定版本

组成

  1. 基本信息: 魔数, 版本号, 访问标识(public final), 父类和接口
  2. 常量池: 字符串常量, 类或接口名、字段名, 主要在字节码指令中使用
  3. 字段: 类或者接口声明的字段信息
  4. 方法: 类或者接口声明的方法信息
  5. 属性: 类的属性, 比如源码的文件名、内部类的列表
  • 字节码文件, 文件头是0xCAFEBABE, 这个就是魔数
  • 主版本号 - 44 就是1.2版本之后的大版本计算方法, 比如主版本号52JDK8, 主版本号为61则是JDK17
  • 常量池作用: 避免相同的内容重复定义, 节省空间
    • 符号引用: 字节码指令通过编号引用到常量池的过程
  • 方法:
    • iconst_<i> 就是将i放在操作数栈中
    • istore_<i> 将操作数栈中的数字放到局部变量表的i号位置
    • iload_<i> 将局部变量表中的数据复制一份放到操作数栈中
    • iadd 将操作数栈顶的两个数字相加, 只保留一个数据
    • iinc <i> by <j> 将局部变量表中的i号位置的元素加j, 直接在局部变量表上操作
    • 局部变量数组表就是一个数组, 存放所有的局部变量, 并且位置0args参数

i = i + 1 VS i ++ VS i += 1 VS ++ i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
iconst_0
istore_1
iconst_0
istore_2
iconst_0
istore_3
iconst_0
istore 4
iinc 1 by 1
iinc 2 by 1
iinc 3 by 1
iload 4
iconst_1
iadd
istore 4
return
  • 除了i = i + 1以外, 都只需要一条指令iinc 1 by 1, 即可完成自增操作
  • i = i + 1需要四条指令才能完成, 即12~15

类的生命周期

  1. 加载
  2. 连接
    • 验证
    • 准备
    • 解析
  3. 初始化
  4. 使用 (new)
  5. 卸载 (GC)

加载

  1. 类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
  2. 类加载器加载完类以后, 会将字节码信息保存到内存的方法区
  3. 生成一个InstanceKlass对象, 保存所有类的信息, 包含特定功能比如多态的信息(虚方法表)
  4. 堆区中生成一份与方法区数据类似的java.lang.Class对象, 为了在Java代码中获取类的信息以及存储静态字段的数据(JDK8之后放在堆区)

为什么需要分别在方法区堆区中同时保留两份代码?

  • 方法区中的InstanceKlass对象是用cpp编写的, Java不能直接操作, 所以要在堆区放一个Java编写的java.lang.Class
  • 堆区的字段少于方法区的字段, 因为方法区中有虚方法表, 开发者不需要用, 因此开辟了一块堆区, 只包含了方法的内容, 开发者只能访问堆区,提升安全性
  • JDK自带hsdb工具可以查看JVM的内存信息, hsdb位于安装目录下的lib/sa-jdi.jar
  • java -cp sa-jdi.jar sun.jvm.hotspot.HSDB用于启动hsdb工具, 启动后需要输入进程号, 可以通过jps命令查找

连接

验证

  1. 验证内容是否满足JVM规范, 比如文件头是否满足CAFEBABE, 主次版本号是否满足要求
  2. 验证元信息, 比如类必须要有父类, super不能为空
  3. 验证程序语义是否正确, 不能跳转到不正确的位置
  4. 符号引用验证, 比如是否访问了其他类中的private方法
1
2
3
4
5
6
7
8
9
10
11
12
// 检查版本号是否满足要求
// 检查版本号的具体逻辑:
// 主版本号不能高于运行环境主版本号;
// 如果主版本号相同, 那么副版本号也不能超过
return (major >= JAVA_MIN_SUPPORTED_VERSION)
&& (major <= max_version)
&& ((major != max_version) || (minor <= JAVA_MAX_SUPPORTED_MINOR_VERSION));
// major 主版本号
// minor 副版本号
// JAVA_MIN_SUPPORTED_VERSION 支持的最低版本号, jdk8是45 表示jdk1.0
// max_version 支持的最高版本号, jdk8是52
// JAVA_MAX_SUPPORTED_MINOR_VERSION 支持的最高副版本号, jdk8未使用, 为0

准备

  • 静态变量分配内存和设置初始值
  • 设置初始值是所有的变量都赋值为0, 或者null, 或者'\u0000'
  • 只有用final修饰的变量, 会将其直接赋值为定义的值, 因为final的值不会修改了

解析

  • 将符号引用替换为直接引用, 就是使用地址替换编号

初始化

  • public static int v = 1;这句代码在连接准备阶段会将v = 0, 然后在初始化阶段会v = 1
  • 初始化阶段就是执行静态代码块中的代码, 为静态变量赋值, 执行字节码文件中clinit部分的字节码指令
  • putstatic 从操作数栈中获取值, 设置静态变量
  • clinit方法执行顺序跟Java中的编写顺序一样
  • 使用-XX:+TraceClassLoading参数可以打印出加载并初始化的类
1
2
3
4
5
6
7
8
9
10
public class D{
static {
v = 2;
}
public static int v = 1;
public static void main(String[] args){}
}
// 这段代码最终的v=1, 因为v是static变量, 所以在连接准备阶段v=0
// 经过初始化阶段, v首先经过静态代码块, v=2
// 然后v经过静态设置, v=1

触发初始化:

  1. 访问一个类的静态变量或者静态方法, 如果变量是final修饰的并且等号右边是常量, 则不会触发初始化(在准备阶段就已经完成赋值了)
  2. 调用Class.forName(String className), 如果只传入类名, 那么会默认进行初始化, 也可以传入参数指定不初始化
  3. new一个类的对象
  4. 执行main方法的当前类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test1 {
public static void main(String[] args) {
System.out.println("A");
new Test1();
new Test1();
}

public Test1() {
System.out.println("B");
}
{
System.out.println("C");
}
static {
System.out.println("D");
}
}
// 输出DACBCB
// main方法执行之前, 首先会执行static静态代码块, 因此输出D
// 然后执行main函数, 输出A
// 接下来调用构造方法, 但是字节码中构造代码块比构造函数先执行, 所以输出CB
// 调用了两次构造方法, 因此输出CBCB

不触发初始化:

  1. 没有静态代码块, 并且没有静态变量赋值语句
  2. 静态变量声明, 但是没有赋值语句
  3. 静态变量使用final关键字, 等号右边是常量, 会在准备阶段直接进行赋值, 则不会有初始化
  • 直接访问父类的静态变量, 不会触发子类的初始化
  • 子类的初始化clinit调用之前, 会先调用父类的clinit初始化方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test1 {
public static void main(String[] args) {
new B(); // 如果有这个new B(),则会初始化B,就会优先初始化父类,
// 如果没有new B(),则直接调访问父类的静态变量。
System.out.println(B.a);
}
}

class A{
static {
a = 1;
}
static int a = 0;
}
class B extends A{
static{
a = 2;
}
}
// 如果有new B(), 输出2
// 如果没有new B(), 输出0
  • 访问父类的静态变量不需要初始化子类,初始化子类之前一定会初始化父类
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test1 {
public static void main(String[] args) {
A[] arr = new A[10];
}
}

class A{
static {
System.out.println("AAA");
}
}
// 输出空
// 数组的创建不会导致数组元素中的类初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test1 {
public static void main(String[] args) {
System.out.println(A.a);
}
}

class A{
public static final int a = Integer.valueOf(1);
static {
System.out.println("AAA");
}
}
// AAA 1
// final修饰的变量,如果等号右边需要执行指令才能得出结果,则会执行clinit初始化