程序如何装载

Main\.java, Minor\.java -> jar包.java Main\.main(): 编译打包
jar包.java Main\.main() -> 验证: 加载
jar包.java Main\.main() -> Minor\.class: 使用
Minor\.class -> JVM: 加载
验证 -> 准备 -> 解析 -> 初始化 -> JVM

加载:从磁盘加载到内存。(懒加载,用到类才加载,如main方法或new对象)
验证:验证字节码是否正确、是否可识别。
准备:初始化静态(static,不包括常量)变量、赋初值(默认值)。
解析:符号引用 -> 直接引用。静态方法(如main) -> 指向数据所在内存的指针。这是静态链接,在类加载期间完成;而动态链接在程序运行期间完成。
初始化:为静态变量赋值,执行静态代码块。

类加载器

加载过程由类加载器实现,有几种类加载器:

  1. 引导类加载器(C++):JRE核心lib的jar类包
  2. 扩展类加载器:JRE拓展lib(ext)jar类包
  3. 应用程序类加载器:ClassPath路径下的类包(自己编写的类)
  4. 其他加载器:加载自定义路径下的类包
java com\.site\.jvm\.Math\.class -> java\.exe调用底层jvm\.dll创建Java虚拟机 -> 创建引导类加载器实例
创建引导类加载器实例 -> sum\.misc\.Launcher\.getLauncher(): C++调用Java代码,创建JVM启动器实例,这个实例负责创建其他类加载器
sum\.misc\.Launcher\.getLauncher() -> launcher\.getClassLoader(): 获取运行类自己的加载器ClassLoader(AppClassLoader实例)
launcher\.getClassLoader() -> classLoader\.loadClass("com\.site\.jvm\.Math"):调用loadClass加载即将要运行的类
classLoader\.loadClass("com\.site\.jvm\.Math") -> Math\.main(): 加载完成后,JVM执行Math.main()
创建引导类加载器实例 -> Math\.main(): C++发起调用
Math\.main()-> JVM销毁: Java程序运行结束

双亲委派机制

direction: up
应用程序类加载器 -> 拓展类加载器: 向上委托
拓展类加载器 -> 引导类加载器: 向上委托
引导类加载器 -> 拓展类加载器: 父加载器加载失败,由子加载器自己加载
拓展类加载器 -> 应用程序类加载器: 父加载器加载失败,由子加载器自己加载

pros:

  • 避免重复加载:下层加载了,上层不会加载;
  • 沙箱安全机制:可以防止核心API被篡改
    cons: 上层不能调动下层,层层传递比较繁琐

打破双亲委派 避免弊端

通过ContentTextClassLoader反向委托,可以使上层调用你想要用的加载器。

典型案例 Tomcat8

Tomcat不遵循双亲委派机制,自己写一个HashMap类,会不会有风险?

Tomcat不遵循双亲委派机制,只是自定义的classloader顺序不同,但是还是需要到顶层请求classloadder

JVM内存布局

grid-rows: 3
cf: 类文件\nClass Files
cf.shape: page
cls: 类加载子系统\nClass Loader Subsystem
rda: 运行时数据区(Runtime Data Area): {
方法区(共享)\nMethod Area
程序计数器\nPC Reg
本地方法栈\nNative Method Stack
堆(共享)\nHeap
虚拟机栈\nJVM Stack
}
ee: 执行引擎\nExecution Engine
nmi: 本地方法接口\nNative Method Interface
nml: 本地方法库\nNative Method Libs

cf <-> cls
cls <-> rda
rda <-> ee
rda <-> nmi
ee <-> nmi
nmi <-> nml

垃圾回收

direction: right
young: 年轻代: {
ed: Eden(8)
s: Survivor区: {
s0: s0(1)
s1: s1(1)
}
}
young.ed -> young.s.s0 <-> young.s.s1 -> Old(2/3)

当Eden区不够放,就会执行Minor GC,将空指针、无用对象回收,并把有用对象放入s0/s1(然后清楚Eden和另外一块survivor区的所有对象),年龄+1;
年龄到15时,会把对象放入老年代。
但是如果s0/s1放不下Minor GC后存活的对象,会直接放入老年代。
老年代满了,会触发Full GC,会暂停所有用户线程(STW, Stop The World)。

对象创建

start: 类加载检查
cond: 是否已加载类
cond.shape: diamond
yes: 分配内存
yes -> 初始化 -> 设置对象头 -> 执行\<init\>方法
no: 加载类
start -> cond
cond -> no: 否
no -> yes
cond -> yes: 是

分配内存

  1. 指针碰撞(默认方法):Java堆中的内存规整分配,没有碎片,那么只需要在最后一个对象的指针后加上一段偏移量(大小)即可完成分配。
  2. 空闲列表:Java堆中内存分配不规整,碎片化。需要维护一张表,记录哪些空间可用。在分配内存时,找到足够大的空间划分给对象实例。

在并发的情况下,可能出现正在给A分配内存,指针未修改,此时又给B分配内存,B内存区与A重合的情况。

并发解决方法

  1. CAS(Compare and Swap):比较交换,虚拟机就采用CAS+失败重试的方法保证原子性。
  2. TLAB(Thread Local Allocation Buffer):按照不同线程划分内存区域,每个线程在区域内分配,防止重合。

Object Header 对象头

  1. Mark Word标记字段:运行时数据哈希值、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳,32位4B,64位8B
锁状态 23bit 2bit 4bit 1bit
是否指向偏向锁
2bit
锁标志位
无锁态 对象的- -HashCode 分代年龄 0 01
轻量级锁 指向- -栈中锁- -记录的- -指针 00
重量级锁 指向- -互斥量- -(重量级锁)- -的指针 10
GC标记 - - - - 11
偏向锁 线程ID Epoch 分代年龄 1 01
  1. Klass Pointer类型指针:指向类的元数据,8B,压缩后4B
  2. 数组长度:4B

<init> 方法

执行<init>方法,会将对象按照程序的意愿进行初始化,是真正的属性赋值(不是赋初值0),会执行构造方法。

对象内存分配

start -> cond1: new Object()
cond1: 栈内分配?
cond1 -> 栈: Y
栈 -> End: POP
cond1 -> cond2: N
cond2: 大对象?
cond2 -> Old: Y
Old -> End: Full GC
cond2 -> cond3: N
cond3: TLAB?
cond3 -> Eden: Y
cond3 -> Eden: N
condMgc: Minor GC?
Eden -> condMgc
condMgc -> S1: N
S1 -> Age?
Age? -> Old: Y
Age? -> S2: N
S2 -> condMgc
condMgc <-> End: Y

对象逃逸分析

public Object method1() {
Object obj = new Object();
...
return Object; // 被其他方法使用,逃逸
}

public void method2() {
Object obj = new Object();
...
businessMapper.insert(obj); // 生命周期和函数一起结束,非逃逸
}

非逃逸对象会分配在栈空间,随着方法结束被释放;而逃逸对象会真正分配在堆上。

Survivor到老年代

一块Survivor区,一批对象占用<=50%的内存大小。如果一批对象大于这个值,那么大于等于这批对象最大年龄的对象都会被放入老年代。

因此,如果一秒时间内新对象太多,超出这个Survivor区的生存阈值,就会直接放入老年代,从到导致更频繁的Full GC。解决方法是:1. 要么扩大Eden,让Minor GC间隔更长;2. 要么扩大Survivor,让垃圾对象能被放入,并在下一次GC及时释放,不会被错误地放入老年代。

Full GC

每次Minor GC,都会判断老年代内存空间是否够Survivor区的对象放入老年代,不够,则触发Full GC,还不够,则OOM
当老年代空间不够放,JVM会尽快Full GC(因为即使Minor GC可能也不够放,反而浪费了时间)

对象内存回收算法

1. 引用计数器

新增一个引用,计数器加1;一个引用失效,计数器减1。
实现简单,但是存在循环引用问题,A<->B之间互相引用,于是无法回收。

2. 可达性分析

以GC Roots对象(线程栈本地对象、静态变量、本地方法栈变量等)作为起点,向下搜索引用对象,找到的对象都标记为非垃圾对象。

无用类判断

同时满足以下3个条件即为无用类:

  1. 所有实例都已被回收
  2. 该类的ClassLoader已被回收
  3. 该类的java.lang.Class对象没有被引用。
    一般只有自定义的类加载器会被回收。

垃圾收集

垃圾收集算法

分代收集理论

根据对象存活周期的不同,将内存分成几块(在不同的年龄代,用不同的垃圾收集算法)。一般Java堆分为新生代和老年代。
例如,新生代几乎99%的对象会被回收,所以简单地标记-复制就可以完成内存整理;而老年代存活几率比较高,而且可能没有额外空间分配,因此需要用标记-清除、标记-整理算法。

标记-复制比标记-清除和标记-整理快10倍以上。

标记-复制算法

这个算法把内存分为大小相等的两块,每次整理只需要将一端的存活对象标记好,复制到另一端,然后把这一端内存全部清空。因此,使用标记-复制算法,每次都对内存区间的一半进行回收。(内存利用率低,最多只有50%)

标记-清除算法

这个算法简单地标记存活对象,然后一次性清除未被标记的对象。算法实现简单,但是也有问题:

  1. 效率低。如果标记的对象太多而且内存不连续,效率不高。
  2. 空间碎片化。只是简单地清除对象而不整理,会产生大量内存碎片,缺少整块内存。

标记-整理算法

这个算法标记存活对象,然后让所有存活对象都向内存一端移动。然后清除其余的内存区间,获得整块连续的内存空间。

垃圾收集器

垃圾收集器是内存回收的具体实现。

1 Serial收集器

串行收集器是最基础的收集器。它的单线程体现在:

  1. 只用一条垃圾收集线程
  2. 垃圾收集时,Stop The World
    Serial在新生代-XX:+UserSerialGC使用标记-复制算法,在老年代-XX:+UserSerialOldGC使用标记-整理算法。

2 Parallel Scavenge收集器(JDK1.8默认收集器)

并行收集器,实际上就是Serial的多线程版本,回收时同样会Stop The World。默认的收集线程数与CPU核心数量保持一致。
并行收集器关注CPU吞吐量,减少用户线程停顿时间。
Parallel Scavenge同样在新生代-XX:+UserParallelGC使用标记-复制算法,在老年代-XX:+UserParallelOldGC使用标记-整理算法。

ParNew收集器

ParNew收集器-XX:+UseParNewGC和Parallel类似,但是可以与CMS配合使用。

3 CMS收集器

CMS(Concurrent Mark Sweep)收集器只适用于老年代,以最短停顿时间为目标。是HotSpot JVM第一款并发收集器,可以使垃圾回收与用户线程同时工作。
CMS使用的是标记-清除算法(Mark Sweep),运作过程是:

  1. 初始标记:STW,记录GC Roots直接引用的对象。
  2. 并发标记:从GC Roots直接关联对象开始遍历整个对象图,过程中不需要暂停用户线程。因为没有STW,所以过程中已经标记的对象状态会改变。
  3. 重新标记:STW,用三色标记的增量更新算法做重新标记,修正第2步状态改变的对象标记记录。
  4. 并发清理:恢复用户线程,同时开始GC清扫。
  5. 并发重置:重置本次GC的标记数据。
    CMS主要优点是并发收集、短停顿,然而也有下面几个缺点:
  6. 对CPU资源敏感,会和服务器抢资源。
  7. 无法处理浮动垃圾(并发标记、并发清理阶段产生的新垃圾),只能等下一次GC再清理。
  8. 清扫算法,不能腾出整块连续内存,只能得到许多内存碎片。
  9. 执行不确定性。在并发标记、并发清理阶段会出现上一轮垃圾回收还没完成,下一轮又开始的情况。

并发失败

在CMS并发标记、并发清理阶段,如果用户线程又实例化了许多新对象,导致老年代触发Full GC,STW,那么这个清理线程实际上就失败了,还是用效率低下的线性收集器收集。

CMS参数

-XX:+UseConcMarkSweepGC 启用CMS(老年代)
-XX:ConcGCThreads 并发GC线程数量
-XX:+UseCMSCompactAtFullCollection FullGC后碎片整理
-XX:CMSFullGCsBeforeCompaction 设置每隔多少次FullGC做一次碎片整理,默认值为0
-XX:CMSInitiatingOccupancyFraction 设置老年代FullGC空间占比阈值,默认是92;不设置100是为了避免并发失败启用线性收集器
-XX:+UseCMSInitiatingOccupancyOnly 强制使用设定阈值。默认是只在第一次使用设定的阈值,后续动态调整
-XX:+CMSScavengeBeforeRemark CMSGC前启动一次MinorGC,降低CMS标记阶段的开销
-XX:+CMSParallelInitialMarkEnabled 初始标记阶段使用多线程,缩短STW
-XX:+CMSParallelRemarkEnabled 重新标记阶段采用多线程,缩短STW

三色标记

黑色:全部引用都扫描过的对象,存活
灰色:还没完全扫描的对象
白色:未扫描的对象。扫描开始时所有对象为白色;扫描结束后,清理白色对象
三色标记可能会产生漏标的问题,对于灰色对象中还没扫描的对象,如果这个对象被已经扫描过的黑色对象引用,而灰色对象的引用又被置null,那么这个应该被扫描和标记的对象就会被漏扫,从而导致错误清理。

漏标-读写屏障

漏标会错误清理,是非常严重的错误,会使用增量更新和原始快照两种方法避免:

  • 增量更新(IU):当黑色对象新增白色对象引用时,会把这个新增引用记录下来(这样黑色就变成灰色对象),等并发扫描结束,重新扫描这些黑色对象。
  • 原始快照(STAB):当灰色对象删除白色对象的引用时,记录这个引用删除,等并发扫描结束,将这些白色对象直接设置为黑色对象,使这一轮GC不清理这些对象。

写屏障

void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 写屏障,记录旧值(先加入队列,与实际操作异步)
*field = new_value; // 赋值操作
post_write_barrier(field, value); // 写屏障,记录新值(入队,异步执行)
}

CMS使用的是写屏障+增量更新方案
G1,Shenandoah使用写屏障+STAB方案
ZGC使用读屏障方案

为什么G1使用STAB,CMS用增量更新?

不处理,效率更高。有多少被删除的引用会真的被黑色对象引用呢?再做一次深度扫描太浪费了。而G1是为大内存设计的,分了很多区域,与只有一块老年代区域的CMS不同,做深度扫描的成本会高很多。

记忆集与卡表

在新生代做GC Roots可达性扫描过程中,可能会碰到跨代引用的问题(如被老年代引用)。这时,如果要进入老年代做扫描,效率太低了。
因此,在新生代引入记录集(Remember Set)数据结构,记录从非收集区到收集区的指针集合,避免把整个老年代纳入GCRoots扫描范围。

Hotspot使用一种叫“卡表”(Cardtable)的方式实现记忆集。
卡表使用一个字节数组CARD_TABLE[]实现,其中每个元素对应一块特定大小的内存,称为“卡页”。
Hotspot使用的卡页大小为2^9 = 512字节。即CARD_TABLE1字节对应512B(0~511)

具体来说,老年代按卡页被分割成许多个区块。当老年代地址0x0200(2*16^2=512B)区块引用了新生代的对象,CARD_TABLE[] = { 0, 1, ... },将对应区域标记为dirty。此时,新生代不仅会做GC Roots扫描,还会到老年代对应的地址0x0200扫描引用。

卡表状态也是用写屏障维护。

G1收集器

使用-XX:+UseG1GC启动G1(Garbage-First)。
G1不再使用原来的分代概念,而是将内存分割成大小相等的区域(Region)。JVM最多可以有2048个Region(每块区域大小=堆大小/2048)。
G1保留了年轻代、老年代的概念,但是它们都可以随机放在任何一块Region中,而不是物理隔离。
默认的年轻代占堆空间的5%,在运行过程中,JVM会给年轻代增加内存,但是年轻代不会超过60%。在年轻代内部,仍然保持8:1:1的比例

G1的特点是有一个Humongous(极大的)分区,专门存放短期巨型对象(超过region50%就算大对象,如果超过一个region大小,那就跨region存放),而不是直接放入老年代。在FullGC时,会将Humongous区垃圾对象一并回收。

GC过程

  1. 初始标记,STW
  2. 并发标记,和CMS一样
  3. 最终标记:和CMS重新标记一样
  4. 筛选回收:对各个Region的回收价值/成本进行排序,根据用户期望停顿时间-XX:MaxGCPauseMillis,默认200ms制定回收计划。

因为是用户设置的停顿时间,所以G1直接STW并发回收提高效率。

G1主要使用复制算法,将一个region的存活对象放入另一个region中,内存碎片比较少,而且不像CMS需要整理。
G1因为内部实现复杂,没用实现并发回收。Shenandoah就实现了这一点,可以看作G1的升级版。
G1在后台维护一个优先队列,优先选择允许时间内价值最大(回收空间最多)的Region

G1垃圾收集分类

YoungGC

YoungGC会计算Eden区回收所需的时间,接近用户设置的允许时间时,就会触发YoungGC。
G1通过动态调整Eden区大小(默认5%)来实现上述算法。

MixedGC

老年代对占有率达到-XX:InitiatingHeapOccupancyPercent,默认45的设定值就会触发,将回收所有Young、部分Old和Humongous区域。
G1会优先在老年代做MixedGC,如果复制对象过程中,没有足够的内存,那么会触发FullGC。

FullGC

STW,线性标记-压缩,整个过程比较耗时;Shenandoah优化这个过程为多线程。

ZGC

ZGC的设计目标:

  1. TB级别的堆内存
  2. GC Pause <= 10ms
  3. 下一代GC特征基础
  4. 最多15%的吞吐量下降
    ZGC源自Azul的C4,最大优势是,停顿时间与堆大小无关,而是都在10ms内。

NUMA架构,识别每块CPU使用的内存区域,防止竞争和锁的效率问题

ZGC运作过程

  1. 并发标记:ZGC不把标记放在对象内部,而是在颜色指针上标记
  2. 并发预备重分配:得出本次收集需要清理的Region并放入重分配集Relocation Set,与G1不同,ZGC每次都会扫描所有Region,这样就不需要维护卡表
  3. 并发重分配:会将Relocation Set的存活对象复制到新的Region上。由于会发生数据不同步问题。因此维护一个转发表(Forward Table),并通过读屏障来确保数据一致,ZGC称之为Self-Healing。
  4. 并发重映射:修正整个堆指向重分配集中,旧对象的所有引用

读屏障(懒更新)

由于ZGC使用颜色指针,而复制对象的过程中会发生数据不同步问题。
所以,ZGC直到原来的指针被读取(即此时没有发生写入),才会真正地修正指针引用,成为读屏障。

转发表

读屏障怎么知道地址有没有变化?在并发重分配阶段维护转发表,就知道对象去向。

颜色指针

以前的垃圾回收器GC信息保存在对象头,而ZGC将这些信息保存在指针上。
每个对象有一个64位的指针,其中

  • 42位用于寻址(4^42=4T)
  • 1位Marked1标识
  • 1位Marked0标识
  • 1位Remapped标识,设置后,说明对象没有指向RelocationSet
  • 1位Finalizable标识,与并发引用处理有关,表示这个对象只能通过finalizer访问
  • 18位未使用

为什么2个Mark

每个GC周期开始,会交替使用标记位(01、10互换),使上次GC标记失效。

颜色指针3大优势

  1. 一旦某个Region的存活对象被移走,这个Region能够立刻被释放和重用。
  2. 颜色指针大幅减少内存屏障使用数量,ZGC使用读屏障
  3. 颜色指针有强大扩展性(18位)

ZGC问题

最大的问题也是浮动垃圾,ZGC的停顿时间是10ms,但是实际上执行回收时间远大于这个值,在此期间会产生许许多多不能处理的新垃圾对象,只能等待下一次回收。

解决方法只有增加堆容量,让程序有更多喘息时间(未分代情况下)。
分代ZGC区分新生代和老年代(代际隔离),对新生代的回收更频繁(分代回收)。

JVM调优工具

Jmap

jmap可以查看内存信息、实例个数以及所占内存大小

$ jps
<PID> <Name>
22928 Jps

$ jmap -histo <PID> > jlog
# 查看堆情况
$ jmap -heap <PID>
# 导出快照Dump
$ jmap -dump:format=b,file=<FileName> <PID>

Jstack

jstack可以查找死锁

$ jstack <PID>
...
java.lang.Thread.State: BLOCKED
...
Found <amount> Java-level deadlock:
...

Jvisualvm

jvisualvm可视化监管java进程,点击进程dumpjstack一致

Jstat

jstat [-option] [vmid] [gap(ms)] [query_times]可以查看堆内存各部分使用情况,例如查看GC:

$ jstat -gc <PID>

结合不同参数可以具体查看各代的情况。

Linux工具

top

$ top -p <PID>
# H查看所有线程情况,配合jstack找到线程ID,即可找到对应代码

实用脚本

Jstat

#!/bin/bash

# 检查是否提供了PID参数
if [ -z "$1" ]; then
echo "Usage: $0 <PID>"
echo "Example: $0 5527"
exit 1
fi

PID=$1

# 检查进程是否存在
if ! ps -p $PID > /dev/null; then
echo "Error: Process with PID $PID not found"
exit 1
fi

# 执行jstat并格式化输出
jstat -gc $PID | awk '
function progress(pct) {
bars = int(pct/5)
return sprintf("[%-20s]", substr("||||||||||||||||||||", 1, bars))
}
NR==2 {
printf "Eden: %5.1fMB %s %5.1fMB (%d%%)\n", $6/1024, progress(100*$6/$5), $5/1024, 100*$6/$5
printf "Old: %5.1fMB %s %5.1fMB (%d%%)\n", $8/1024, progress(100*$8/$7), $7/1024, 100*$8/$7
printf "Meta: %5.1fMB %s %5.1fMB (%d%%)\n", $10/1024, progress(100*$10/$9), $9/1024, 100*$10/$9
printf "GC Stats: YGC=%d(%.3fs) FGC=%d(%.3fs) Total=%.3fs\n", $13, $14, $15, $16, $19
}'

exit 0

输出

Eden:   32.0MB [|||||||||||||       ]  47.0MB (68%)
Old: 18.8MB [|||||||||||| ] 30.0MB (62%)
Meta: 52.5MB [||||||||||||||||||| ] 52.9MB (99%)
GC Stats: YGC=18(0.166s) FGC=0(0.000s) Total=0.174s

新增配置

sudo vim /etc/nginx/sites-available/yourdomain.conf
# 符号链接
sudo ln -s /etc/nginx/sites-available/yourdomain.conf /etc/nginx/sites-enabled/

port2domain

后端服务

server {
listen 80;
server_name yourdomain.com www.yourdomain.com;

location / {
proxy_pass http://127.0.0.1:9000; # 项目运行的端口
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

# 日志配置(可选)
error_log /var/log/nginx/yourdomain.error.log;
access_log /var/log/nginx/yourdomain.access.log;
}

启用

sudo nginx -t  # 检查配置是否正确
sudo systemctl restart nginx

前端打包文件

location / {
root /var/www/railcloud/dist;
index index.html;
try_files $uri $uri/ /index.html; # 支持SPA路由
}

启用

# ③ 设置正确权限
sudo chown -R www-data:www-data /var/www/railcloud
sudo chmod -R 755 /var/www/railcloud

sudo nginx -t && sudo systemctl reload nginx

基本配置

  1. sudo权限: 修改/etc/sudoers,或者rootadduser <username> sudo并重启
  2. apt软件包管理系统换源:/etc/apt/sources.list修改软件发布源
    deb http://站点/目录名/stretch版本名 main contrib non-free三类软件包
    Debian官方软件源:官网/mirror/list
  3. /usr/share/doc 有安装软件的信息

1.0 命令篇

基本命令

参考资料 https://missing-semester-cn.github.io/2020/course-shell/
  • 关机、重启
    shutdown
    -h now halt,挂起,相当于 halt
    -r now reboot,重启
    poweroff
    reboot

  • 手册
    man 命令

  • 导航
    pwd 显示当前所在目录
    cd 进入文件夹 '..' 上级目录 '.'当前目录 '/'开头的是绝对路径`

  • 查看文件
    ls 列出所有文件
    ls -l 查看文件权限信息

  • 创建文件夹
    mkdir 文件夹名
    rmdir 删除文件夹

  • 没有vim的时候如何创建、编辑、查看文件
    touch 文件 创建文件
    echo 文本 > 文件 echo+重定向输入文件(会把原来内容覆盖)
    echo 文本 >> 文件 追加输入(在原来内容的结尾另起一行输入)
    cat 文件 查看文件
    除了使用cat看文件,还有tac(从最后一行开始显示),more, less(可以翻页,好用)

  • 压缩
    压缩一整个目录,使用 tar
    压缩单个文件 bzip2 gzip(-d解压)
    tar -cvf 目标名 文件名 压缩 , tar -xvf解压

  • 查找
    grep 用法 grep "word" filename

grep "string" * # 在所有文件中搜索string
grep -r "string" # 递归搜索
`find`用法 `find filename`
阅读全文 »

ssh登录

ssh <user_name>@<remote_ip> -p <remote_port> -i <your_key>

ssh端口映射

可以用于不保留端口的情况下,远程连接数据库等。

ssh -N -L <local_port>:localhost:<remote_port> <user_name>@<remote_ip> -p <remote_port> -i <your_key> 

脚本批量映射

需要注意,Nacos有gRPC,除了8848端口外,9848端口也要一起开放。

# Port Mapping
PORTS=(
"ulocalport:localhost:uremoteport"
# MySQL
"53306:localhost:3306"
# Nacos
"58848:localhost:8848"
"59848:localhost:9848"
# Redis
"56379:localhost:6379"
# RocketMQ namesrv
"59876:localhost:9876"
# RocketMQ broker
"510911:localhost:10911"
)

ARGS=()
for port in "${PORTS[@]}"; do
ARGS+=(-L "$port")
done

ssh -o ServerAliveInterval=60 -N "${ARGS[@]}" <username>@<remote_ip>

密钥登录

  1. 首先在本地生成一份密钥,然后将公钥上传到remote的~/.ssh/authorized_keys
  2. 修改remote/etc/ssh/sshd_config
# 新端口
Port 22
# 启用密钥认证
PubkeyAuthentication yes
# 禁用密码登录
PasswordAuthentication no
# 允许Root登录但禁止密码验证
PermitRootLogin prohibit-password
  1. 重启ssh
# Ubuntu/Debian
sudo systemctl restart ssh

# CentOS/RHEL
sudo systemctl restart sshd

文本格式转换

pandoc --from markdown --to docx source.md -o dest.docx
pandoc -f markdown source.md -t docx -o dest.docx
pandoc source.md -o dest.docx --ignore-args # 忽略参数

注意:为了最佳转换效果,markdown文件每行后都要空行

md2epub

# 首先把所有的md文件列出来
## 递归查找所有 .md 文件(排除 README.md 和 SUMMARY.md)
find . -name "*.md" ! -name "README.md" ! -name "SUMMARY.md" | sort > filelist.txt
## 然后编辑 `filelist.txt`,确保文件顺序正确(例如按 `SUMMARY.md` 的目录结构排序)。

pandoc --standalone --toc \
--metadata title="MIT6.824 分布式系统" \
--metadata author="Robert Morris" \
-o output.epub $(cat filelist.txt)

注意:对于gitbook,pandoc可能不能正确处理路径,推荐使用honkit。

honkit

// book.json
{
"title": "MIT6.824 分布式系统",
"author": "Robert Morris",
"plugins": ["hints"],
"pluginsConfig": {
"hints": {
"info": "fa fa-info-circle",
"warning": "fa fa-exclamation-triangle"
}
}
}
# 安装honkit
npm install honkit --save-dev
# 需要calibre转换
ebook-convert --version

npm init -y
npx honkit epub ./ ./mybook.epub

参考教程: https://www.ruanyifeng.com/blog/2019/10/tmux.html
  • tmux使会话与窗口解绑,一个窗口source .bashrc更新了,另一个窗口可能没有
  • ctrl+b 前缀键
    % 分成左右两栏
    " 分成上下两栏
    up 选择上边的窗口
    [ 查看历史记录

窗口

  • <C-d> 删除窗口
  • <C-b> z 最大化/最小化一个窗口
  • <C-b> c 创建一个新的窗口,使用 <C-d>关闭
  • <C-b> N 跳转到第 N 个窗口,注意每个窗口都是有编号的
  • <C-b> p 切换到前一个窗口
  • <C-b> n 切换到下一个窗口
  • <C-b> w 列出当前所有窗口

Tmux-Path 双向绑定

使用tmux会存在一个问题:在1个窗口设置了环境变量,在其他窗口并不会生效。
下面的脚本会帮助我们解决问题,它将tmux白名单内的全局变量值自动同步到Shell环境变量。

阅读全文 »

Shortcuts 快捷键

  • CTRL+N|P 下、上一条命令
  • CTRL+A|E 跳到行首、行尾
  • ALT+F|BCTRL+←|→ 下、上一个单词(右ALT开始使用~)
  • CTRL+W 删除前面的单词
  • CTRL+D|H 删除一个字符(D向后删除=Delete,H向前删除=Backspace)
  • CTRL+U 删除整行
  • CTRL+R 搜索整行,Esc退出

Proxy

export https_proxy=http://host:port;
export http_proxy=http://host:port;
export all_proxy=socks5://host:port;

Bash prompt string配置

使用Windows CMD比使用MinTTY更快!

  • PS(prompt string): 是命令行的默认显示文本,如:
username@hostname /work_directory
$
  • 可以在~/.bash_profile.bashrc中修改默认显示(Windows Git Bash是git-prompt.sh
# Windows Git Bash默认配置,其中\007前面的$TITLEPREFIX和$PWD是标签栏的内容,git_ps1会显示git工作分支
export PS1='\[\e]0;$TITLEPREFIX:$PWD\007\]\n\[\e[32m\]\u@\h \[\e[35m\]$MSYSTEM \[\e[33m\]\w\[\e[36m\]`__git_ps1`\[\e[0m\]\n$ '
# 显示效果
username@hostname MINGW /work_directory
$

# 简洁配置,将\h换成了指定文本,保留了命令行前的换行,将标签名改为当前目录
# 注意,使用单引号才有动态解析效果
export PS1='\[\e]0;\W\007\] \n\[\e[32m\]\u@Host \[\e[33m\]\w\[\e[36m\]`__git_ps1`\[\e[0m\]\n$ '
# 显示效果
username@Host /work_directory
$
  • Windows虚拟环境后不会换行:设置venv/Scripts/activate
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1-}"
PS1="\n(${VIRTUAL_ENV_PROMPT}) ${PS1-}" # 添加换行符
export PS1
fi
  • MSYS2显示git prompt
# etc/profile.d/git-prompt.sh
if test -f /etc/profile.d/git-sdk.sh
then
TITLEPREFIX=SDK-${MSYSTEM#MINGW}
else
TITLEPREFIX=$MSYSTEM
fi

if test -f ~/.config/git/git-prompt.sh
then
. ~/.config/git/git-prompt.sh
else
PS1='\[\033]0;$TITLEPREFIX:$PWD\007\]' # set window title
PS1="$PS1"'\n' # new line
PS1="$PS1"'\[\033[32m\]' # change to green
PS1="$PS1"'\u@\h ' # user@host<space>
PS1="$PS1"'\[\033[35m\]' # change to purple
PS1="$PS1"'$MSYSTEM ' # show MSYSTEM
PS1="$PS1"'\[\033[33m\]' # change to brownish yellow
PS1="$PS1"'\w' # current working directory
if test -z "$WINELOADERNOEXEC"
then
GIT_EXEC_PATH="$(git --exec-path 2>/dev/null)"
COMPLETION_PATH="${GIT_EXEC_PATH%/libexec/git-core}"
COMPLETION_PATH="${COMPLETION_PATH%/lib/git-core}"
COMPLETION_PATH="$COMPLETION_PATH/share/git/completion"
if test -f "$COMPLETION_PATH/git-prompt.sh"
then
. "$COMPLETION_PATH/git-completion.bash"
. "$COMPLETION_PATH/git-prompt.sh"
PS1="$PS1"'\[\033[36m\]' # change color to cyan
PS1="$PS1"'`__git_ps1`' # bash function
fi
fi
PS1="$PS1"'\[\033[0m\]' # change color
PS1="$PS1"'\n' # new line
PS1="$PS1"'$ ' # prompt: always $
fi

MSYS2_PS1="$PS1" # for detection by MSYS2 SDK's bash.basrc

# Evaluate all user-specific Bash completion scripts (if any)
if test -z "$WINELOADERNOEXEC"
then
for c in "$HOME"/bash_completion.d/*.bash
do
# Handle absence of any scripts (or the folder) gracefully
test ! -f "$c" ||
. "$c"
done
fi

# ~/.bashrc || ~/.bash_profile
shopt -q login_shell || . /etc/profile.d/git-prompt.sh
syntax description
\u username
\h hostname
\e set colors
\w work directory
\W current diretory
\n line feed
阅读全文 »

微服务

非单体项目,可以用下面的脚本启动微服务。

#!/bin/bash

# 获取服务名称和额外参数
SERVICE_NAME=$1
shift # 移除第一个参数(服务名),将剩余参数保存到 $@
EXTRA_ARGS="$@"

# 检查是否输入服务名称
if [ -z "$SERVICE_NAME" ]; then
echo "Usage: ./run.sh <servicename|all> [additional_maven_args]"
exit 1
fi

# 定义运行单个服务的函数
run_service() {
local service=$1
local args=$2
echo "Building and running $service with args: $args..."
mvn clean install -pl $service -am
if [ "$service" == "gateway/" ]; then
echo "Gateway starting..."
mvn spring-boot:run -pl $service -Dreactor.netty.http.server.accessLogEnabled=true $args
else
mvn spring-boot:run -pl $service $args
fi
}

# 如果输入 "all",运行所有服务(默认不传参)
if [ "$SERVICE_NAME" == "all" ]; then
echo "Building and running all services..."
mvn clean install -pl "!generator"
for module in $(mvn help:evaluate -Dexpression=project.modules -q -DforceStdout | sed -e 's/<[^>]*>//g' -e 's/\s*//g' | tr ',' '\n'); do
if [ "$module" != "generator" ]; then
echo "Running $module..."
mvn spring-boot:run -pl $module
fi
done
else
# 运行指定的单个服务,并传递额外参数
run_service $SERVICE_NAME "$EXTRA_ARGS"
fi

核心命令是这一条:

mvn spring-boot:run -pl $your_service

想要增加JVM参数,指定端口可以加上

-Dspring-boot.run.arguments=--server.port=$your_port

全局换源

找到 settings.xml 文件:

  • 全局配置:位于 Maven 安装目录的 conf 文件夹下(例如/usr/local/maven/conf/settings.xml)。
  • 用户配置:位于用户主目录下的 .m2 文件夹中(例如~/.m2/settings.xml)。
<settings>
<!-- 其他配置 -->
<mirrors>
<!-- 阿里云 Maven 镜像 -->
<mirror>
<id>aliyun-maven</id>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>central</mirrorOf> <!-- 指定替换中央仓库 -->
</mirror>
<!-- 华为云 Maven 镜像 -->
<mirror>
<id>huaweicloud</id>
<name>华为云 Maven</name>
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
<!-- 其他配置 -->
</settings>

上传文件

scp ./upload/path/file.postfix user@host.com:/path/to/file
scp -P <port> -i <key>
# 文件夹
scp -r ./upload/path/folder user@host.com:/path/to/folder

下载文件

scp user@host.com:/path/to/file.postfix ./download/path
# 文件夹
scp -r user@host.com:/path/to/folader ./download/path

基本操作

  • gg=G:代码格式化
  • GX, GF:打开链接/文件
  • :%y *:复制全部到系统剪贴板
  • <C-o>, <C-i>:回到前一个/后一个位置(例如,打开文件默认在第一行,<C-o>回到上次编辑位置)。注意这个操作是跨文件的
  • `0:返回上次位置
  • 词:w(下一个词),b(词初),e(词尾)
  • 行:0(行初),^(第一个非空格字符),$(行尾)
  • 文件:gg(文件头),G(文件尾)
  • 搜索:/{正则表达式},n/N用于导航匹配
  • x删除字符(等同于dl
  • :s(substitute)替换字符(等同于xi
    • 替换命令:{作用范围}s/{目标文本}/{替换文本}/{替换标志}
    • :%s/s_content/o_content/gc 全局替换,附带确认提示
Sign Range
% 整个文件
. 当前行
$ 最后一行
,n 当前行到n行
n, n行到当前行
+n 当前行后n行
  • 可视化模式 + 操作
    • 选中文字,d删除(剪切) 或者c改变
  • u撤销,<C-r>重做
  • y复制 / “yank” (其他一些命令比如d也会复制)
  • p粘贴
    • +p 粘贴系统剪贴板
  • 更多值得学习的:
    • :<line> 跳到line行,相当于<line>G
    • ~改变字符的大小写
    • 3w向前移动三个词
    • A(大写)可以迅速定位到行尾进行修改
    • :!python prog.py 使用!直接运行shell命令
  • %匹配括号

Change

ce 替换一个单词
cs"' 把当前词块的"全部替换成’

多行操作

<C-v> 选中多行,Shift + i输入后Esc,即可多行同步输入

Esc Map

Vim和NeoVim内置了<C-[>作为<Esc>的映射;
还可以通过<A->Alt加上任何键(Meta键)的方式触发<Esc>-

录制宏

命令模式下,

  1. q<marcoName>进行录制,<marcoName>是宏的名字,例如qa
  2. 执行一系列操作后,再次按q结束录制

使用宏

命令模式下,使用@<marcoName>即可执行;使用5@<marcoName>可以重复执行宏

文件操作

  • :e filename切换到filename文件
  • :bn/bp切换到下/上个文件
  • <C-x><C-f> 自动补全路径
阅读全文 »
0%