Skip to content

Commit 2a238b7

Browse files
committed
Update Java Note
1 parent c601509 commit 2a238b7

File tree

2 files changed

+33
-34
lines changed

2 files changed

+33
-34
lines changed

DB.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6776,7 +6776,6 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fs
67766776
* 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。日志已经在操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的
67776777
* 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务**
67786778
* 服务器关闭时
6779-
* checkpoint 时(下小节详解)
67806779
* 并行的事务提交(组提交)时,会将将其他事务的 redo log 持久化到磁盘。假设事务 A 已经写入 redo log buffer 中,这时另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘,**因为多个事务共用一个 redo log buffer**,所以一次 fsync 可以刷盘多个事务的 redo log,提升了并发量
67816780

67826781
服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘
@@ -10795,7 +10794,7 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的
1079510794

1079610795
内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略:
1079710796

10798-
* 空间预分配:当 SDS需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间
10797+
* 空间预分配:当 SDS 需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间
1079910798

1080010799
* 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等
1080110800

@@ -12096,6 +12095,11 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不
1209612095

1209712096
当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn)
1209812097

12098+
为什么用跳表而不用平衡树?
12099+
12100+
* 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找
12101+
* 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针
12102+
1209912103

1210012104

1210112105
***
@@ -12651,12 +12655,12 @@ appendfsync always|everysec|no #AOF写数据策略:默认为everysec
1265112655
特点:安全性最高,数据零误差,但是性能较低,不建议使用
1265212656

1265312657

12654-
- everysec:先将 aof_buf 缓冲区中的内容写入到 AOF 文件,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次对 AOF 文件进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的
12658+
- everysec:先将 aof_buf 缓冲区中的内容写入到操作系统缓存,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的
1265512659

1265612660
特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置
1265712661

1265812662

12659-
- no:将 aof_buf 缓冲区中的内容写入到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统来决定
12663+
- no:将 aof_buf 缓冲区中的内容写入到操作系统缓存,但并不进行同步,何时同步由操作系统来决定
1266012664

1266112665
特点:**整体不可控**,服务器宕机会丢失上次同步 AOF 后的所有写指令
1266212666

@@ -14620,7 +14624,7 @@ SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
1462014624

1462114625
### 领头选举
1462214626

14623-
主服务器被判断为客观下线时,监视这个主服务器的各个 Sentinel 会进行协商,选举出一个领头 Sentinel 对下线服务器执行故障转移
14627+
主服务器被判断为客观下线时,**监视该主服务器的各个 Sentinel 会进行协商**,选举出一个领头 Sentinel 对下线服务器执行故障转移
1462414628

1462514629
Redis 选举领头 Sentinel 的规则:
1462614630

@@ -14748,7 +14752,7 @@ typedef struct clusterState {
1474814752
int state;
1474914753

1475014754
// 集群中至少处理着一个槽的(主)节点的数量,为0表示集群目前没有任何节点在处理槽
14751-
// 【选举时投票数量超过半数,可以从这里获取的
14755+
// 【选举时投票数量超过半数,从这里获取的
1475214756
int size;
1475314757

1475414758
// 集群节点名单(包括 myself 节点),字典的键为节点的名字,字典的值为节点对应的clusterNode结构
@@ -16135,6 +16139,13 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解
1613516139
- 在抢购或秒杀场景下,可能因商品对应库存 Key 的请求量过大,超出 Redis 处理能力造成超卖
1613616140
- 热 Key 的请求压力数量超出 Redis 的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务
1613716141

16142+
热 Key 分类两种,治理方式如下:
16143+
16144+
* 一种是单一数据,比如秒杀场景,假设总量 10000 可以拆为多个 Key 进行访问,每次对请求进行路由到不同的 Key 访问,保证最终一致性,但是会出现访问不同 Key 产生的剩余量是不同的,这时可以通过前端进行 Mock 假数据
16145+
* 一种是多数据集合,比如进行 ID 过滤,这时可以添加本地 LRU 缓存,减少对热 Key 的访问
16146+
16147+
16148+
1613816149

1613916150

1614016151
参考文档:https://help.aliyun.com/document_detail/353223.html

Java.md

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11497,7 +11497,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提
1149711497
- 主要不足是**只使用了内存的一半**
1149811498
- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小
1149911499

11500-
现在的商业虚拟机都采用这种收集算法**回收新生代**,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间
11500+
现在的商业虚拟机都采用这种收集算法**回收新生代**,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间
1150111501

1150211502

1150311503

@@ -11713,7 +11713,7 @@ CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿
1171311713

1171411714
- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快
1171511715
- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行
11716-
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况)
11716+
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况)
1171711717
- 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的
1171811718

1171911719
Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的**对象的地址改变**,影响用户线程继续执行
@@ -11785,7 +11785,7 @@ G1 对比其他处理器的优点:
1178511785
- 空间整合:
1178611786

1178711787
- CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理
11788-
- G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片
11788+
- G1:整体来看是**基于标记 - 整理算法实现**的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片
1178911789

1179011790
- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒
1179111791

@@ -12544,7 +12544,7 @@ Java 对象创建时机:
1254412544

1254512545
- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码)
1254612546
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构(Java 类模型)
12547-
- **在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口**
12547+
- **将字节码文件加载至方法区后,在堆中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口**
1254812548

1254912549
其中二进制字节流可以从以下方式中获取:
1255012550

@@ -12553,8 +12553,6 @@ Java 对象创建时机:
1255312553
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类
1255412554
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码
1255512555

12556-
将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象
12557-
1255812556
方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构:
1255912557

1256012558
* `_java_mirror` 即 Java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用
@@ -12641,7 +12639,7 @@ Java 对象创建时机:
1264112639
public static final int value = 123;
1264212640
```
1264312641

12644-
* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false
12642+
* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,故 boolean 的默认值就是 false
1264512643

1264612644

1264712645

@@ -12794,7 +12792,7 @@ new 关键字会创建对象并复制 dup 一个对象引用,一个调用 <ini
1279412792

1279512793
#### 卸载阶段
1279612794

12797-
时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止
12795+
时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java 虚拟机进程终止
1279812796

1279912797
卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求:
1280012798

@@ -12944,7 +12942,7 @@ ClassLoader 类常用方法:
1294412942

1294512943
- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入
1294612944

12947-
- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载
12945+
- **双亲委派:**某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载
1294812946

1294912947
- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区(方法区)中
1295012948
- 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因
@@ -12981,7 +12979,7 @@ ClassLoader 类常用方法:
1298112979
}
1298212980
```
1298312981

12984-
此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法
12982+
此时执行 main 函数会出现异常,在类 java.lang.String 中找不到 main 方法。因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法
1298512983

1298612984
双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性)
1298712985

@@ -13056,9 +13054,9 @@ protected Class<?> loadClass(String name, boolean resolve)
1305613054
* 如果不想破坏双亲委派模型,只需要重写 findClass 方法
1305713055
* 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法
1305813056

13059-
* 引入线程**上下文类加载器**
13057+
* 引入**线程上下文类加载器**
1306013058

13061-
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类:
13059+
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类:
1306213060

1306313061
* SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的
1306413062
* SPI 的实现类是由系统类加载器加载,引导类加载器是无法找到 SPI 的实现类,因为双亲委派模型中 BootstrapClassloader 无法委派 AppClassLoader 来加载类
@@ -14471,14 +14469,9 @@ public static int invoke(Object... args) {
1447114469
在 JVM 中,将符号引用转换为直接引用有两种机制:
1447214470

1447314471
- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段)
14474-
- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段)
14475-
14476-
对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次:
14472+
- 动态链接:被调用的方法在编译期无法被确定下来,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段)
1447714473

14478-
- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定
14479-
- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法
14480-
14481-
* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载
14474+
* 对应方法的绑定(分配)机制:静态绑定和动态绑定,编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载
1448214475

1448314476
非虚方法:
1448414477

@@ -14512,7 +14505,7 @@ public static int invoke(Object... args) {
1451214505
普通调用指令:
1451314506

1451414507
- invokestatic:调用静态方法
14515-
- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法
14508+
- invokespecial:调用私有方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法
1451614509
- invokevirtual:调用所有虚方法(虚方法分派)
1451714510
- invokeinterface:调用接口方法
1451814511

@@ -14544,9 +14537,6 @@ public static int invoke(Object... args) {
1454414537

1454514538
在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符
1454614539

14547-
* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针
14548-
* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引
14549-
1455014540
符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用:
1455114541

1455214542
```java
@@ -14648,8 +14638,6 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类
1464814638

1464914639
2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常
1465014640

14651-
IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变
14652-
1465314641
3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程
1465414642

1465514643
4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常
@@ -14673,15 +14661,15 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类
1467314661

1467414662
虚方法表的执行过程:
1467514663

14676-
* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法
14677-
* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针)
14664+
* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针
14665+
* 对于动态绑定的方法调用而言,实际引用是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针)
1467814666

1467914667
为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法
1468014668

1468114669
方法表满足以下的特质:
1468214670

1468314671
* 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同
14684-
* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法
14672+
* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**,这就是为什么多态情况下可以访问父类的方法
1468514673

1468614674
<img src="https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-虚方法表.png" style="zoom: 80%;" />
1468714675

0 commit comments

Comments
 (0)