架构师训练营第 2 期 第 9 周总结
数据库架构
数据库核心的几个组件: 连接器、语法分析器、语义分析与优化器、执行引擎
1、数据库通过远程连接,拿到提交的 sql 语句,交给语法分析器。
2、语法分析器生成语法树(关系型的树结构),然后对这个语法树进行语义分析,并优化,舍弃不必要的操作,合并可以合并的操作,提高执行效率。优化完后,生成一个执行计划。把执行计划交给执行引擎。
3、执行引擎在数据库存储上,把相关的增删改引擎执行完了。
连接器: 数据库连接器不仅仅是建立连接(TCP 的长连接),为了提高后续数据库操作的效率, 它会为每个连接分配专用的内存空间用于会话上下文管理。这个操作比较重,有资源消耗,且需要花费一定时间,所以程序启动一般会初始化建立一些数据库的连接放在连接池,等待后续调用。
语法分析器:利用提交上来的 SQL 语句构建抽象语法树。如果这个 sql 是有问题的,就会在构建的过程中出错。
语义分析与优化器:语义分析与优化器就是要将各种复杂嵌套的 SQL 进行语义等价转化,得到有限几种关系代数计算结构(选择、投影、交、积、连接),并利用索引等信息进一步进行优化。如把表中的子查询转化为 join 操作。各个数据库性能的差别,在于语法优化有没有更好的一些语法优化的点,或针对更加复杂语句的优化机制。做到这点,需要数据库公司的员工分析各种复杂 sql 的组合,整理优化方案,输出优化规则,纳入到数据库执行引擎去。语义分析与优化器是代码量最多,核心的一部分。
执行计划:根据语法树,生成执行计划(包含利用到的表,索引,潜在可以利用的所有,要处理的行数等等),交给执行引擎去处理。
PrepareStatement 预编译
把 sql 语句其中的参数设置为 ?的占位符去构建,构建完后,在提交 EXECUTER 执行前,把参数值根据位置 SET 进去,如 updateObject.setInt(1,2)
PrepareStatement 会预先提交带占位符的 SQL 到数据库进行预处理,提前生成执行计划、当给定占位符参数,真正执行 SQL 的时候,执行引擎可以直接执行,提高效率。同时防止 SQL 注入攻击(原理就是 SQL 已经提交,语法树,甚至执行计划已经生成,只是等着参数值传入)。
数据库的存储的数据结构:MYSQL B+树
聚簇索引:数据库记录与索引存储在一起
MSQL 数据库的主键就是聚簇索引,主键 ID 和所在记录存储在一个 B+树中
数据库事务日志:
进行事务操作时,事务日志文件会记录更新前的数据记录,然后再更新数据库中的记录,如果全部记录都更新成功,事务就正常结束;如果某条记录更新失败,整个事务回滚,以更新的记录根据事务日志中记录的数据进行恢复,全部数据恢复到事务提交前的状态。
LSN:一个按时间顺序分配的唯一事务记录日志序列号。
TransID: 产生操作的事务 ID。
PageID: 被修改的数据在磁盘上的位置。
PrevLSN: 同一个事务产生的上一条日志记录的指针。
UNDO: 取消本次操作的方法,按照该方法回滚
REDO: 重复本次操作的方法
JVM 组织架构
堆:每个 jvm 实例唯一对应一个堆。存放应用程序在运行中创建的所有类实例或数组,由所有线程共享。
堆栈:jvm 为每一个新创建的线程都分配一个堆栈。
java 中所有对象的存储空间都是在堆中分配的,该对象的引用在栈中分配。
方法区 &程序计数器:方法区存放类字节码。jvm 进入启动类的 main 方法时,会为应用程序创建主线程,每个线程都有自己的 java 栈,栈里存放着方法运行期的局部变量。当前线程执行到哪一行字节码指令,存放在程序计数寄存器。(广义上说,方法区也是堆的一部分,只不过是放类字节码的。)
Java(线程)栈: 方法内定义的基本类型变量,会被运行该方法的线程放入自己的栈中,线程间栈彼此隔离,这些变量必定线程安全。
线程工作内存 &volatile: Java 内存模型规定在多线程情况下,线程操作主内存变量,需要将主内存变量,拷贝副本到线程独有的工作内存中去。这样在一个线程工作内存中的改动,无法及时通知其他线程。一个共享变量被 volatile 修饰之后,如一个线程修改了它的值,会立刻刷新到主内存去,另一个线程要用到这个变量时,一定要从主内存重新加载一次这个值。
Jvm 垃圾回收
识别不再被使用的对象通过可达性分析算法进行。
具体过程:从线程栈帧中的局部变量,或方法区的静态变量出发,将这些变量引用的对象进行标记,然后看标记的对象是否引用了其他有对象,继续标记,那些没有被标记的对象就是可回收的垃圾对象。回收方法如下:
清理(标记-清理): 清理对象占据的内存。JVM 将这些垃圾对象占用的内存空间标记为空闲,记录到空闲列表,暴露应用程序,在创建新对象时使用。但内存碎片太多,可能导致大数据无法保存。
压缩(整理): 从堆空间的头部开始,将存活的对象拷贝在连续的内存空间中,其余的空间就可以标记为连续的空闲空间。
复制:将堆空间分为两部分,只在一部分创建对象,这部分空间用完时,将标记过的可用对象复制到另一个空间中。
jvm 分代垃圾回收
大部分的对象生存时间是很短暂的,如果能放在较小的区域去处理,扫描,回收,会极大提高效率。用这种思路,划分出新生代与老年代,创建的对象归属于新生代,在新生代进行回收,如果一段时间未被回收掉,再复制到老年代。
新生代划分三个区域 Eden 区(伊甸园),from 区, to 区。 Eden 区满的时候,进行垃圾回收,大部分对象在此时会被回收,算法就是复制。还在用的对象就复制到了 from 区。如果在复制到 from 区的时候,发现空间不足,会把 from 区中的可用对象与拷贝过来的可用对象复制到 to 区,to 区变成新的 from 区(原 from 区已空,变为 to 区)。如果来回几次后,某些对象还是没被回收,就把这些对象拷贝到老年代。当老年代也满的时候,也会进行一次垃圾回收(full GC)。
垃圾回收过程中的标记操作 stop the world ,无法做其他事情。
1、串行回收器:单线程垃圾回收,速度太慢。
2、并行回收器:针对 CPU 核心数,启动多个垃圾回收线程,并行执行垃圾回收,但用户线程还是被 stw,无法做任何事情。
3、并发垃圾回收器(CMS) :它把标记清理的过程拆分为:初始标记,并发标记,重标记、并发清理。减少用户线程被 stw 的时间,过程如下:
(1)在初始标记是 stw。
(2)在并发标记阶段,垃圾回收线程与用户线程是并发执行的;由于用户线程的执行,可能对该标记的对象产生变动,所以需要重标记。
(3)在重标记阶段是 stw。
(4)在并发清理阶段,垃圾回收线程与用户线程是并发执行的.
4、垃圾回收器: 它把整个内存空间分成更小的区域(如 2000 个),针对小区域进行垃圾回收,每个区域也有以上的分区。
Java 性能监控及优化
JPS 查看 host 上所有 Java 进程的 pid
-l 输出 main class 完整包
-q 显示 pid
-m 输出传递给 main 方法的参数
-v 输出传递给 JVM 的参数,也就是 JVM 参数设置
JSTAT 针对 Java 应用程序的资源与性能进行实时监控,通常查看垃圾回收相关的参数。
语法 jstat [Options] vmid [interval] [count]
Options: -gcutil 查看 gc 情况
vmid: VM 的进程号,当前 Java 进程号
interval:间隔时间,单位毫秒
count: 打印次数,就是监控几次。默认不限制
JMAP 输出所有内存中对象,可以以二进制形式,输出堆的内容到文本。
jmap -histo pid>a.log 可以保存到文本中去,一段时间后,用文本比对工具,再输出,可以看 GC 回收了哪些对象,用于处理内存泄漏的场景。
jmap -dump:format=b,file=f1 PID 可以将 PID 进程的内存 heap 输出到 f1 文件。
JSTACK 查看 jvm 内的线程堆栈信息
语法: jstack -l [pid]
JAVA 代码优化技巧及原理
合理谨慎使用多线程
启动线程数 = [任务执行时间/ (任务执行时间-IO 等待时间)]*CPU 内核数
静态条件与临界区
多线程访问到相同的资源,并且对资源的访问顺序敏感(比如更改数据),代表存在竞态条件,该部分代码区即临界区。需要使用使得的同步(锁)避免竞态条件。
Java 线程安全 允许被多个线程安全执行的代码
方法局部变量:方法内部的变量,基础类型的局部变量是线程安全的。
方法局部的对象引用:某个方法中创建的对象只在方法内部使用,则线程安全。否则可能被其他对象引用(比如把对象 set 到外部类对象中去,使得外部对象能操作数据),还是不安全的。
对象成员变量: 对象存储在堆上,如果两个线程同时更新一个对象的同一个成员,那这个代码不是线程安全的。
ThreadLocal 既是私有的,又是共享的,
B 类中获得的值与 A 类 set 进去的值无关。原因如下
第一层的 key 是个线程对象,返回 value 是 ThreadLocal map,局部参数就保存在第二层的这个 ThreadLocal map 中的。
共享的:整个 map 对象是共享的。
私有的:每个线程使用第二层保存的 map。
Java 内存泄漏 是由开发人员的错误引起的。
程序保留了不再使用的对象的引用,比如长生命周期对象,静态容器、缓存。比如用完后不干掉某些缓存,还放在某些静态容器中。因此对象放在静态容器中要小心点。
合理使用线程池和对象池
复用线程或对象资源。
池管理算法(记录哪些对象是空闲的,哪些在被使用)。
对象内容清除(Threadlocal 清理)。
使用合适的 JDK 容器类
不同的容器类优缺点不同,要在合适的场景使用
比如插入删除链表结构更好,修改数组更好。
比如 concurrentHashMap 实现分段锁,对数组中的分段,一段一段分别进行加锁,当修改的段没有处于并发的场景时,不会遇到互斥操作,也不会被阻塞(可以分段的并发)。它不需要应用程序加锁,也比应用加锁的性能更优。
缩短对象的生命周期,加速垃圾回收
减少对象驻留空间的时间。
在使用创建对象,用完就释放(引用对象置为 null,但会会使得代码复杂性增加)。
创建对象的步骤: 静态代码段-静态成员变量-父类构造函数- 子类构造函数
技术选型时使用 I/O buffer 及 NIOSDAD
S
评论