spider 爬虫架构
go go-concurrency
Process And Thread
操作系统会为应用程序创建一个进程, 作为应用程序. 它是一个为应用程序所有资源而运行的容器, 这些资源包含内存地址, 文件句柄, 设备和线程. 每个进程中都包含了一个主进程
线程是操作系统调度的一种执行路径, 用于在处理器中执行我们编写的代码.
一个进程从一个线程开始, 即主线程, 当该线程终止时,进程终止。这是因为主线程是应用程序的原点。然后,主线程可以依次启动更多的线程,而这些线程可以启动更多的线程。
无论线程属于哪个进程,操作系统都会安排线程在可用处理器上运行。每个操作系统都有自己的算法来做出这些决定。
Goroutines and Parallelism
Go 语言层面支持的 go 关键字,可以快速的让一个函数创建为 goroutine,我们可以认为 main 函数就是作为 goroutine 执行的。操作系统调度线程在可用处理器上运行,Go运行时调度 goroutine 在绑定到单个操作系统线程的逻辑处理器中运行(P)。即使使用这个单一的逻辑处理器和操作系统线程,也可以调度数十万 goroutine 以惊人的效率和性能并发运行。
Concurrency is not Parallelism.
并发不是并行。并行是指两个或多个线程同时在不同的处理器执行代码。如果将运行时配置为使用多个逻辑处理器,则调度程序将在这些逻辑处理器之间分配 goroutine,这将导致 goroutine 在不同的操作系统线程上运行。但是,要获得真正的并行性,您需要在具有多个物理处理器的计算机上运行程序。否则,goroutine 将针对单个物理处理器并发运行,即使 Go 运行时使用多个逻辑处理器。
Keep yourself busy or do the work yourself
Leave concurrency to the caller
Never start a goroutine without knowning when it will stop
Any time you start a Goroutine you must ask yourself:
-
When will it terminate?
-
What could prevent it from terminating?
小结: 由开发者管理goroutine的生命周期, 将并发性交给调用方
go go异常处理
在了解go异常处理的时候,有必要先了解为什么需要做异常处理,异常处理主要在哪几个方面,区分异常和错误的区别等等.
QA
为什么需要做异常处理?
我个人认为有一下几点
- 从程序设计的角度来看, 保证程序的鲁棒性,健壮性
- 从开发的角度来看, 快速定位问题,解决问题,预防问题
异常处理主要在哪几个方面
异常处理主要在实践上可以区分为
- 业务层面: 保证业务的稳定性, 逻辑性
- 基础库: 保证代码逻辑正常
异常与错误的区别
编程语言中的异常和错误是两个相似但不相同的概念。异常和错误都可以引起程序执行错误而退出,他们属于程序没有考虑到的例外情况(exception)。
便于理解举个例子:
一个网络请求, 没有网络-错误
一个网络请求过程中,对方服务器处理超时(注意是对方服务器正常) - 异常
Error 和 Exception
go error
go error 就是一个普通的接口, 普通的值.
1 |
// https://golang.org/pkg/builtin/#error |
经常使用 errors.New()
来返回一个 error
对象
1 |
// https://go.dev/src/errors/errors.go |
New() 返回的是 errorString对象的指针
为什么返回的是指针?
- 避免创建的error的值一致
基础库中大量定义的error
1 |
// https://go.dev/src/bufio/bufio.go |
tip: 在定义错误的时候带上包名,便于区分. 如ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
中的bufio:
Error VS Exception
各语言的演进历史
C: 但返回值, 入参通过传递指针作为入参, 返回int 表示成功还是失败, 以及如果失败的状态码是什么
C++: 引入了Exception,但无法知道被调用者抛出什么异常
Java: 引入了checked exception,方法的所有者必须申明, 调用者必须处理.
go: 支持多参数返回, 所以很容易在函数签名上实现了error interface的对象,交由调用者处理
如果一个函数返回了
(value,error)
, 不能对这个value
做任何假设, 必须先判定error
补充: go中panic
机制,意味着 fatal all, 不能假设调用者来解决panic
意味着代码down了
记录单一清晰的错误, 并处理!!!
注意二值性
go特征
- 简单
- 考虑失败而不是成功
- 没有隐藏的控制流
- 完全交给开发者来处理
-
Error are values
对于真正的错误, 表示不可恢复的程序错误,例如索引越界, 不可恢复的环境问题, 堆栈溢出,才使用
panic
,对于其他的错误情况,应该是情况我使用error来进行判定
go error type
Sentinel Error
预定义的特定错误,称之为 Sentinel Error. 这个名字起源于计算机编程中使用一个表示不可能进一步处理的做法. 使用特定值来表示错误.
1 |
if err == ErrorSomething { ....} |
使用 Sentinel Error 值是最不灵活的错误处理策略, 因为调用方法 必须使用==
将结果与预先声明的值进行比较. 当需要提供更多的上下文时,就会出现一个因为反返回一个不同的错误将被破坏相等性检查.
例如一些有意义的fmt.Errorf
携带一些上下文,也会破坏调用者的==
,调用者将被迫查看error.Error()
方法的输出,以查看它是否与特定的字符串匹配
tips:
- 不依赖检查
error.Error
的输出.
不应该以来检测
error.Error
的输出, Error方法存在于error接口主要用于方便开发者使用,而不是程序(编写测试会依赖这个返回). 这个输出的字符串用于记录日志,输出到stdout
- Sentient errors 成为你API公共部分
如果公共函数或方法返回一个特定的值,那么该值必须是公共的,当然要有文档记录,这会增加API的表面积
如果API定义了以恶搞返回特定错误的
Interface
,则该接口的所有实现都将被限制为仅返回该错误, 即使他们可以提供更具有描述性错误比如: io.Reader. 像io.Copy这类函数需要reader的实现者比如返回 io.EOF 来告诉调用者没有更多数据量,但这又不是错误
- Sentient errors 在这个两个包之间创建依赖
Sentinel errors 最糟糕的问题是他们在两个包之间创建了源码依赖关系
例如检查错误是否等于io.EOF, 代码就必须要导入io包, 虽然听起来似乎不那么糟糕,但想象一下,当项目中的许多包到处错误值时,存在耦合,项目中的其他包必须要导入这些错误值才能校验特定的错误条件
建议:尽可能的避免使用 sentinel errors
Error Types
Error type 实现了error接口自定义类型.例如ExampleError
类型记录了文件和行号以及展示发生了什么. 如下代码所示
1 |
import ( |
与错误值相比, 错误类型的优点是他们能够包装底层错误以提供更多上下文.
官方实例 os.PathError:
1 |
type PathError struct { |
调用者要使用类型断言和switch,就要让自定义的error变为公共的, 这种模型会导致和调用者产生强耦合,从而导致API非常脆弱
结论: 尽量避免使用error types,或者说尽量避免其成为公共API的一部分
虽然错误类型比sentinel error更完善,提供更多的上下文信息, 但error types 共享error value许多相同的问题.
Opaque errors
不透明的错误处理
直接返回错误而不假设其内容
- Assert errors for behaviour, not type
在某些情况下,这种二分错误处理方法是不够的, 例如与外界交互(网络), 需要调用方法查错误的性质,以确定重试是否合理. 在这种情况下,可以使用断言错误实现了特定的行为.
Handle Error
Indented flow is for errors
缩进流用于错误
1 |
// 无错误的正常流程代码应为一条直线 |
Eliminate error handing by eliminating errors
通过消除错误来消除错误处理
1 |
func AuthenticateRequest(r *Requests) error { |
io Reader Example
统计 io.Reader
读取内容的行数代码实例
1 |
func CountLines(r io.Reader) (int, error) { |
改进-使用bufio.scanner
1 |
func CountLines1(r io.Reader) (int, error) { |
Http Example
1 |
type Header struct { |
1 |
import ( |
Wrap errors
you should only handle errors once. Handing an error means inspecting the error value, and make a single decision
日志与错误无关且对调试没有帮助的信息都应视为噪声, 应予以质疑. 记录的原因是应为某些东西失败了,而包含了答案
- 错误要被日志记录
- 应用程序处理错误,保证百分百完整性
- 之后不在报当前错误
pkg-errors
- 在应用代码中,使用
pkg/errors
中的errors.New
或者error.Errorf
返回错误 - 如果调用其他包内的函数,通常简单的直接返回
- 如果与其他库协作, 考虑使用
pkg/errors
中的errors.New
或者error.Errorf
返回错误保持堆栈信息 - 直接放回错误, 而不是每个错误产生的地方打日志
- 在程序的顶部或者是工作的 goroutine顶部(请求入口), 使用
%+v
保存堆栈详情记录 - 使用
errors.Cause
获取root error
在进行sentinel error判定
小结
Packages that are reusable across many projects only return root error values.
选择 wrap error 是只有 applications 可以选择应用的策略。具有最高可重用性的包只能返回根错误值。此机制与 Go 标准库中使用的相同(kit 库的 sql.ErrNoRows)。
If the error is not going to be handled, wrap and return up the call stack.
这是关于函数/方法调用返回的每个错误的基本问题。如果函数/方法不打算处理错误,那么用足够的上下文 wrap errors 并将其返回到调用堆栈中。例如,额外的上下文可以是使用的输入参数或失败的查询语句。确定您记录的上下文是足够多还是太多的一个好方法是检查日志并验证它们在开发期间是否为您工作。
Once an error is handled, it is not allowed to be passed up the call stack any longer.
一旦确定函数/方法将处理错误,错误就不再是错误。如果函数/方法仍然需要发出返回,则它不能返回错误值。它应该只返回零**(比如降级处理中,你返回了降级数据,然后需要 return nil)。
爬虫 爬虫对抗相关总结
又双叒叕 一篇相关于爬虫对抗的总结. 一般来说对于爬虫的对抗主要体现在如下几个方面
-
反调: 反抓包\反调试\代码混淆与防护
-
反(欺诈)模拟: 网络协议校验\TLS\请求头(字段\字段顺序\反盗链)校验
-
数据防护\验证码\风控对抗等
反爬虫(风险策略)工程师预防以及防止爬虫工程师对数据的抓取, 一方要”守”一方要”攻”,各为其主各司其职你来我往谁也多呈不让.
反抓包
反抓包的目的多种多样,简而言之便是让爬虫工作者无法捕获网络数据包
如: 不走http\不走代理\请求分离\SSLping单(双)向校验\网络协议定制等
反调试
反调试的策略诸多, 主要体现方面有思路为 预防调试\ 阻断调试\ 破坏调试,让爬虫逆向者无法有效进行调试
预防调试
预防调试的手段也多种多样, 简而言之便是预防开发者调试从而阻断分析无法进入下一步,主要体现在环境检测\动态代码
譬如:
- web端: 控制台检测\动态js\wasm\代码防护与混淆\hook检测\堆栈顺序检测
-
Android: 运行环境检测(root\签名校验\hook检测\模拟器检测)\进程检测与调试工具检检测(jadx\apktool\jeb\xposed\frida\ida)\壳\so\
-
web:
- 分辨率检测
- 控制台检测
- 动态js
- wasm
-
Android:
- 运行环境检测
- root检测
- 设备检测
- 签名校验
- 调试器工具检查
- 工具特性检测
- jadx
- apktool
- jeb
- xposed
- frida
- ida
- 工具特性检测
- 代码防护
- 壳
- so
- 代码隐写
- 运行环境检测
- 代码混淆
- 布局混淆
- 数据混淆
- 数字混淆
- 字符串混淆
- 控制流平坦化
阻断调试
阻断调试, 顾名思义便是阻止调试.或着说将你引入错误的逻辑中,无法正常的调试
譬如代码中的环境检测\设备指纹
破坏调试
相对于阻断调试更加极端的手段,一方面对调试者信息进行采集、攻击调试者。例如删除文件、重置电脑、甚至释放病毒等
小结
做反调试主要有两个方面一方面是特征区分预防\阻断\破坏调试, 另一方面是对代码进行保护与混淆
反(欺诈)模拟
如果说反调试是为了防止逆向工作者,那么反模拟便是针对爬虫工程师.
简而言之,防止模拟请求. 那么对于此.我们只需要区分开这个请求, 亦或者将结果数据进行处理这样就可以让开发者无法拿到正常有价值的数据,从而实现反爬虫的效果.
因为我们知道爬虫简单来说就是模拟用户请求的代码实现
反模拟主要在对于爬虫的请求进行甄别,常见的关键点有
请求库特征,譬如tls指纹
请求协议的区分: 譬如强制指定http2或quic等\使用自研的协议
参数校验: 校验的思路有 参数字段的校验如cookie等, 参数顺序的校验
加密与编码算法的定制修改,
- 位运算\ 在特殊的头\尾部加入字符
- 魔改hash算法\base(16\32\64)等
- 对称加密与非对称加密算法
校验的字段通常使用到加密算法或编码算法
数据防护
当我们完成了以上的种种, 便将”战场”迁移到最后的数据保护上,
常见的方式有数据加密(从服务端返回的数据进行加密或编码)
数据隐写
无法轻松直接的从返回的数据包中获取到数据, 只需要保证界面展示上正常的即可
验证码
验证码不必多说, 譬如常见的如下几种类型
- 滑动型
- 计算型
- 点选型(图像: 一维\二维\三维)
- 短信或语音验证型
通常验证码还会对对应的譬如图片进行安全处理, 如图片重排\模糊化等等干扰识别以及防止图像被还原
还有就是检测运行环境
风控
IP风控
设备风控
账号风控
风控其核心的对抗 个人认为有两点
- 特征
- 逻辑
写的比较散,但实现起来却并不是唯一, 仅按个人认为的进行的区分, 大多情况都不可能单独出现.
最后
无论是爬虫工程师还是反爬虫亦或者风险策略工程师. 攻与守并非是一成不变的. ,掌握底层逻辑,不断的成长向上吧. 瑞斯拜~
frida frida-rpc
frida 主动调用
主动调用: 强制去调用函数执行
被动调用: 由app主导,按照正常的执行顺序执行函数. 函数执行完全依靠与用户交互完成从而间接的调用到关键函数
在Java中,类的函数可以分为两种: 类函数与实例方法, 也可以称之为静态方法和动态方法.
类函数使用关键字static
修饰,与对应的类绑定, 当然如果该类函数还被public
修饰,则在外部就可以直接通过类去调用
实例方法没有被 staic
修饰,在外部只能通过实例化对应的类,在通过该实例调用对应的方法.
在frida中主动调用的类型会根据方法的类型区分开来, 类函数的直接调用使用Java.use
即可,实例方法则需要先找到对应的实例后对方法进行调用, 通常使用Java.choose
.
示例代码如下
1 |
setImmediate(function () { |
frida-rpc
通过exports 将结果导出,以便于python 结合frida模块直接调用.
js脚本与hook脚本写法基本一致,示例代码如下所示
1 |
// rpc.js |
1 |
# rpc.py |
smali smali语法
Smali
Smali是Android虚拟机的反汇编语言
Android代码一般是用JVM语言编写,执行Androdi程序一般需要用到JVM,在Android平台上也不例外,但是出于性能上的考虑,并没有使用标准的JVM,而是使用专门的Android虚拟机(5.0以下为Dalvik,5.0以上为ART)。Android虚拟机的可执行文件并不是普通的class文件,而是再重新整合打包后生成的dex文件。smali是dex格式的文件的汇编器
反汇编器\ 其语法是一种宽松的jasmin/dedexer 语法,实现了.dex格式的所有功能(注解/调试信息/线路信息等)
学习smali必要性
- 动态调试与修改APK, 当静态分析已经无法满足时,此时便需要对Android进行动态调试, 而动态调试便是调试smail
- 修改APK运行逻辑, 通过修改smali代码,在重新打包.便可对app进行持久化的修改.(常用的注入均在外部而不是app内部)
插件: java2smail
Smali基本语法
关键字
关键字
关键字 | 说明 |
---|---|
.class | 定义类名 |
.super | 定义父类名 |
.source | 定义源文件名 |
.filed | 定义字段 |
.method | 定义方法开始 |
.end method | 定义方法结束 |
.annotation | 定义注解开始 |
.end annotation | 定义注解结束 |
.implements | 定义接口指令 |
.local | 指定了方法内局部变量的个数 |
.registers | 指定方法内使用寄存器的总数 |
.prologue | 表示方法中代码的开始处 |
.line | 表示java源文件中指定行 |
.paramter | 指定方法的参数 |
.param | 和.paramter含义一致,但是表达格式不同 |
数据类型
Smali | Java | 备注 |
---|---|---|
v | void | 只能用于返回值类型 |
Z | boolean | |
B | byte | |
S | short | |
C | char | |
I | int | |
J | long | |
F | float | |
D | double | |
Lpackage/name; | 对象类型 | L表示这是一个对象类型,package表示该对象所在的包,;表示对象名称的结束 |
[类型 | 数组 | [I表示一个int型数据,[Ljava/lang/String 表示一个String的对象数组 |
类声明
1 |
.class + 修饰符 + 类名 |
构造函数
1 |
.method 权限修饰符 constructor <init>(参数类型) V |
成员变量定义格式
1 |
.field public/private [static][final] varName:<类型> |
返回值关键字
返回关键字 | Java数据类型 |
---|---|
return | byte short float char boolean |
return-void | void |
return-wide | long double |
return-object | 数组 object |
获取指令
1 |
iget, sget, iget-boolean, sget-boolean, iget-object, sget-object |
操作指令
1 |
iput, sput, iput-boolean, sput-boolean, iput-object, sput-object |
指令解析
1 |
sget-object v0,Lcom/aaa;->ID:Ljava/lang/String; |
example
1 |
// example 1 相当于java代码:this.timer = null; |
调用指令
调用关键字 | 作用 |
---|---|
invoke-virtual | 非私有实例方法调用 |
invoke-direct | 构造方法以及私有方法的调用 |
invoke-static | 静态方法的调用 |
invoke-super | 父类方法的调用 |
invoke-interface | 接口方法调用 |
调用格式: invoke-指令类型 {参数1, 参数2,…}, L类名;->方法名 如果不是是静态方法,参数1代表调用该方法的实例。
寄存器
Java中变量都是存放在内存中的,Android为了提高性能,变量都是存放在寄存器中的,寄存器为32位,可以支持任何类型。64位类型(Long/ Double) 用2个格式的寄存器表示; Dalvik字节码有两种类型: 原始类型和引用类型(
包括对象和数组)
寄存器分为如下两类: 1、本地寄存器: 用v开头数字结尾的符号来表示,v0, v1, v2,… 2、参数寄存器: 用p开头数字结尾的符号来表示,p0,p1,p2,…
注意:
在非static方法中,p0代指this,p1为方法的第一个参数。
在static方法中,p0为方法的第一个参数。
1 |
const/4 v0, 0x1 //把值0x1存到v0本地寄存器 |
tip:
查看smali代码时可以和java代码结合来看
referer
frida frida hook
frida启动方式
1 |
# frida -h |
Injection
attach hook: 这种模式建立在app已经启动的情况下,frida利用ptrace的原理注入app进而完成Hook
spawn hook: 将app启动权限交与frida 来控制。使用spawn实现hook时会由frida将app重启在进行hook
注意:由于attach hook 基于ptrace原理进行完成,因此无法在IDA正在调试的目标app以attach注入进程中,当然若先用frida attach注入
在使用IDA进行调试则正常
Shell
attach
1 |
frida -Ul script_hook.js [-n] app_name |
spawn
1 |
frida -Ul script_hook.js -f Identifier(package name) --no-pause |
Python script
1 |
import sys |
frida api
JavaScript-api : https://frida.re/docs/javascript-api
JavaScript-api-java: https://frida.re/docs/javascript-api/#java
JavaScript-api-module: https://frida.re/docs/javascript-api/#module
Hook
1 |
Java.perfrom(function () { |
Hook 类方法
1 |
Java.perfrom(function () { |
this.成员变量名.value
Hook 内部(匿名)类方法
1 |
Java.perfrom(function () { |
从匿名类/内部类访问外部类的属性写法: this.this$0.value.外部类的属性名.value
Hook 重载方法
1 |
Java.perfrom(function () { |
Hook 构造方法
1 |
Java.perfrom(function () { |
Hook 实例
1 |
Java.perfrom(function () { |
工作 一些工作上的思考
流程
规范的 、结构的构造端到端
计算机
技术的本质就是结构与组合。
今天我们在谈技术架构,有时候我们也会谈产品架构,再往前走,我们会谈商业架构,它中间都是一个结构的问题。
技术最重要的目标是开始追求生产效率,带来产能上的巨大提升,当产能得到一个巨大提升之后,就能够创造出越来越多的产品,进而实现规模化。技术要通过架构师或者工程师的工作才能完成这个过程。
技术要走向的就是规模化
计算机真正在做的事情是模拟
创造
闭环
在职场中,在领导口中.口口声声说 “闭环”,但 究竟什么是闭环?
闭环思维是一种有始有终的思维,它是一份生活的智慧。
比如拖延症导致的熬夜。由于有拖延症,导致无法及时完成任务,最后不得不熬夜,第二天精神萎靡,工作效率下降,于是不得不继续熬夜。这就是一个恶性闭环。
要想改变这个闭环,我们需要找到一个突破点,对上一个阶段的效果进行总结,把控改进方向。
工作效率就是一个突破点,我们可以提升工作效率,原本需要2个小时完成的任务,火力全开用1个小时做完,那么晚上就不需要再熬夜了。有了充足的睡眠,第二天精力充沛,就可以保证工作效率,于是形成了一个正向的闭环。
什么是闭环思维
闭环思维实际上被称为反馈控制系统,它将系统输出的测量值与预期和定值进行比较,由此造成一个偏差信号,然后运用偏差去做调整,便于输出值尽可能接近期望值。
PDCA循环将管理分为计划(Plan)、执行(Do)、检查(Check)、行动(Act) 四个阶段,这四个阶段不是独立存在的,而是周而复始的循环。这也是我们在企业中经常听到的“商业闭环”“闭环管理” 等概念。
我们这里提到的闭环思维是指在一定的基础上,对于他人发起的活动或者工作,无论我们完成的程度如何,都要在要求的时间之内认真地反馈给发起人,并且每个活动或者工作都要贯穿这个思维。
思考
1 |
你需要有体系结构化思维的能力 -- 5w3h\ 6why |
理解业务
技术 + 产品 + 运营
Referer
github github项目搜索技巧
搜项目大全
awesome-xxx
例如想了解python的所有技术体系,我们就可以搜索 awesome-python ,当然Java\spider\datascience等等
搜索样例
xxx simple 或者 xxx demo
空项目结构
xxx starter
例如
springboot starter
springboot+mybatis starter
教程与学习路线
xxx tutorial
例如: python tutorial
当然结合
awesome-xxx
更香哦
常用快捷键
s: 快速定位到搜索框进行搜索
t: 项目文件树
ctrl + K
: 快速查看文件
.
: 在线预览\运行项目
快捷键: https://docs.github.com/cn/get-started/using-github/keyboard-shortcuts
GitHub Search: https://github.com/search/advanced
Android android投屏工具
scrcpy
https://github.com/Genymobile/scrcpy/
vysor
QtScrcpy
https://github.com/barry-ran/QtScrcpy
Total Control
Appetizer
https://www.notion.so/appetizerio/AppetizerIO-1383d971ded94388b8e76f3236fe1f4f
source code 源码阅读技巧
本文仅提供思路,还需自身有足够的基础。
很多时候通常需要阅读很多源码,当然阅读源码也是有很多技巧的。
通常,根据他人总结的文档,先看整体(目录和类图)再看局部(类和函数)。
对于 Java 项目,就是先查看包的层级关系,然后分析包中类(接口)之间的关系,包括继承、实现、委托、方法调用等,最后再查看某个类具体的属性和方法的具体实现。
对于Python 项目,也是这样,先整体后局部,后核心。遵循先广度在深度,先核心在周围
源码阅读基本步骤
首先是通读官方文档,对框架或项目有个大致的了解
学习一些基础库及联系紧密的方法
找到一个趁手的IDEA,深层沉浸式阅读
尝试二次开发、修源码进行深入理解
源码阅读小技巧
搜索
根据文件名搜索文件/类等搜索
快捷键:shift + shift(连按两次)
字段搜索
局部搜索快捷键:Win: Ctrl + F Mac: Command + F
全局搜索快捷键:Win: Ctrl + shift + F Mac: Command + Shift + F
代码跳转
对应文件跳转
Mac快捷键:Command + e Win快捷键:Ctrl + e
跳转到上/下次光标的位置
查看源码时,经常需要在两个类中来回跳转,这个功能就变得相当实用!
查看上次光标位置快捷键:Win: Alt + ← Mac: Option + Command + ←
查看下次光标位置快捷键:Win: Alt + → Mac: Option + Command + →
调用
查看方法调用树
可以查看指定方法的所有调用方和被调方。
快捷键:Win: Ctrl + Alt + H MAC: Control + Option + H
调用与被调用
方法函数结构
快捷键:快捷键:Win: Alt + 7 Mac: Command + 7
类关系
referer
Jetbrains Jetbrains plugins
Jetbrains plugins
通用
ignore:生成git、dodkcer配置文件
Node.js:提供nodejs 支持
CSV: 用于编辑 CSV/TSV/PSV 文件的轻量级插件,具有灵活的表格编辑器、语法验证、结构突出显示、可自定义的颜色、新意图和有用的检查。
Rainbow CSV:用于以不同颜色突出显示 CSV 文件的插件。
Rainbow:基于 IntelliJ IDEA 的 IDE 的彩虹高亮标识符和分隔符
Tabnine: 代码智能提示
Ideolog: “.log”文件的交互式查看器。
CodeGlance Pro:提供代码缩略图
env files support:基于 .env、Dockerfile 和 docker-compose.yml 文件的环境变量补全。
BashSupport Pro:是用于高级Bash和shell脚本开发的插件—调试器、测试运行程序、代码完成、查找用法、重命名、ShellCheck、shfmt等。
Key Promoter X:提供快捷键提示
Nyan Progress Bar:彩虹进度条
translation: 翻译插件
Markdown:提供markdown 格式支持
Monokai Pro:主题
Graph Buddy: 旨在加快您阅读和学习源代码的过程。Graph Buddy 插件提供了一组有用的功能和技术,可帮助您轻松浏览扭曲的代码依赖项。同时,它可以让您更好地了解代码库中的代码结构。
Material Theme UI Plugin: 提供主题
Material Design Dark-Theme: 黑暗主题
AEM IDE: 保护您避免引入讨厌的错误,在编写代码时给您反馈
Save Actions:格式化代码插件
Grep console: 自定义控制台输出
statistic:代码统计插件
GitToolBox: git增强
Pycharm
AEM PRO: 本部分提供有关开始使用 IntelliJ AEM 所需步骤的信息,包括插件安装和初始项目设置。
Sourcery 是一个人工智能驱动的编码助手,它可以帮助你更快地编写更好的 Python 代码。它通过动态提供重构建议来工作,您可以立即将这些建议集成到您的代码中。
Idea
Codota: 代码智能提示
Lombok:简化臃肿代码插件
Alibaba Java Coding Guidelines:阿里巴巴代码规范检查插件
CamelCase: 驼峰命名和下划线命名转换
MybatisX:高效操作 MybatisX 插件
SonarLint:代码质量检查插件
checkstyle:代码风格检查插件
MetricsReloaded 代码复杂度检查插件
Jump To Line: 转到任意行并设置执行点而无需执行前面的代码。
Maven helper: 查看、分析和排除相互冲突的依赖项
Squaretest: 自动生成单元测试
Jrebel:热部署插件
EasyCode: 数据库表自动的生成实体类
Arthas Idea: Java 在线诊断工具
go go-reflect
反射简介
Go在标准库中提供的reflect包让Go程序具备运行时的反射能力(reflection)。反射是程序在运行时访问、检测和修改它本身状态或行为的一种能力,各种编程语言所实现的反射机制各有不同。
Go语言的interface{}
类型变量具有析出任意类型变量的类型信息(type)和值信息(value)的能力,Go的反射本质上就是利用interface{}
的这种能力在运行时对任意变量的类型和值信息进行检视甚至是对值进行修改的机制。
反射让静态类型语言Go在运行时具备了某种基于类型信息的动态特性。利用这种特性,fmt.Println在无法提前获知传入参数的真正类型的情况下依旧可以对其进行正确的格式化输出
反射三大法则
Rob Pike还为Go反射的规范使用定义了三大法则,如果经过评估,你必须使用反射才能实现你要的功能特性,那么你在使用反射时需要牢记这三条法则。
- 反射世界的入口:经由接口(interface{})类型变量值进入反射的世界并获得对应的反射对象(reflect.Value或reflect.Type)。
- 反射世界的出口:反射对象(reflect.Value)通过化身为一个接口(interface{})类型变量值的形式走出反射世界。
- 修改反射对象的前提:反射对象对应的reflect.Value必须是可设置的(Settable)。
reflect.TypeOf和reflect.ValueOf是进入反射世界仅有的两扇“大门”。通过reflect.TypeOf这扇“门”进入反射世界,你将得到一个reflect.Type对象,该对象中包含了被反射的Go变量实例的所有类型信息;
而通过reflect.ValueOf这扇“门”进入反射世界,你将得到一个reflect.Value对象。Value对象是反射世界的核心,不仅该对象中包含了被反射的Go变量实例的值信息,而且通过调用该对象的Type方法,我们还可以得到Go变量实例的类型信息,这与通过reflect.TypeOf获得类型信息是等价的:
reflect.Value.Interface()是reflect.ValueOf()
的逆过程,通过Interface方法我们可以将reflect.Value对象恢复成一个interface{}类型的变量值。这个离开反射世界的过程实质是将reflect.Value中的类型信息和值信息重新打包成一个interface{}的内部表示。
小结
reflect包所提供的Go反射能力是一把“双刃剑”,它既可以被用于优雅地解决一类特定的问题,但也会带来逻辑不清晰、性能问题以及难于发现问题和调试等困惑。
因此,我们应谨慎使用这种能力,在做出使用的决定之前,认真评估反射是不是问题的唯一解决方案;在确定要使用反射能力后,也要遵循上述三个反射法则的要求。
go go-unsafe
Go标准库
Go标准库中的unsafe包非常简洁,如下所示
1 |
// 用于获取一个表达式值的大小 |
典型使用
怎么理解Go核心团队在尽力保证go类型安全的情况下,又提供了可以打破安全屏障的unsafe.Pointer
这一行为?
首先被广泛应用于Go标准库和Go 运行时的实现当中,reflect、sync、syscall、runtime都是unsafe包的重度用户。
reflect
ValueOf 和TypeOf函数是reflect包中用得最多的两个API,他们是进入运行时反射层、获取发射层信息的入口。这两个函数均将任意类型变量转化为一个interface{}
类型变量,再利用unsafe.Pointer
将这个变量绑定的内存区域重新解释为reflect.emptyInterface类型,以获得传入变量的类型和值类的信息
1 |
// $GOROOT/src/reflect/values.go |
sync
sync.Pool 是个并发安全的高性能临时对象缓冲池。Pool 为每个P分配了一个本地缓冲池,并通过下列函数为每个P分配啦一个本地的缓冲池,并通过如下函数实现快速定位P的本地缓冲池。
1 |
func indexLocal(l unsafe.Pointer, i int) *poolLocal { |
标准库中的saycall包封装了与操作系统交互的系统调用接口,比如Statfs、Listen、Select
1 |
// $GOROOT/src/syscall/zsyscall_linux_amd64.go |
这类的高级调用的最终都会落到调用函数下面一系列的Syscall和RawSyscall函数上面。
1 |
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) |
这些Saycall系列的函数接受的参数类型均为uintptr,这样当封装的系统调用的参数为指针类型时(比如上面Select的参数r、w、e等)。只能只能通过unsafe.uintptr值,就像上面Select函数实现中那样。因此,syscall包是unsafe重度使用者,它的实现离不开的unsafe.Pointer。
runtime包中的unsafe包的典型应用
runtime 包实现的goroutine调度和内存管理(包括GC)都有unsafe包的身影以goroutine的栈管理为例:
1 |
// $GOROOT/src/runtime/stack.go |
unsafe.Pointer 与 uintptr
作为Go类型安全层上的一个“后门”,unsafe包在带来强大的低级编程能力的同时,也极容易导致代码出现错误。而出现浙西错误的原因主要就是对unsafe.Pointer和uintptr的理解不到位。因此正确理解unsafe.Pointer和uintptr对于安全使用unsafe包非常有必要。
Go语言内存管理是基于垃圾回收的,垃圾回收会定期进行,如歌一块内存没有被任何对象引用,他就会被垃圾回收器回收掉,而对象引用是通过指针实现的。
unsafe.Pointer 和其他常规类型的指针一样,可以作为对象引用。如果一个对象仍然被某个unsafe.Pointer 变量引用着则该对象不会被回收(垃圾回收)。
即使他存储的是某个对象的内存地址值,它也不会被算作该对象的被算作对该对象的引用
如果认为将对象地址存储在一个uintptr变量中,该对象就不会被垃圾回收器回收,那是对uintptr的最大误解
安全使用
我们既需要unsafe.Pointer打破类型的安全屏障,又需要器能够被安全的使用。如下有几条安全法则
*T1
-> unsafe.Pointer -> *T2
其本质就是内存重解释,将原本解释为T1的类型内存重新解释为T2类型。
这是unsafe.Pointer 突破Go类型安全的屏障的基本使用模式
注意:转换后类型T2, 对对其系数不能比转换前类型T1的对其系数更严格,即Alignof(T1) => Alignof(T2)
unsafe.Pointer -> uintptr
将unsafe.Pointer 显示转换为uinptr,并且转换后的uintptr类型不会在转换回unsafe.Pointer 只用于打印输出,并不参与其他操作。
模拟指针运算
操作任意内存地址上的数据都离不开指针运算。Go常规语法不支持指针运算,但我们可以使用unsafe.Pointer的第三种安全使用模式来模拟指针运算,即在一个表达式中,将unsafe.Pointer转换为uintptr类型,使用uintptr类型的值进行算术运算后,再转换回unsafe.Pointer
经常用于访问结构体内字段或数组中的元素,也常用于实现对某内存对象的步进式检查
注意事项:
- 不要越界offset理论上可以是任意值,这就存在算术运算之后的地址超出原内存对象边界的可能。 经过转换后,p指向的地址已经超出原数组a的边界,访问这块内存区域是有风险的,尤其是当你尝试去修改它的时候。
- unsafe.Pointer -> uintptr -> unsafe.Pointer的转换要在一个表达式中
调用syscall.Syscall系列函数时指针类型到uintptr类型参数的转换
将reflect.Value.Pointer或reflect.Value.UnsafeAddr转换为指针
reflect.SliceHeader和reflect.StringHeader必须通过unsafe.Pointer -> uintptr
构建
Tips:
使用unsfa包前,请牢记并理解unsafe.Pointer 的六条安全使用模式
如果使用了unsafe包,请使用go vet
等工具对代码进行unsafe包使用合规性检查
go go code tips
Go code tips
使用 pkg/errors
我们在一个项目中使用错误机制,最核心的几个需求是什么?我觉得主要是这两点:
- 附加信息:我们希望错误出现的时候能附带一些描述性的错误信息,甚至这些信息是可以嵌套的
- 附加堆栈:我们希望错误不仅仅打印出错误信息,也能打印出这个错误的堆栈信息,可以知道出错的具体代码。
在 Go 语言的演进过程中,error 传递的信息太少一直是被诟病的一点。使用官方的 error 库,只能打印一条简单的错误信息,而没有更多的信息辅助快速定位错误。所以,推荐在应用层使用 github.com/pkg/errors
来替换官方的 error 库。因为使用 pkg/errors
,我们不仅能传递出标准库 error 的错误信息,还能传递出抛出 error 的堆栈信息。
官方示例代码如下
1 |
package main |
在初始化 slice 的时候尽量补全 cap
当我们要创建一个 slice 结构,并且往 slice 中 append 元素的时候,我们可能有两种写法来初始化这个 slice。
1 |
// 直接使用[]int 的方式来初始化 |
方法二相较于方法一,就只有一个区别:在初始化[]int slice 的时候在 make 中设置了 cap 的长度,就是 slice 的大小。
而且,这两种方法对应的功能和输出结果是没有任何差别的,但是实际运行的时候,方法二会比方法一少运行了一个 growslice 的命令,能够提升我们程序的运行性能。具体我们可以打印汇编码查看一下。
这个 growslice 的作用就是扩充 slice 容量,每当我们的 slice 容量小于我们需要使用的 slice 大小,这个函数就会被触发。
它的机制就好比是原先我们没有定制容量,系统给了我们一个能装两个鞋子的盒子,但是当我们装到第三个鞋子的时候,这个盒子就不够了,我们就要换一个盒子,而换这个盒子,我们势必还需要将原先的盒子里面的鞋子也拿出来放到新的盒子里面。
而 growsslice 的操作是一个比较复杂的操作,它的表现和复杂度会高于最基本的初始化 make 方法。对追求性能的程序来说,应该能避免就尽量避免。如果你对 growsslice 函数的具体实现感兴趣,你可以参考源码 src 的 runtime/slice.go 。
当然,并不是每次都能在 slice 初始化的时候,就准确预估到最终的使用容量,所以我这里说的是“尽量补全 cap”。明白是否设置 slice 容量的区别后,我们在能预估容量的时候,请尽量使用方法二那种预估容量后的 slice 初始化方式。
初始化一个类的时候,如果类的构造参数较多,尽量使用 Option 写法
遇到一定要初始化一个类的时候,大部分时候都会使用类似下列的 New 方法:
1 |
type Foo struct { |
在这段代码中,我们定义一个 NewFoo 方法,其中存放初始化 Foo 结构所需要的各种字段属性。这个写法乍看之下是没啥问题的,但是一旦 Foo 结构内部的字段发生了变化,增加或者减少了,那么这个初始化函数 NewFoo
就怎么看怎么别扭了。参数继续增加?那么所有调用了这个 NewFoo 方法的地方也都需要进行修改,且按照代码整洁的逻辑,参数多于 5 个,这个函数就很难使用了。而且,如果这 5 个参数都是可有可无的参数,就是有的参数可以不填写,有默认值,比如
age 这个字段,即使我们不填写,在后续的业务逻辑中可能也没有很多影响,那么我在实际调用 NewFoo 的时候,age 这个字段还需要传递 0 值:
1 |
foo := NewFoo("payne", 1, 0, nil) |
乍看这行代码,你可能会以为我创建了一个 Foo,它的年龄为 0,但是实际上是希望表达这里使用了一个“缺省值”,这种代码的语义逻辑就不对了。
这里其实有一种更好的写法:使用 Option 写法来进行改造
Option 写法,顾名思义,就是将所有可选的参数作为一个可选方式,一般我们会设计一个“函数类型”来代表这个
Option,然后配套将所有可选字段设计为一个这个函数类型的具体实现。在具体的使用的时候,使用可变字段的方式来控制有多少个函数类型会被执行。比如上述的代码,我们会改造为:
1 |
|
我们创建了一个 FooOption 的函数类型,这个函数类型代表的函数结构是 func(foo *Foo) 。
这个结构很简单,就是将 foo 指针传递进去,能让内部函数进行修改。然后我们针对三个初始化字段 name,age,db 定义了三个返回了 FooOption 的函数,负责修改它们:
- WithName
- WithAge
- WithDB
以 WithName 为例,这个函数参数为 string,返回值为 FooOption。在返回值的 FooOption 中,根据参数修改了 Foo 指针。
1 |
// WithName 代表Name为可选参数 |
有兴趣可以看看知名爬虫框架colly-https://github.com/gocolly/colly/blob/master/colly.go#L55,构造
巧用大括号控制变量作用域
在写 Go 的过程中,你一定有过为 := 和 = 烦恼的时刻。一个变量,到写的时候,我还要记得前面是否已经定义过了,如果没有定义过,使用 := ,如果已经定义过,使用 =。
当然很多时候你可能并不会犯这种错误,如果变量命名得比较好的话,我们是很容易记得这个变量前面是否有定义过的。但是更多时候,对于 err 这种通用的变量名字,你可能就不一定记得了。
这个时候,巧妙使用大括号,就能很好避免这个问题。
小结
- 使用 pkg/error 而不是官方 error 库
- 在初始化 slice 的时候尽量补全 cap
- 初始化一个类的时候,如果类的构造参数较多,尽量使用 Option 写法
- 巧用大括号控制变量作用域
这几种写法和注意事项都是我在工作和阅读开源项目中的一些总结和经验,每个经验都是对应为了解决不同的问题。虽然说 Go 已经对代码做了不少的规范和优化,但是好的代码和不那么好的代码还是有一些差距的,这些写法优化点就是其中一部分。
阅读 少有人走的路
少有人走的路
我们在生活中经常会评价一个人说这个人不成熟像个孩子,那到底什么成熟,一个人成熟与否最重要的是自律性然后是否懂得什么是真正的爱。就是当一个人学会自律,主动的要求自己用积极的心态去面对承受痛苦解决问题的时候那就是距离成熟的距离很近了。而且我们生活中大量的人就宁愿在不成熟当中徘徊宁愿在痛苦当中打转,就是不愿意走上这少有人走的路。走上这条用自律和爱,铸就通向心灵承受的道路。
自律是解决人生问题的最重要的主要工具
全面的自律
不自律的表现
遇到问题拖延着不去主动解决回避问题,假装看不见用药物、酒精等成瘾物麻痹自己,换得一时解脱
实现自律的四个原则
推迟满足感:父母没耐心教孩子,而企图靠吼或打骂的方式一下子解决问题。童年得不到父母爱的孩子容易缺乏安全感,而陷入“今朝有酒今朝醉”处事模式儿无法做到延迟满足。
承担责任:必须面对自己的问题,并承担解决问题的责任
- 神经官能症:患者总是为自己增加责任
- 人格失调症:患者总是为自己推卸责任
忠于事实
越是了解事实,处理问题就越得心应手
移情:固守自己已成型的“人生地图”用来解决一切事,不肯改变
保持平衡:确立富有弹性的束缚机制来平衡以上三个原则保持平衡的最高原则,是放弃
爱是是自律的动力
爱的定义
爱上人们自律的原动力,是一种未来促进自己和他人心智成熟,而不断拓展再叫我界限,实现自我完善的意愿
爱的表现
当事人意识和潜意识中目标一致。 是一个人长期、渐进的过程,应用心灵不断的成长和心智不断成熟
爱他人者必自爱, 爱需要付出努力
爱一个人必须付诸行动,想爱,爱的感觉和口头的爱都不等于去爱
非爱的情况
堕入情网
堕入情网不是出于主观臆断,不是有计划、有一事的选择
堕入情网不需要付出努力,并没有真正拓展自我界限
堕入情网不是出于可以消除寂寞,但无法有目的促进心灵
过分依赖
没有他/她我就活不下去,是一种寄生心理,而不是爱症状有忍受的寂寞,空虚感强烈
把失去伴侣当成极其恐怖的事情
精神灌注
不能给心灵带来滋养的精神关注都不是爱
爱金钱、爱权利、爱宠物、爱园艺都被称为“爱”
如果这些爱好中增长了知识,活的了自我界限的拓宽,是爱,否则就不是爱
自我牺牲
给予者以 “爱”作为幌子,满足自己的付出欲。不将对方的心智成熟当回事
爱的体现
关注:努力聆听,帮助对方成长
不惧风险:不要因为害怕失去就放弃爱
独立:人生唯一的安全感,来自于成分体验人生的不安全感保持自己独立,也许伴侣独立
充分透入:由承担来推动,持续或渐渐增多的投入是爱的基石,给对方以安全感
勤于自律,平等交流:当感觉矛盾与冲突时,用恰当的语言提出想法,而非使用自大的态度随意批评
懂得自律:真正的爱必然懂得约束自己,并促进双发心智的成熟,真正的爱对自己的约束力,容不下第三者,对伴侣和孩子和孩子负有责任感
自我界限
自我界限的设立
0-7个月,婴儿分辨与外部 世界
1岁:婴儿开始意识到自己和外界的区别,明白“我”是什么,什么不是“我”
2-3岁:从界限不明,以为自己无所不能,到开始明白自己能力有限,有了自我界限,开始感到孤独
自我界限的暂时崩溃
坠入情网意味着意味着自我界限的某一部分突然崩溃,人有体会到了小时候无所不能的感觉,所以兴奋不已。等到冷静下来,发现彼此的问题,各自的自我界限又再次合拢
自我界限的拓宽
走出舒适区,努力为一件事情付出,对自我边界形成永久性的扩张,实现自我正常
信仰与恩典
盲目地认为宗教信仰有助于或有害于个人心智的成熟与健康,都不是我们应有的态度,因人而异,因事而异。
生命中出现的恩典与奇迹,虽不能尽数解释,也应当理智对待。
阻碍成熟的障碍—懒惰
逃避痛苦的原动力
恐惧直面问题的麻烦和痛苦,是懒惰的深层原因
GC GC垃圾回收原理
如何判断对象是垃圾
经典判断方法 1:引用计数法
思路很简单,但是如果出现循环引用,即 A 引用 B,B 又引用 A,这种情况下就不好办了。所以一般还使用了另一种称为“可达性分析”的判断方法。
经典判断方法 2:可达性分析
如果 A 引用 B,B 又引用 A(发生了循环引用问题),这 2 个对象是否能被 GC回收?
关键不是在于 A、B 之间是否有引用,而是 A、B 是否可以一直向上追溯到 GC Roots。如果与 GC Roots 没有关联,则会被回收;否则,将继续存活。
上图是一个用“可达性分析”标记垃圾对象的示例图,灰色的对象表示不可达对象,将等待回收
哪些内存区域需要 GC
常用的 4 种 GC 算法
mark-sweep 标记清除法
黑色区域表示待清理的垃圾对象,标记出来后直接清空。
优:简单快速;
缺:产生很多内存碎片。
mark-copy 标记复制法
思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。
优:避免了内存碎片问题;
缺:内存浪费很严重,相当于只能使用 50% 的内存。
mark-compact 标记-整理(也称标记-压缩)法
将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续。
优:节约了内存,并避免了内存碎片问题。
缺:整理过程会降低 GC 的效率。
上述三种算法,每种都有各自的优缺点,都不完美;在现代 JVM 中,往往是综合使用的。经过大量实际分析,发现内存中的对象,大致可以分为两类:
有些生命周期很短,比如一些局部变量/临时对象;
而另一些则会存活很久,典型的比如 websocket 长连接中的 connection 对象。如下图,纵向 y 轴可以理解分配内存的字节数,横向 x 轴理解为随着时间流逝(伴随着 GC)。
可以发现大部分对象其实相当短命,很少有对象能在 GC 后活下来,因此诞生了分代的思想。
generation-collect 分代收集算法
如下图所示,可以将内存分成了三大块:年青代(Young Genaration)、老年代(Old Generation)、永久代(Permanent Generation)。其中 Young Genaration 更是又细为分
eden、S0、S1 三个区。
结合我们经常使用的一些 jvm 调优参数后,一些参数能影响的各区域内存大小值,示意图如下:
GC 的主要过程
刚开始时,对象分配在 eden 区,s0(即:from)及 s1(即:to)区几乎是空着。
随着应用的运行,越来越多的对象被分配到 eden 区。
当 eden 区放不下时,就会发生 minor GC(也被称为 young GC)。
首先当然是要先标识出不可达垃圾对象(即下图中的黄色块);
然后将可达对象,移动到 s0 区(即:4个淡蓝色的方块挪到s0区);
然后将黄色的垃圾块清理掉,这一轮后 eden 区就成空的了。
注:这里其实已经综合运用了“【标记-清理eden】+【标记-复制 eden->s0】”算法。
随着时间推移,eden 如果又满了,再次触发 minor GC,同样还是先做标记,这时 eden 和 s0 区可能都有垃圾对象了(下图中的黄色块)。
这时 s1(即:to)区是空的,s0 区和 eden 区的存活对象,将直接搬到 s1 区。
然后将 eden 和 s0 区的垃圾清理掉,这一轮 minor GC 后,eden 和 s0 区就变成了空的了。
继续随着对象的不断分配,eden 空可能又满了,这时会重复刚才的 minor GC 过程。不过要注意的是:
这时候 s0 是空的,所以 s0 与 s1 的角色其实会互换。即存活的对象,会从 eden 和 s1 区,向 s0 区移动。
然后再把 eden 和 s1 区中的垃圾清除,这一轮完成后,eden 与 s1 区变成空的,如下图。
代龄与晋升
对于那些比较“长寿”的对象一直在 s0 与 s1 中挪来挪去,一来很占地方,而且也会造成一定开销,降低 gc 效率,于是有了“代龄(age)”及“晋升”。
对象在年青代的 3 个区(edge,s0,s1)之间,每次从一个区移到另一区,年龄 +1,在 young 区达到一定的年龄阈值后,将晋升到老年代。
下图中是 8,即挪动 8 次后,如果还活着,下次 minor GC 时,将移动到 Tenured 区。
晋升的主要过程
下图是晋升的主要过程:对象先分配在年青代,经过多次 Young GC 后,如果对象还活着,晋升到老年代。
如果老年代,最终也放满了,就会发生 major GC(即 Full GC)。由于老年代的的对象通常会比较多,标记-清理-整理(压缩)的耗时通常也会比较长,会让应用出现卡顿的现象。这也就是为什么很多应用要优化,尽量避免或减少 Full GC
的原因。
注:上面的过程主要来自 oracle 官网的资料,但是有一个细节官网没有提到:如果分配的新对象比较大,eden 区放不下,但是 old 区可以放下时,会直接分配到 old 区。
即没有晋升这一过程,直接到老年代了。
GC 流程图
8 种垃圾回收器
这些回收器都是基于分代的,把 G1 除外,按回收的分代划分如下。
横线以上的 3 种:Serial、ParNew、Parellel Scavenge 都是回收年青代的;
横线以下的 3 种:CMS、Serial Old、Parallel Old 都是回收老年代的。
接下来,我们将以上提到的 8 种垃圾回收器逐一讲解,其中 CMS、G1、ZGC 这三种收集器是面试考试重点,我也会着重讲解。
Serial 收集器
单线程用标记-复制算法,快刀斩乱麻,单线程的好处避免上下文切换,早期的机器,大多是单核,也比较实用。但执行期间会发生 STW(Stop The World)。
ParNew 收集器
Serial 的多线程版本,也同样会 STW,在多核机器上会更适用。
Parallel Scavenge 收集器
ParNew 的升级版本,主要区别在于提供了两个参数:
-XX:MaxGCPauseMillis 最大垃圾回收停顿时间;
-XX:GCTimeRatio 垃圾回收时间与总时间占比。
通过这 2 个参数,可以适当控制回收的节奏,更关注于吞吐率,即总时间与垃圾回收时间的比例。
Serial Old 收集器
因为老年代的对象通常比较多,占用的空间通常也会更大。如果采用复制算法,得留 50% 的空间用于复制,相当不划算;而且因为对象多,从一个区,复制到另一个区,耗时也会比较长。
所以老年代的收集,通常会采用“标记-整理”法。从名字就可以看出来,这是单线程(串行)的, 依然会有 STW。
Parallel Old 收集器
一句话:Serial Old 的多线程版本。
CMS 收集器
Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是 JDK 7 中广泛使用的收集器,有必要多说一下。
G1 收集器
鉴于 CMS 的一些不足之外,比如:老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1 就横空出世了,它对于 heap 区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。
G1 的全称是 Garbage-First
G1 垃圾收集器的原理
如下图,G1 将 heap 内存区,划分为一个个大小相等(1-32M,2的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,但是 region
与 region 之间不要求连续。
注:Humongous,简称 H 区是专用于存放超大对象的区域,通常 >= 1/2 Region Size,且只有 Full GC 阶段,才会回收 H 区,避免了频繁扫描、复制/移动大对象。
所有的垃圾回收,都是基于 1 个个 region 的。JVM 内部知道,哪些 region 的对象最少(即该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快)。这就是 Garbage-First 得名的由来,G
即是 Garbage 的缩写,1 即 First。
- G1 Young GC young GC 前:
young GC 后:
理论上讲,只要有一个 Empty Region(空区域),就可以进行垃圾回收。
由于 region 与 region 之间并不要求连续,而使用 G1 的场景通常是大内存,比如 64G 甚至更大,为了提高扫描根对象和标记的效率,G1 使用了二个新的辅助存储结构:
-
Remembered Sets:简称 RSets,用于根据每个 region 里的对象,是从哪指向过来的(即谁引用了我),每个 Region 都有独立的 RSets(Other Region -> Self Region)。
-
Collection Sets :简称 CSets,记录了等待回收的 Region 集合,GC 时这些 Region 中的对象会被回收(copied or moved)。
RSets 的引入,在 YGC 时,将年青代 Region 的 RSets 做为根对象,可以避免扫描老年代的 region,能大大减轻 GC 的负担。
注:在老年代收集 Mixed GC 时,RSets 记录了 Old->Old 的引用,也可以避免扫描所有 Old 区。
Old Generation Collection(也称为 Mixed GC)
按 oracle 官网文档描述,分为 5 个阶段:Initial Mark(STW) -> Root Region Scan -> Cocurrent Marking -> Remark(STW) -> Copying/Cleanup(
STW && Concurrent)
注:也有很多文章会把 Root Region Scan 省略掉,合并到 Initial Mark 里,变成 4 个阶段。
阶段 1:存活对象的“初始标记”依赖于 Young GC,GC 日志中会记录成 young 字样。
阶段 2:并发标记过程中,如果发现某些 region 全是空的,会被直接清除。
阶段 3:进入重新标记阶段。
阶段 4:并发复制/清查阶段。这个阶段,Young 区和 Old 区的对象有可能会被同时清理。GC 日志中,会记录为 mixed 字段,这也是 G1 的老年代收集,也称为 Mixed GC 的原因。
上图是,老年代收集完后的示意图。
通过这几个阶段的分析,虽然看上去很多阶段仍然会发生 STW,但是 G1 提供了一个预测模型,通过统计方法,根据历史数据来预测本次收集,需要选择多少个 Region 来回收,尽量满足用户的预期停顿值(-XX:MaxGCPauseMillis
参数可指定预期停顿值)。
注:如果 Mixed GC 仍然效果不理想,跟不上新对象分配内存的需求,会使用 Serial Old GC(Full GC)强制收集整个 Heap。
小结:与 CMS 相比,G1 有内存整理过程(标记-压缩),避免了内存碎片;STW 时间可控(能预测 GC 停顿时间)。
ZGC(截止目前史上最好的 GC 收集器)
在 G1 的基础上,它做了如下 7 点改进
动态调整大小的 Region
G1 中每个 Region 的大小是固定的,创建和销毁 Region,可以动态调整大小,内存使用更高效。
不分代,干掉了 RSets
G1 中每个 Region 需要借助额外的 RSets 来记录“谁引用了我”,占用了额外的内存空间,每次对象移动时,RSets 也需要更新,会产生开销。
带颜色的指针 Colored Pointer
这里的指针类似 Java 中的引用,意为对某块虚拟内存的引用。ZGC 采用了64位指针(注:目前只支持 linux 64 位系统),将 42-45 这 4 个 bit 位置赋予了不同含义,即所谓的颜色标志位,也换为指针的 metadata。
-
finalizable 位:仅 finalizer(类比 C++ 中的析构函数)可访问;
-
remap 位:指向对象当前(最新)的内存地址,参考下面提到的relocation;
-
marked0 && marked1 位:用于标志可达对象。
这 4 个标志位,同一时刻只会有 1 个位置是 1。每当指针对应的内存数据发生变化,比如内存被移动,颜色会发生变化。
读屏障 Load Barrier
传统 GC 做标记时,为了防止其他线程在标记期间修改对象,通常会简单的 STW。而 ZGC 有了 Colored Pointer 后,引入了所谓的“读屏障”。
当指针引用的内存正被移动时,指针上的颜色就会变化,ZGC 会先把指针更新成最新状态,然后再返回(你可以回想下 Java 中的 volatile 关键字,有异曲同工之妙)。这样仅读取该指针时,可能会略有开销,而不用将整个 heap STW。
重定位 Relocation
如下图,在标记过程中,先从 Roots 对象找到了直接关联的下级对象 1,2,4。
然后继续向下层标记,找到了 5,8 对象, 此时已经可以判定 3,6,7 为垃圾对象。
如果按常规思路,一般会将 8 从最右侧的 Region,移动或复制到中间的 Region,然后再将中间 Region 的 3 干掉,最后再对中间 Region 做压缩 compact 整理。
但 ZGC 做得更高明,它直接将 4,5 复制到了一个空的新 Region 就完事了,然后中间的 2 个 Region 直接废弃,或理解为“释放”,作为下次回收的“新” Region。这样的好处是避免了中间 Region 的 compact
整理过程。
最后,指针重新调整为正确的指向(即:remap),而且上一阶段的 remap 与下一阶段的mark是混在一起处理的,相对更高效。
多重映射 Multi-Mapping
这个优化,说实话没完全看懂,只能谈下自己的理解(如果有误,欢迎指正)。虚拟内存与实际物理内存,OS 会维护一个映射关系,才能正常使用,如下图:
zgc 的 64 位颜色指针,在解除映射关系时,代价较高(需要屏蔽额外的 42-45 的颜色标志位)。考虑到这 4 个标志位,同 1 时刻,只会有 1 位置成 1(如下图),另外 finalizable
标志位,永远不希望被解除映射绑定(可不用考虑映射问题)。
所以剩下 3 种颜色的虚拟内存,可以都映射到同1段物理内存。即映射复用,或者更通俗点讲,本来 3 种不同颜色的指针,哪怕 0-41 位完全相同,也需要映射到 3 段不同的物理内存,现在只需要映射到同 1 段物理内存即可。
支持 NUMA 架构
NUMA 是一种多核服务器的架构,简单来讲,一个多核服务器(比如 2core),每个 cpu 都有属于自己的存储器,会比访问另一个核的存储器会慢很多(类似于就近访问更快)。
相对之前的 GC 算法,ZGC 首次支持了 NUMA 架构,申请堆内存时,判断当前线程属是哪个CPU在执行,然后就近申请该 CPU 能使用的内存。
小结:革命性的 ZGC 经过上述一堆优化后,每次 GC 总体卡顿时间按官方说法<10ms。
注:启用 zgc,需要设置 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC。
Remap 的流程图
抓包 雷电模拟器抓包配置
雷电模拟器抓包配置
爬虫免不了抓包分析,当然也免不了中间转发相关的便捷操作。如果有一个稳定的、妥善的抓包环境。相信在爬虫开发中一定会事半功倍。
有真机当然是最好的,同时如果爬虫开发者并没有真机。那么模拟器变成为了不二之选。当然如果支持多开的话那就更好了。
业界比较知名的有
MuMu模拟器:https://mumu.163.com/
雷电模拟器:https://www.ldmnq.com/
夜神模拟器:https://www.yeshen.com/
在开发爬虫的时候,我个人使用的比较多的的是雷电模拟器,因为他支持多开(虽然这之间有限制),以及抓包也有些问题。但奈何,它支持多开啊。配合 Mitmproxy的组件譬如Mitmdump
简直不要太舒服。
雷电模拟器的安装
首先进入官网, 如下图所示,https://www.ldmnq.com/
选择更新日志,寻找历史版本。笔者这里使用的是3.74 较为稳定。以及支持网桥。
https://dl.softmgr.qq.com/original/game/ldinst_3.74.0.exe
选择3.74版本,主要是由于新版本的网桥这个bug在修复。以及完善度如何,至今不敢恭维。so,就选择较为稳定的它。
必须需要支持网桥,否则无法抓包。
抓包工具安装
抓包工具这里没什么好讲的,常规常见的都都行。
fiddle:https://www.telerik.com/download/fiddler
Charles: https://www.charlesproxy.com/
mitmproxy:https://www.mitmproxy.org/
都比较不错,具体选择那个请你酌情考虑。
雷电模拟器抓包配置
首先新建一个雷电模拟器,如下图所示。以及网桥设置如下。
当然记得安装桥接的驱动。
完成后,如下所示
以Mitmproxy的证书为例子,如下
首先需要安装mitmproxy,执行如下命令(在此之前需要有Python环境)
1 |
pip install mitmproxy |
如果配置了环境变量的话,首次执行一下mitmproxy相关的命令(由于非Uinux,是没有mitmrpoxy的),所以执行mitmweb
, 我个人建议是添加对应的端口。命令如下所示
1 |
mitmweb -p 8088 |
当然,我建议是8088,我习惯给他配置为mitm相关的抓包的端口。当然你可以选择其他的端口,当然后续操作就可能会有些许出入。
此时,进入~.mitmproxy
目录下
1 |
# windows |
使用ls
命令即可查看到相关的证书了!。我建议是在这里寻找到证书而非mitm.it,因为在手机上很有可能上不去
配置代理
在配置证书前,我强烈建议你先配置好代理端口。这将大大到降低与简化后续的操作。由于模拟器都是采用的自生的网络。直接配置WI-FI即可。如下
点击修改网络,以及高级配置,同时将代理选择为手动。如下图所示
端口在这里我设置为8088
, 代理服务器主机名。你可以在终端中的ifconfig
或者ipconfig
,中获取。在此便不再过多赘述。
配置证书
在~.mitmproxy
下将昵称为mitmproxy-ca-cert.pem
的文件拖入到雷电模拟器中,如下图所示
将该文件移动屏幕内,即可跳转到该目录
开始配置证书,打开设置-> 安全-> 从SD卡安装 选择证书所在的目录。双击即可安装
在终端中运行
1 |
mitmweb -p 8088 |
浏览百度
至此,证书就已经安装完成啦
Android 7 及以上的版本抓包
1 |
adb shell |
Referer
command Linux command多版本设置
在Linux上有这样一个需求,需要将Python的默认版本设置为python3.8
但由于Linux系统自带的是3.6.那么实现他只需要完成python3.8的安装,以及python命令的指向问题。
python3.8 的安装在此便不再过多赘述,如下命令都可任选其一即可
1 |
sudo yum install -y python38 python38-pip |
修改指向
alternatives 修改
1 |
alternatives --config python3 |
此时直接输入1,修改即可。如下图所示
alternatives —install
1 |
alternatives --install <链接> <名称> <路径> <优先度> |
软链接
1 |
ln -fs /usr/bin/python3.8 /usr/bin/python3 |