Java笔记_0
面向对象的特性
封装
- 将数据和行为组合在一个包中, 并对使用者隐藏具体的实现细节
- 关键在于不能让别的类调用到当前类的实例字段
继承
- 实现了
IS-A
的关系, 子类可以获得父类非private
的属性和方法 - 应该要遵循里氏替换原则
- 里氏替换原则: 子类必须能够替换掉所有的父类对象, 父类引用指向子类对象称为向上转型
举例
Cat
和Animal
就是一种IS-A
关系, 所以Cat
可以继承Animal
, 并且获得Animal
的所有非private
的属性和方法Cat
可以当做Animal
使用, 所以Animal
可以引用Cat
对象:Animal a = new Cat()
, 这就是向上转型
类常见的关系
- 依赖
USES-A
: 只要一个类需要使用或操作另一个类, 就说明前者依赖后者. 需要尽可能减少相互依赖的类, (减少耦合) - 聚合
HAS-A
: 类A的对象包含类B的对象 - 继承
IS-A
多态
- 分为编译时多态和运行时多态
- 编译时多态: 指方法的重载
- 运行时多态: 指对象引用的具体类型在运行期间才能确定
- 运行时多态具有三个条件: 继承、覆盖(重写)、向上转型
- 重载指的是多个方法具有相同的方法名, 以及不同的参数(或者参数顺序). 编译器查找匹配更适合的方法的过程是重载解析
- 所以如果需要完整的描述一个方法, 则需要他们的方法名, 参数类型, 称为方法的签名
- 返回类型不是签名的一部分, 所以重载不允许存在(方法名相同, 参数类型相同, 但返回值不同的多个函数)
JVM
功能
- 解释运行:
javac
编译源代码, 得到class
字节码,JVM
将其实时解释成机器码, 让计算机执行, 这是为了实现跨平台 - 内存管理: 自动为对象方法分配内存, 自动垃圾回收不再使用的对象
- 即时编译
JIT
:对热点代码进行优化, 提升执行效率
组成
- 类加载器
- 运行时数据区域(
JVM
管理的内存) - 执行引擎(即编译器, 解释器, 垃圾回收等)
- 本地接口
字节码文件
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 包名.类名
可以将字节码反编译成源代码, 然后可以确定代码是否可以满足要求. 比如可以确定版本
组成
- 基本信息: 魔数, 版本号, 访问标识(
public final
), 父类和接口 - 常量池: 字符串常量, 类或接口名、字段名, 主要在字节码指令中使用
- 字段: 类或者接口声明的字段信息
- 方法: 类或者接口声明的方法信息
- 属性: 类的属性, 比如源码的文件名、内部类的列表
- 字节码文件, 文件头是
0xCAFEBABE
, 这个就是魔数 主版本号 - 44
就是1.2
版本之后的大版本计算方法, 比如主版本号52
是JDK8
, 主版本号为61
则是JDK17
- 常量池作用: 避免相同的内容重复定义, 节省空间
- 符号引用: 字节码指令通过编号引用到常量池的过程
- 方法:
iconst_<i>
就是将i
放在操作数栈中istore_<i>
将操作数栈中的数字放到局部变量表的i
号位置iload_<i>
将局部变量表中的数据复制一份放到操作数栈中iadd
将操作数栈顶的两个数字相加, 只保留一个数据iinc <i> by <j>
将局部变量表中的i
号位置的元素加j
, 直接在局部变量表上操作- 局部变量数组表就是一个数组, 存放所有的局部变量, 并且位置
0
是args
参数
i = i + 1
VS i ++
VS i += 1
VS ++ i
1 | iconst_0 |
- 除了
i = i + 1
以外, 都只需要一条指令iinc 1 by 1
, 即可完成自增操作 i = i + 1
需要四条指令才能完成, 即12~15
类的生命周期
- 加载
- 连接
- 验证
- 准备
- 解析
- 初始化
- 使用 (
new
) - 卸载 (
GC
)
加载
- 类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
- 类加载器加载完类以后, 会将字节码信息保存到内存的方法区中
- 生成一个
InstanceKlass
对象, 保存所有类的信息, 包含特定功能比如多态的信息(虚方法表) - 在堆区中生成一份与方法区数据类似的
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
命令查找
连接
验证
- 验证内容是否满足
JVM
规范, 比如文件头是否满足CAFEBABE
, 主次版本号是否满足要求 - 验证元信息, 比如类必须要有父类,
super
不能为空 - 验证程序语义是否正确, 不能跳转到不正确的位置
- 符号引用验证, 比如是否访问了其他类中的
private
方法
1 | // 检查版本号是否满足要求 |
准备
- 给静态变量分配内存和设置初始值
- 设置初始值是所有的变量都赋值为
0
, 或者null
, 或者'\u0000'
- 只有用
final
修饰的变量, 会将其直接赋值为定义的值, 因为final
的值不会修改了
解析
- 将符号引用替换为直接引用, 就是使用地址替换编号
初始化
public static int v = 1;
这句代码在连接准备阶段会将v = 0
, 然后在初始化阶段会v = 1
- 初始化阶段就是执行静态代码块中的代码, 为静态变量赋值, 执行字节码文件中
clinit
部分的字节码指令 putstatic
从操作数栈中获取值, 设置静态变量clinit
方法执行顺序跟Java
中的编写顺序一样- 使用
-XX:+TraceClassLoading
参数可以打印出加载并初始化的类
1 | public class D{ |
触发初始化:
- 访问一个类的静态变量或者静态方法, 如果变量是
final
修饰的并且等号右边是常量, 则不会触发初始化(在准备阶段就已经完成赋值了) - 调用
Class.forName(String className)
, 如果只传入类名, 那么会默认进行初始化, 也可以传入参数指定不初始化 new
一个类的对象- 执行
main
方法的当前类
1 | public class Test1 { |
不触发初始化:
- 没有静态代码块, 并且没有静态变量赋值语句
- 有静态变量声明, 但是没有赋值语句
- 静态变量使用
final
关键字, 等号右边是常量, 会在准备阶段直接进行赋值, 则不会有初始化
- 直接访问父类的静态变量, 不会触发子类的初始化
- 子类的初始化
clinit
调用之前, 会先调用父类的clinit
初始化方法
1 | public class Test1 { |
- 访问父类的静态变量不需要初始化子类,初始化子类之前一定会初始化父类
1 | public class Test1 { |
1 | public class Test1 { |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sangs Blog!