人脑的容量有限,为了在有限的脑容量中高效的存储更多的知识,需要对知识进行归纳整理,变成自己的文章。但是并不是所有的知识都能够变成文章,出于篇幅、与其他知识点的关联等原因,很多知识当前还处于一种零散状态。

为了更有效的管理这些零散知识,现在将它们都存储在博客中的这个模块之中。当某些知识变成了一种常识或者许多知识积累了足够的信息量能够写一篇文章,则这些知识就会从这里删除。

========计算机类一般性知识========

Bash常用快捷功能

指令 解释 说明
cd - 回到上一次停留的目录
!<num> 快速执行history里的某个指定命令 !743
!! 指代上一个命令 sudo !! 以管理员权限重新执行上一条指令

Sqlite显示表头

默认情况下执行select语句只能得到|分割的数据, 如果数据比较多, 这种格式就不够直观. 可以通过如下的指令启用表头和以行模式展示数据.

1
2
.head on
.mode column

CPU如何实现时间片切换

显然CPU在执行过程中并没有时间的概念,也不可能在执行进程的代码的时候,在每一条指令后检查是否已经执行了指定的时间。所以时间片的切换显然是通过中断触发的。

通过单独的计时模块可以按照一定的时间间隔触发时钟中断(例如每隔1ms触发一次),从而CPU在执行完当前指令后通过响应中断的方式通知操作系统判断当前进程是否执行了足够长的时间。

ELF文件加载过程

从ELF文件的结构来看,似乎有很多不同的段,似乎如果要执行ELF文件,还需要进行解析过程。但需要注意到ELF的两个常见的格式,即可重定位目标文件和可执行目标文件。对于可重定位目标文件,由于需要重定位,因此有很多辅助的信息表。但是对于可执行目标文件,少了很多辅助段。

而且代码中的.text段和.data段实际上是相邻的,分别将这两个段加载到内存之中即可构建进程在内存中的状态。整个过程中只需要简单的解析每个段所在的位置即可,不需要进行复杂的分析过程。

当然,这个解析过程肯定还是需要加载器进行处理的,并不是硬件直接完成。

前端校验和后端校验

前端校验的目的是防止用户错误输入。 后端校验的目的是方式恶意攻击。

所以一些关键的校验既需要前端校验也需要后端校验

x86-64 Core体系结构历史

x86-64处理器是对x86-32处理器的扩展. 第一个支持x86-32的处理器是1985年推出的Intel 80386微处理器. 80386是对16位体系的80286的扩展, 具有32位寄存器和4GB虚拟地址空间, 使用一个独立的, 称为80387的浮点单元进行浮点运算.

1993年推出的奔腾CPU基于P5架构, 支持MMX技术, 能够对64位大小的寄存处执行单指令多数据(SIMD操作. 1995年推出P6架构, 并在1997年推出的奔腾II中引入三路超标量设计, 使得CPU能够平均在每个周期内解码, 分派和执行三条不同的指令. 此外还引入了无序指令执行, 改进的分支预测等优化.

1999年推出的奔腾III也基于P6架构, 并且支持一种称为 Streaming SIMD Extension, SSE的指令集, 从而能够对8个128位的寄存处进行操作. 2006年, Intel推出Core架构, 支持SSE3和SSE4.1指令集. 2008年推出Nehalem架构, 再次引入超线程技术并支持SSE4.2指令集. 2011年推出Sandy Bridge架构, 引入一种新得SIMD技术, 称为Advanced Vector Extension, AVX. 2013年推出Haswell架构, 支持AVX2指令集和FMA(乘法加法融合)操作. 2017年推出的Skylake-X架构支持了AVX-512指令集, 能够操作512位的寄存器.

与此同期, AMD在2003推出一系列基于K8架构的CPU, 能够支持MMX, SSE和SSE2指令集. 2007年推出K10架构, 支持一种对SSE的扩展指令集SSE4a. 2011年推出Bulldozer架构, 支持SSE3, SSE4.1, SSE4.2, SSE4a和AVX指令集.2017年推出Zen架构, 支持AVX2指令集.

POSIX接口

POSIX是 Portable Operating System Interface for uniX的缩写, 即可移植操作系统接口. 在POSIX接口提出以前, 存在很多不同的操作系统, 这些操作系统提供的接口也都不尽相同, 因此对程序开发带来了很大的困难.

POSIX标准通常由C语言的库实现, 例如glibc和musl等. 在C语言层面通过调用POSIX接口实现在不同的操作系统上转换为合适的系统调用.

ll各列的含义

1
2
3
4
5
6
7
8
9
drwxrwxr-x   9 lizec lizec 4.0K 4月   7 10:10 ./
drwxrwxr-x 7 lizec lizec 4.0K 4月 3 17:45 ../
drwxrwxr-x 8 lizec lizec 4.0K 4月 7 18:31 .git/
-rw-rw-r-- 1 lizec lizec 1.1K 3月 26 19:24 LICENSE
drwxrwxr-x 230 lizec lizec 12K 4月 7 10:10 node_modules/
drwxrwxr-x 2 lizec lizec 4.0K 3月 26 19:24 scaffolds/
-rwxrwxr-x 1 lizec lizec 496 3月 26 19:24 service.sh*
drwxrwxr-x 11 lizec lizec 4.0K 3月 26 19:24 source/
drwxrwxr-x 3 lizec lizec 4.0K 3月 26 19:24 themes/

第一列表示文件权限,可以分为四个部分,其中第一部分包含一个字母,之后的三个部分每个部分包含3个字母。第一个字母表示文件的类型,d表示文件夹,-表示普通文件。后续的三个部分分别表示此文件的用户,同组的用户以及其他用于对该文件的权限。

第二列表示链接数量。对于文件夹表示其中的一级子目录的数量,对于文件表示链接的数量。

后续几列分别表示文件的所有者,所有者所在的组,文件的大小,修改时间以及相应的文件名。

SSH隧道

========Go相关知识========

垃圾回收算法

Go的垃圾回收算法是三色算法,即根据对象之间的引用关系,将对象分割为白色对象,灰色对象和黑色对象。

垃圾回收过程使用单独的线程进行处理,并使用写屏障维护指针的关系。

Mid-stack inlining

与C系列的语言一样, Go语言的编译器对于比较短小的函数会默认进行内联操作. 是否内联的主要依据是代码的代价(复杂度), 越是简单的代码越容易被内联.

内联是递归的, 例如对于下面的函数, 由于g是简单函数, 因此f中的调用被内联, 之后f也是简单函数, 因此f被内联到main方法中.

1
2
3
4
5
6
7
8
9
10
11
func main() {
f()
}

func f() int {
g() + g()
}

func g() int {
// 简单函数
}

由于是否内联取决于函数的代价, 因此部分无法衡量代价的操作将导致整个函数无法被内联. 此外, 如果一个函数内调用了不可内联的函数, 则此函数由于无法计算代价也必然不可内联.


针对函数内调用了不可内联的函数导致不可内联的问题, Go语言引入了Mid-stack inlining技术. 只要一个函数f本身足够简单, 即使调用了不可内联的函数g, f自己还是可以被内联.

无法对f进行内联的主要原因是Go的编译器对于此类内联操作无法准确的追踪堆栈和调用信息, 导致报错时无法准确输出信息. 对编译器进行改造以后即可修复此问题. 引入此优化后, Go语言有大约9%的性能提升和大约15%的体积增加


标准库的sync包中的Lock方法采用了Mid-stack inlining特性, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}

上述代码对于加锁操作分为两个部分, 首先尝试CAS加锁, 如果失败则执行后续逻辑. 此处CAS操作的代价是确定的, 而lockSlow()逻辑非常复杂, 不可被内联.

按照上述写法, 可以确保Lock方法本身可以被内联, 从而使得锁压力较小时性能较高.

GO的指针与对象

Go存在类似C语言的指针,其含义也和C语言中的指针一样。

Go语言的特性使得结构体指针可以和结构体变量一样的语法来访问成员变量。因此有时候有一种和Java类似的错觉,似乎可以把指针视为对象。

但是另一方面Go的指针又具有函数可能修改这一变量的含义,但是Go没有泛型,类似场景只能定义一个万能的interface{}类型,这导致函数内如果想要修改变量,没有办法明确的要求这里是一个指针。

如果调用的时候忘记了取地址,虽然函数的代码中可以做运行时检查,但这基本导致Go语言的强类型系统报废了。这绝对是一个设计缺陷。

golang-open-source-projects

这个项目可以理解为针对互联网IT人打造的中文版awesome-go。已有的awesome-go项目, 汇总了很多go开源项目, 但存在的问题是收集太全了, 而且每个项目没有详细描述。

本项目作为awesome-go的一个扩展,根据go语言中文社区提供的资料,还有互联网企业架构设计中的常见组件分类, 共精心挑选了154个开源项目(项目不限于在github开源的项目), 分成以下17个大类。

项目初衷是帮助到那些想学习和借鉴优秀golang开源项目, 和在互联网架构设计时期望快速寻找合适轮子的人。

ps: 以下项目star数均大于100,且会定期检查项目的url,剔除无效链接。 每个分类下的项目会按照star数从高到低进行排列。

Go值得关注的开源项目

Go语言小工具

Go语言相关库

Go项目

========Python相关知识========

Flask安全文档

========JavaScript相关知识========

ES6 与 ECMAScript 2015

ES6是JavaScript语言的一种规范定义, 可以类似的理解为Java 8. 该标准于2015年推出, 因此有时候也使用ECMAScript 2015 指代. 在ES6标准推出的时候, 许多浏览器对于该语法的支持还不完善, 因此还需要使用Babe转码器进行处理.

对于NodeJs而言, 通常情况下是领先于浏览器的, 对于ES6的支持比较全面.

都2023年了, 感觉应该并不存在这些问题了.

NPM基本使用

基本指令

TODO: 常用命令学习

更新依赖

对于npm项目, 可执行npm outdated指令查看有哪些依赖可以升级, 之后执行npm update指令可将展示的依赖全部升级.

该升级行为仅升级小版本, 而不会升级大版本. 如果存在大版本变更, 需要手动执行npm install指令.

如何更新 NPM 依赖

package.json文件

TODO: 依赖于开发依赖

对于一个给定的版本号x.y.z, ^表示可以更新yz到最新版, ~表示可以更新z到最新版

使用模块

在启用webpack打包之前, 只能根据位置位置在HTML中引用对应的文件. 启用webpack后, 可以使用ES6的require语法或import语法

1
const $ = require('jquery')

webpack原理与使用

TODO:

  1. 回顾JS语法特性, 补充必要的语法知识
  2. 补充对于NodeJS情况下, JS的语法知识
  3. 增加Typescript相关内容

========Java相关知识========

安全管理器

如果阅读JDK中的代码,会发现很多地方都会出现类似的代码

1
2
3
4
SecurityManager sec = System.getSecurityManager();
if (sec != null) {
sec.checkPermission(SET_LOG_PERMISSION);
}

其中SecurityManager是Java内置的安全管理器,其主要功能是对一些需要权限的操作进行检查。如果有过Android开发经历,那么对于这种权限系统肯定不会陌生。

但由于目前的Java生态已经和当年的模式有很大的区别,所以现在已经很少使用此功能了。见到这部分代码直接忽略即可。

关于Java开发的一些思考

在Java Web开发过程中,相比于直接使用MyBatis并手写基础的SQL语句,还是直接使用MyBatis-Plus更好,尤其在涉及字段变化的时候,能够减少很多维护的负担。

一个Java Web项目,即使什么操作也不做,也需要大约200M内存空间,而与此相对的,Go和Python即使实现了一个稍微有些复杂的后端,可能内存还是能够控制在50M以内,因此项目规模决定了是否使用Java。否则开发也费劲,服务器也承受不了。

SPI机制

SPI机制即Service Provider Interface, 是服务提供方根据现有接口提供服务实现的一种机制. 典型应用是JDBC驱动, 即无论具体使用哪一种数据库, 客户端代码都只使用JDBC的接口进行交互, 而各个数据库厂商根据接口提供相应的驱动实现类.

例如在mysql-connector-java-8.0.15.jar包中的META-INF文件夹下有一个services/java.sql.Driver文件,其中指定了一个类名com.mysql.cj.jdbc.Driver,而这个类实际上实现的就是将自己注册到DriverManager的功能。通过Java的SPI机制即可实现客户端不使用任何操作的情况下添加新的Drive实现的目的

SPI提供了一种运行时加载具体实现类的方式. 本质上来说就是根据JAR包中提供的类名, 在运行时动态的加载指定的实现类. 因此这一套逻辑完全可以自己用代码实现,Java只是提供了一组封装好的接口而已.

检测线程死锁

其实正常写代码的情况下比较少出现死锁,但如果怀疑程序中出现死锁问题,可以使用JDK自带的工具进行检测

  1. 使用jstack查看堆栈信息,可以看到线程持有的锁的情况,从而分析死锁问题
  2. 使用visualVM,直接点击检测死锁按钮,也能分析死锁情况

如果代码中出现死锁,可以从如下的角度考虑解决

  1. 统一加锁顺序
  2. 设置请求锁超时时间
  3. 响应中断
  4. 使用无锁编程

一些Java对象的名称

名称 全称 含义
PO Persistent Object 对应数据库记录的Java对象
VO Value Object 业务层之间传递数据的的对象
DAO Data Access Object 控制数据库访问的对象, 通常与PO结合
DTO Data Transfer Object 数据传输对象, 与数据库传递数据
BO Business Object 封装业务逻辑的对象, 调用DAO的方法

序列化ID

serialVersionUID 用于标记序列化类的版本, 如果对类进行了修改, 那么也应该同步的修改serialVersionUID. IDEA可以直接提供随机值.

手动检查Null

手动检查是否为Null, 并手动抛出NullPointException可以视为一种防御性编程, 即如果可能发生错误, 则应该尽可能早的产生.

JDK

前几天更新Intellij IDEA的时候发现, 新版本已经支持JDK 11了, 于是下载了JDK 11 体验了一下. 相较于JDK 8, JDK 11的改动比较大, 目前很多第三方库还没有针对JDK 11做调整, 因此不建议直接把JDK升级, 最好还是先同时保留JDK 8 和JDK 11.

JDK 新特性介绍

JDK集合类归纳

JVM常用参数

参数 含义
-Xms 设置初始堆大小
-Xmx 设置最大堆大小
-XX:+PrintCommandLineFlags 打印设置的参数(包括默认参数)

实际上JVM会根据设备可用的内存数量来调整默认使用的垃圾回收算法. 例如在我的服务器上(1GB内存)和开发机器上(16GB内存)分别打印默认的参数. 其结果如下所示:

1
2
3
4
-XX:InitialHeapSize=16071680 -XX:MaxHeapSize=257146880 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC 
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment 18.9 (build 11.0.10+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode, sharing)
1
2
3
4
-XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=267607488 -XX:MaxHeapSize=4281719808 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
java 11.0.4 2019-07-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.4+10-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.4+10-LTS, mixed mode)

由于服务器上内存较少, 因此默认采取最简单的Serial系列垃圾回收器, 从而节省内存消耗. 而开发机器上内存比较充足, 因此采取了性能更好的G1垃圾收集器.

Java删除文件

曾经有过使用Java删除文件的经历, 但是偶尔会遇到删除失败. 即使用file.delete()函数时, 返回false. 因为这个函数并不抛异常, 所以并不清楚是什么原因导致了这一问题. 如果查看file.delete()的源代码, 可以发现这个函数的注释上写了如下的语句:

1
2
3
4
5
6
7
/**
* <p> Note that the {@link java.nio.file.Files} class defines the {@link
* java.nio.file.Files#delete(Path) delete} method to throw an {@link IOException}
* when a file cannot be deleted. This is useful for error reporting and to
* diagnose why a file cannot be deleted.
*/

上面这段注释指明, 可以使用java.nio.file.Files类的静态方法delete(Path)来删除文件, 这个函数会抛出异常来指明为什么删除失败. 注意到delete(Path)方法需要传入一个Path类作为参数, 而File类正好有一个toPath()方法可以将一个File类转化为Path类.

因此在明白了这些函数以后, 可以将原来的代码中的相关删除操作换成这个会抛异常的方法, 然后根据异常信息来确定具体的异常原因. 替换后, 抛出异常指出我想要删除的文件被正在被另外一个程序使用, 进程无法访问. 检查了一下代码的其他部分, 很快就发现了之前读取这个文件的文件流没有关闭. 因此在原来的代码中加入了关闭流的操作后, 就可以顺利的将文件删除了.

值得一提的是, 同样的代码, 在Linux平台上, 似乎并不会因为文件流没有关闭就导致文件无法被删除. 但是因为Linux平台上使用的编译器和JVM与Windows平台不同, 因此也不能确定这一差异是编译器导致的还是JVM或者操作系统导致的. 如果下次有机会, 可能会进一步探索一下其中的差异.

Apache fileupload 无法提取参数

之前使用了Apache的fildupload实现了文件上传功能, 后来发现通过表单传递的参数无法通过request.getParameter()获得. 通过查阅Apache CommonIO文档可知, 使用以下方法获得参数

1
2
3
4
5
6
// Process a regular form field
if (item.isFormField()) {
String name = item.getFieldName();
String value = item.getString();
...
}

其中item通过以下方法获得

1
2
3
4
5
6
7
8
9
10
 Map<String, List<FileItem>> formItemMaps =upload.parseParameterMap(request);

if (formItemMaps != null && formItemMaps.size() > 0) {
for (String name : formItemMaps.keySet()) {
List<FileItem> itemList = formItemMaps.get(name);
for(FileItem item:itemList) {
// 在这里处理item
}
}
}

注意: 现在如果仅仅是文件上传, 其实并不需要使用CommonIO, Tomcat本身也支持有关功能.

========MyBatis源码阅读知识点========

学习的目标

在阅读一个项目的过程中,应该抱着如下的几个目标:

  1. 理解项目的实现原理
  2. 学习成熟项目的架构方案
  3. 学习可靠且巧妙的实现技巧
  4. 补足自身知识盲点

其他可以参考的优质开源项目

  • dubbo
  • netty
  • spring-boot
  • fastjson
  • kafka

MyBatis的功能与实现原理

首先要明确一点,基于Java提供的JDBC接口,仅通过调用这些接口即可实现数据库的所有功能。因此MyBatis实际上并不需要实现任何与数据库相关的代码,这些代码应该由对应的驱动包实现。

所以MyBatis的主要功能是SQL语句拼接,接口绑定,缓存等上层功能,而整个库里面的大部分代码的作用也是实现这些功能。

源码阅读的一些技巧

  1. 根据项目的包结构大致划分每个包的功能
  2. 从外围的基础功能包开始阅读,逐步阅读核心的包

因为外围的基础包的依赖比较少,因此比较容易理解其中的功能。而项目的核心包往往依赖了众多基础包,因此阅读这些包的代码之前也需要先了解核心包的代码。

MyBatis包划分

根据每个包的名称,以及大致对代码的浏览,可以做出如下的划分

  • 基础功能包:exceptions / reflection / annotations / lang / type / io / logging / parsing
  • 配置解析包:binding / builder / mapping / scripting / datasource
  • 核心操作包:jdbc / cache / transaction / cursor / executor / session / plugin

MyBatis中有大量的代码都是解析相关的。

========Clojure相关知识========

Clojure是一种运行在JVM上的Lisp方言. Clojure语言因此在语法上与Scheme有较多的相似性, 同时又可以充分利用Java生态中的各类工具.

基础语法

Clojure的基础语法基本上可以认为和Scheme一致, 以下是一些常用的 Clojure 基本关键字及其描述:

关键字 描述
def 定义一个符号,通常用于定义全局变量。
defn 定义一个函数。
let 绑定一些局部变量,定义永久性绑定。
if 用于条件判断。
do 顺序地执行多个表达式。
loop/recur 创建一个可递归的循环。
fn 创建一个匿名函数。
quote 返回其参数未求值的表示形式。
eval 求值一个表达式。
str 将一个元素转换为字符串(类似ToString)
1
2
3
4
5
6
7
8
9
; 定义符号
(def PI 3.14)

; 定义函数
(defn square [x]
(* x x))

(defn area [r]
(* PI (square r)))
关键字 含义
case 多分支条件判断。
cond 一系列布尔表达式和结果值的有序并列。
for 列表推导,生成各种数据集合。
map 处理列表中每个元素的操作。
reduce 将一个函数应用于一个序列的元素,以汇总结果。
filter 根据一个谓词函数从一个集合中筛选元素。
lazy-seq 创建一个惰性序列。
deref/@ 解引用,获取 Ref、Agent 和 Atom 的当前值。
ref 创建一个 Ref 类型对象。
agent 创建一个 Agent 对象。
atom 创建一个 Atom 类型对象。
alter 改变 Ref 对象的值。
swap! 改变 Atom 对象的值。
send 通过 Agent 发送一个任务。
ns 声明一个命名空间。
require 在当前命名空间导入另一个模块的符号。
import 导入 Java 类。
new 创建一个 Java 对象。
. 调用 Java 的实例方法。
.. 在一个 Java 对象上连续调用方法。
doto 为一个 Java 对象连续调用方法,返回原始对象。

请注意,Clojure 关键字列表并不局限于此,还有许多其他的关键字。这个列表仅提供了一些关键字的简短概述,要了解更详细的信息,请参阅 Clojure 官方文档。

========Protobuf相关知识========

protobuf是一种将结构化数据序列化的机制, 可用于内部设备通信或存储. 与JSON格式相比, 基于protobuf协议的二进制文件体积更小, 解析速度更快.

protobuf简介

类型

类型 解释
float, double 浮点数
int32, int64, uint32, uint64 整数,但不适合编码较大的数字和负数
sint32, sint64 针对负数进行优化的整数类型
fixed32, fixed64, sfixed32, sfixed64 更适合大数字的有符号数或无符号数
bool 布尔值
string 任意的UTF-8字符串
byte 任意的字节

protobuf对数字存储进行了优化,一个数字越小则存储长度越短。由于计算机使用补码表示负数,因此通常情况下负数将使用多个字节表示。为了优化这种情况,sint类型使用交叉的方式表示,绝对值较小的负数依然可以获得较短的存储长度。

protobuf命名冲突解决方案

对于PB的namespace, 规范要求每个PB都是全局唯一的. 如果设计不合理就会导致PB名称冲突, 对于高版本的依赖库, Go语言在启动时会直接painc, 导致系统无法启动.

对于上述问题, 可以通过降级依赖版本临时解决:

1
2
3
4
replace (
github.com/golang/protobuf => github.com/golang/protobuf v1.4.3
google.golang.org/protobuf => google.golang.org/protobuf v1.25.0
)

========Nginx相关知识========

========prolog相关知识========

安装Prolog

对于Win平台和Mac平台, Prolog提供了二进制安装包, 可从官网下载. 对于Linux平台, 可选择使用apt安装或从源码编译.

如果使用apt安装, 则输入如下指令, 详细内容可参考官网文档

1
sudo apt-get install swi-prolog

========有趣的项目推荐========

Github使用

免费开发环境

每月可免费使用120核心小时的服务器资源. 停止运行后, 不计算核心小时资源, 仅计算存储资源.

默认启用2核心服务器, 可使用60小时, 平均每天可使用2小时. 30min无操作自动关闭, 几乎等于无限制使用.

最后更新: 2024年03月27日 17:30

版权声明:本文为原创文章,转载请注明出处

原始链接: https://lizec.top/2010/01/01/%E4%B8%AA%E4%BA%BA%E7%9F%A5%E8%AF%86%E5%BA%93%E4%B9%8B%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%8A%80%E6%9C%AF/