洋蔥

耳不闻人是非,目不视人之短,口不言人之过。

[toc]

4. 虚拟机栈

4.1. 虚拟机栈概述

4.1.1. 虚拟机栈出现的背景

由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。

优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令

4.1.2. 初步印象

有不少 Java 开发人员一提到 Java 内存结构,就会非常粗粒度地将 JVM 中的内存区理解为仅有 Java 堆(heap)和 Java 栈(stack)?为什么?

4.1.3. 内存中的栈与堆

栈是运行时的单位,而堆是存储的单位

  • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
  • 堆解决的是数据存储的问题,即数据怎么放,放哪里

image-20200705163928652

4.1.4. 虚拟机栈基本内容

Java 虚拟机栈是什么?

Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用,是线程私有的。

生命周期

生命周期和线程一致

作用

主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于罹序计数器。

JVM 直接对 Java 栈的操作只有两个:

  • 每个方法执行,伴随着进栈(入栈、压栈)
  • 执行结束后的出栈工作

对于栈来说不存在垃圾回收问题(栈存在溢出的情况)

image-20200705165025382

面试题:开发中遇到哪些异常?

栈中可能出现的异常

Java 虚拟机规范允许Java 栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个StackOverflowError 异常。

  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
test();
}
public static void test() {
test();
}
//抛出异常:Exception in thread"main"java.lang.StackoverflowError
//程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。

设置栈内存大小

我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StackDeepTest{
private static int count=0;
public static void recursion(){
count++;
recursion();
}
public static void main(String args[]){
try{
recursion();
} catch (Throwable e){
System.out.println("deep of calling="+count);
e.printstackTrace();
}
}
}

4.2. 栈的存储单位

4.2.1. 栈中存储什么?

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在

在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

4.2.2. 栈运行原理

JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则

在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

image-20200705203142545

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出

1
2
3
4
5
6
7
8
9
public class CurrentFrameTest{
public void methodA(){
system.out.println("当前栈帧对应的方法->methodA");
methodB();
system.out.println("当前栈帧对应的方法->methodA");
}
public void methodB(){
System.out.println("当前栈帧对应的方法->methodB");
}

4.2.3. 栈帧的内部结构

每个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(operand Stack)(或表达式栈)
  • 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
  • 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
  • 一些附加信息

image-20200705204836977

并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的

image-20200705205443993

4.3. 局部变量表(Local Variables)

局部变量表也被称之为局部变量数组或本地变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及 returnAddress 类型。

  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性的 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。

  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

4.3.1. 关于 Slot 的理解

  • 局部变量表,最基本的存储单元是 Slot(变量槽)

  • 参数值的存放总是在局部变量数组的 index0 开始,到数组长度-1 的索引结束。

  • 局部变量表中存放编译期可知的各种基本数据类型(8 种),引用类型(reference),returnAddress 类型的变量。

  • 在局部变量表里,32 位以内的类型只占用一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占用两个 slot。

  • byte、short、char 在存储前被转换为 int,boolean 也被转换为 int,0 表示 false,非 0 表示 true。

  • JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 slot 上

  • 如果需要访问局部变量表中一个 64bit 的局部变量值时,只需要使用前一个索引即可。(比如:访问 long 或 doub1e 类型变量)

  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 this 将会存放在 index 为 0 的 slot 处,其余的参数按照参数表顺序继续排列。

image-20200705212454445

4.3.2. Slot 的重复利用

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SlotTest {
public void localVarl() {
int a = 0;
System.out.println(a);
int b = 0;
}
public void localVar2() {
{
int a = 0;
System.out.println(a);
}
//此时的就会复用a的槽位
int b = 0;
}
}

4.3.3. 静态变量与局部变量的对比

参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。

我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。

和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

1
2
3
4
public void test(){
int i;
System. out. println(i);
}

这样的代码是错误的,没有赋值不能够使用。

4.3.4. 补充说明

在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

4.4. 操作数栈(Operand Stack)

每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)

  • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
  • 比如:执行复制、交换、求和等操作

image-20200706090618332

代码举例

1
2
3
4
5
public void testAddOperation(){
byte i = 15;
int j = 8;
int k = i + j;
}

字节码指令信息

1
2
3
4
5
6
7
8
9
10
11
public void testAddOperation();
Code:
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6:iload_1
7:iload_2
8:iadd
9:istore_3
10:return

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

操作数栈就是 JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的 Code 属性中,为 max_stack 的值。

栈中的任何一个元素都是可以任意的 Java 数据类型

  • 32bit 的类型占用一个栈单位深度
  • 64bit 的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

另外,我们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

4.5. 代码追踪

1
2
3
4
5
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}

使用 javap 命令反编译 class 文件: javap -v 类名.class

1
2
3
4
5
6
7
8
9
10
11
public void testAddoperation();
Code:
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return

image-20200706093131621

image-20200706093251302

image-20200706093646406

image-20200706093751711

image-20200706093859191

image-20200706093921573

image-20200706094046782

image-20200706094109629

程序员面试过程中,常见的 i++和++i 的区别,放到字节码篇章时再介绍。

4.6. 栈顶缓存技术(Top Of Stack Cashing)技术

前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。

由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

4.7. 动态链接(Dynamic Linking)

动态链接、方法返回地址、附加信息 : 有些地方被称为帧数据区

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic 指令

在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 class 文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

image-20200706101251847

为什么需要运行时常量池呢?

常量池的作用:就是为了提供一些符号和常量,便于指令的识别

4.8. 方法的调用:解析与分配

在 JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

4.8.1. 静态链接

当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接

4.8.2. 动态链接

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。

静态链接和动态链接不是名词,而是动词,这是理解的关键。


对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

4.8.3. 早期绑定

早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

4.8.4. 晚期绑定

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。


随着高级语言的横空出世,类似于 Java 一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。

Java 中任何一个普通的方法其实都具备虚函数的特征,它们相当于 C++语言中的虚函数(C++中则需要使用关键字 virtual 来显式定义)。如果在 Java 程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字 final 来标记这个方法。


4.8.5. 虚方法和非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。

静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法。

在类加载的解析阶段就可以进行解析,如下是非虚方法举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Father {

public static void print(String str) {
System.out.println("father " + str);
}

private void show(String str) {
System.out.println("father" + str);
}
}

class Son extends Father {
public class VirtualMethodTest {
public static void main(String[] args) {
Son.print("coder");
// Father fa=new Father();
//fa.show("atguigu.com");
}
}
}

虚拟机中提供了以下几条方法调用指令:

普通调用指令:

  • invokestatic:调用静态方法,解析阶段确定唯一方法版本
  • invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法,虚方法。

动态调用指令:

  • invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic 指令则支持由用户确定方法版本。其中 invokestatic 指令和 invokespecial 指令调用的方法称为非虚方法,其余的(fina1 修饰的除外)称为虚方法。

关于 invokednamic 指令

  • JVM 字节码指令集一直比较稳定,一直到 Java7 中才增加了一个 invokedynamic 指令,这是Java 为了实现「动态类型语言」支持而做的一种改进。

  • 但是在 Java7 中并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。直到 Java8 的 Lambda 表达式的出现,invokedynamic 指令的生成,在 Java 中才有了直接的生成方式。

  • Java7 中增加的动态语言类型支持的本质是对 Java 虚拟机规范的修改,而不是对 Java 语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在 Java 平台的动态语言的编译器。

动态类型语言和静态类型语言

动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。

说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

4.8.6. 方法重写的本质

Java 语言中方法重写的本质:

  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。
  2. 如果在类型 C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常。
  3. 否则,按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出 java.1ang.AbstractMethodsrror 异常。

IllegalAccessError 介绍

程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

4.8.7. 方法的调用:虚方法表

在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM 采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。

每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

虚方法表是什么时候被创建的呢?

虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。

举例 1:

image-20200706144954070

举例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
interface Friendly {
void sayHello();

void sayGoodbye();
}

class Dog {
public void sayHello() {
}

public String tostring() {
return "Dog";
}
}

class Cat implements Friendly {
public void eat() {
}

public void sayHello() {
}

public void sayGoodbye() {
}

protected void finalize() {
}

public String tostring() {
return "Cat";
}
}

class CockerSpaniel extends Dog implements Friendly {
public void sayHello() {
super.sayHello();
}

public void sayGoodbye() {
}
}

image-20210509203351535

4.9. 方法返回地址(return address)

存放调用该方法的 pc 寄存器的值。一个方法的结束,有两种方式:

  • 正常执行完成
  • 出现未处理的异常,非正常退出

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

当一个方法开始执行后,只有两种方式可以退出这个方法:

  1. 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
    • 一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。
    • 在字节码指令中,返回指令包含 ireturn(当返回值是 boolean,byte,char,short 和 int 类型时使用),lreturn(Long 类型),freturn(Float 类型),dreturn(Double 类型),areturn。另外还有一个 return 指令声明为 void 的方法,实例初始化方法,类和接口的初始化方法使用。
  2. 在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口

方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码

1
Exception table:from to target type4	 16	  19   any19	 21	  19   any

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

4.10. 一些附加信息

栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。

4.11. 栈的相关面试题

  • 举例栈溢出的情况?(StackOverflowError)
    • 通过 -Xss 设置栈的大小
  • 调整栈大小,就能保证不出现溢出么?
    • 不能保证不溢出
  • 分配的栈内存越大越好么?
    • 不是,一定时间内降低了 OOM 概率,但是会挤占其它的线程空间,因为整个空间是有限的。
  • 垃圾回收是否涉及到虚拟机栈?
    • 不会
  • 方法中定义的局部变量是否线程安全?
    • 具体问题具体分析。如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
运行时数据区 是否存在 Error 是否存在 GC
程序计数器
虚拟机栈 是(SOE)
本地方法栈
方法区 是(OOM)

[toc]

3. 运行时数据区及程序计数器

3.1. 运行时数据区

3.1.1. 概述

本节主要讲的是运行时数据区,也就是下图这部分,它是在类加载完成后的阶段

image-20200705111640511

当我们通过前面的:类的加载-> 验证 -> 准备 -> 解析 -> 初始化 这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区

image-20200705111843003

内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行 JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。结合 JVM 虚拟机规范,来探讨一下经典的 JVM 内存布局。

image-20210509174724223

我们把大厨后面的东西(切好的菜,刀,调料),比作是运行时数据区。而厨师可以类比于执行引擎,将通过准备的东西进行制作成精美的菜品

image-20210509174543026

我们通过磁盘或者网络 IO 得到的数据,都需要先加载到内存中,然后 CPU 从内存中获取数据进行读取,也就是说内存充当了 CPU 和磁盘之间的桥梁

image-20200705112416101

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

灰色的为单独线程私有的,红色的为多个线程共享的。即:

  • 每个线程:独立包括程序计数器、栈、本地栈。
  • 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)

image-20200705112601211

每个 JVM 只有一个 Runtime 实例。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。

image-20210509173410373

3.1.2. 线程

线程是一个程序里的运行单元。JVM 允许一个应用有多个线程并行的执行。 在 Hotspot JVM 里,每个线程都与操作系统的本地线程直接映射。

当一个 Java 线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java 线程执行终止后,本地线程也会回收。

操作系统负责所有线程的安排调度到任何一个可用的 CPU 上。一旦本地线程初始化成功,它就会调用 Java 线程中的 run()方法。

3.1.3. JVM 系统线程

如果你使用 console 或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[] args)的 main 线程以及所有这个 main 线程自己创建的线程。

这些主要的后台系统线程在 Hotspot JVM 里主要是以下几个:

  • 虚拟机线程:这种线程的操作是需要 JVM 达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要 JVM 达到安全点,这样堆才不会变化。这种线程的执行类型包括”stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
  • 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
  • GC 线程:这种线程对在 JVM 里不同种类的垃圾收集行为提供了支持。
  • 编译线程:这种线程在运行时会将字节码编译成到本地代码。
  • 信号调度线程:这种线程接收信号并发送给 JVM,在它内部通过调用适当的方法进行处理。

3.2. 程序计数器(PC 寄存器)

JVM 中的程序计数寄存器(Program Counter Register)中,Register 的命名源于 CPU 的寄存器,寄存器存储指令相关的现场信息。CPU 只有把数据装载到寄存器才能够运行。这里,并非是广义上所指的物理寄存器,或许将其翻译为 PC 计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟

image-20200705155551919

作用

PC 寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。

image-20200705155728557

它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域

在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致

任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行 native 方法,则是未指定值(undefined)。

它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

它是唯一一个在 Java 虚拟机规范中没有规定任何 OutofMemoryError 情况的区域。

举例说明

1
2
3
4
5
public int minus(){
intc = 3;
intd = 4;
return c - d;
}

字节码文件:

1
2
3
4
5
6
7
8
0: iconst_3
1: istore_1
2: iconst_4
3: istore_2
4: iload_1
5: iload_2
6: isub
7: ireturn

使用 PC 寄存器存储字节码指令地址有什么用呢?为什么使用 PC 寄存器记录当前线程的执行地址呢?

因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。

JVM 的字节码解释器就需要通过改变 PC 寄存器的值来明确下一条应该执行什么样的字节码指令。

image-20200705161409533

PC 寄存器为什么被设定为私有的?

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU 会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个 PC 寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

由于 CPU 时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

CPU 时间片

CPU 时间片即 CPU 分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。

在宏观上:俄们可以同时打开多个应用程序,每个程序并行不悖,同时运行。

但在微观上:由于只有一个 CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

image-20200705161849557

[toc]

2. 类加载子系统

2.1. 内存结构概述

  • Class 文件
  • 类加载子系统
  • 运行时数据区
    • 方法区
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
  • 执行引擎
  • 本地方法接口
  • 本地方法库

image-20200705080719531

image-20200705080911284

如果自己想手写一个 Java 虚拟机的话,主要考虑哪些结构呢?

  • 类加载器
  • 执行引擎

2.2. 类加载器与类的加载过程

类加载器子系统作用

image-20200705081813409

  • 类加载器子系统负责从文件系统或者网络中加载 Class 文件,class 文件在文件开头有特定的文件标识。
  • ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是 Class 文件中常量池部分的内存映射)

类加载器 ClasLoader 角色

image-20200705081913538

  • class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到 JVM 当中来根据这个文件实例化出 n 个一模一样的实例。
  • class file 加载到 JVM 中,被称为 DNA 元数据模板,放在方法区。
  • 在.class 文件->JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器 Class Loader),扮演一个快递员的角色。

类的加载过程

1
2
3
4
5
6
7
8
/**
*示例代码
*/
public class HelloLoader {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

用流程图表示上述示例代码:

image-20200705082255746

加载阶段

image-20200705082601441

    1. 通过一个类的全限定名获取定义此类的二进制字节流
    1. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    1. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

补充:加载 class 文件的方式

  • 本地系统中直接加载
  • 通过网络获取,典型场景:Web Applet
  • 从 zip压缩包中读取,成为日后 jar、war 格式的基础
  • 运行时计算生成,使用最多的是:动态代理技术
  • 由其他文件生成,典型场景:JSP 应用
  • 从专有数据库中提取.class 文件,比较少见
  • 加密文件中获取,典型的防 Class 文件被反编译的保护措施

链接阶段

  • 验证(Verify)
    • 目的在子确保 Class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
    • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
  • 准备(Prepare)
    • 为类变量分配内存并且设置该类变量的默认初始值,即零值。
    • 这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配了,准备阶段会显式初始化;
    • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。
  • 解析(Resolve)
    • 将常量池内的符号引用转换为直接引用的过程。
    • 事实上,解析操作往往会伴随着 JVM 在执行完初始化之后再执行。
    • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java 虚拟机规范》的 Class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
    • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等。

初始化阶段

  • 初始化阶段就是执行类构造器方法<clinit>()的过程。
  • 此方法不需定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • <clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())
  • 若该类具有父类,JVM 会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
  • 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。

2.3. 类加载器分类

JVM 支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器

无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有 3 个,如下所示:

image-20200705094149223

这里的四者之间的关系是包含关系。不是上层下层,也不是子父类的继承关系。

2.3.1. 虚拟机自带的加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用 C/C++语言实现的,嵌套在 JVM 内部。
  • 它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar 或 sun.boot.class.path 路径下的内容),用于提供 JVM 自身需要的类
  • 并不继承自 ava.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类

扩展类加载器(Extension ClassLoader)

  • Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现。
  • 派生于 ClassLoader 类
  • 父类加载器为启动类加载器
  • 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/1ib/ext 子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载。

应用程序类加载器(系统类加载器,AppClassLoader)

  • java 语言编写,由 sun.misc.LaunchersAppClassLoader 实现
  • 派生于 ClassLoader 类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载
  • 通过 ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器

2.3.2. 用户自定义类加载器

在 Java 的日常应用程序开发中,类的加载几乎是由上述 3 种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏

用户自定义类加载器实现步骤:

  1. 开发人员可以通过继承抽象类 ava.lang.ClassLoader 类的方式,实现自己的类加载器,以满足一些特殊的需求
  2. 在 JDK1.2 之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写 loadClass() 方法,从而实现自定义的类加载类,但是在 JDK1.2 之后已不再建议用户去覆盖 loadclass() 方法,而是建议把自定义的类加载逻辑写在 findClass()方法中
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免自己去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

2.4. ClassLoader 的使用说明

ClassLoader 类是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)

image-20200705103516138

sun.misc.Launcher 它是一个 java 虚拟机的入口应用

image-20200705103636003

获取 ClassLoader 的途径

  • 方式一:获取当前 ClassLoader

    1
    clazz.getClassLoader()
  • 方式二:获取当前线程上下文的 ClassLoader

    1
    Thread.currentThread().getContextClassLoader()
  • 方式三:获取系统的 ClassLoader

    1
    ClassLoader.getSystemClassLoader()
  • 方式四:获取调用者的 ClassLoader

    1
    DriverManager.getCallerClassLoader()

2.5. 双亲委派机制

Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象。而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

工作原理

  • 1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  • 2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  • 3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

image-20200705105151258

举例

当我们加载 jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar 是基于 SPI 接口进行实现的,所以在加载的时候,会进行双亲委派,最终从根加载器中加载 SPI 核心类,然后在加载 SPI 接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类 jdbc.jar 的加载。

image-20200705105810107

优势

  • 避免类的重复加载
  • 保护程序安全,防止核心 API 被随意篡改
    • 自定义类:java.lang.String
    • 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang 开头的类)

沙箱安全机制

自定义 String 类,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 jdk 自带的文件(rt.jar 包中 java\lang\String.class),报错信息说没有 main 方法,就是因为加载的是 rt.jar 包中的 string 类。这样可以保证对 java 核心源代码的保护,这就是沙箱安全机制。

2.6. 其他

如何判断两个 class 对象是否相同

在 JVM 中表示两个 class 对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同。

换句话说,在 JVM 中,即使这两个类对象(class 对象)来源同一个 Class 文件,被同一个虚拟机所加载,但只要加载它们的 ClassLoader 实例对象不同,那么这两个类对象也是不相等的。

对类加载器的引用

JVM 必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM 需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用

Java 程序对类的使用方式分为:主动使用和被动使用。

主动使用,又分为七种情况:

  • 创建类的实例

  • 访问某个类或接口的静态变量,或者对该静态变量赋值

  • 调用类的静态方法

  • 反射(比如:Class.forName(”com.atguigu.Test”))

  • 初始化一个类的子类

  • Java 虚拟机启动时被标明为启动类的类

  • JDK 7 开始提供的动态语言支持:

    java.lang.invoke.MethodHandle 实例的解析结果

    REF_getStatic、REF_putStatic、REF_invokeStatic 句柄对应的类没有初始化,则初始化

除了以上七种情况,其他使用 Java 类的方式都被看作是对类的被动使用,都不会导致类的初始化

推荐链接:

菜鸟 Linux 磁盘管理

Linux磁盘管理—-分区格式化挂载fdisk、mkfs、mount

相关命令:

菜鸟 df 命令df (Unix) - 维基百科

菜鸟 du 命令du (Unix) - 维基百科

菜鸟 fdisk 命令

菜鸟 parted 命令GNU Parted - 维基百科

菜鸟 mkfs 命令

菜鸟 mount 命令mount (Unix) - 维基百科 —> 临时挂载

菜鸟 umount命令

菜鸟 fsck 命令fsck - 维基百科

菜鸟 lsblk 命令

菜鸟 partprobe 命令

菜鸟 resize2fs 命令

阅读全文 »

软件包管理

软件包管理系统dpkgRPM包管理员

软件包管理系统前端:APTYUM

软件包及依赖查询网站

Ubuntu

1
2
3
# 查看依赖
apt-cache depends gnome-core # 正向依赖(查看某个包依赖哪些包)
apt-cache rdepends gnome-core # 反向依赖(查看哪些包依赖这个包)

Centos

1
2
3
# 查看依赖
dnf deplist perl # 正向依赖(查看某个包依赖哪些包)
repoquery --whatrequires perl # 反向依赖(查看哪些包依赖这个包,需要安装 dnf install -y yum-utils

dpkg

https://man.archlinux.org/man/dpkg

1
2
dpkg --help
dpkg -L openssh-server # 显示指定软件包在系统中安装的所有文件和目录的完整路径列表。

apt-get、apt-cache、apt

https://man.archlinux.org/man/apt-get

https://man.archlinux.org/man/apt-cache

https://man.archlinux.org/man/apt

简单来说:

  • apt-get:处理包的安装、升级和移除,是传统的、面向脚本的工具。
  • apt-cache:用于查询本地软件包信息(数据库),例如搜索、查看依赖。
  • apt:是较新的前端工具,结合了 apt-getapt-cache 的最佳功能,并提供了更美观、更友好的用户体验。

三个 APT 工具的详细对比

工具 主要功能/定位 典型命令示例 特点总结
apt-get 核心操作:负责从软件源获取和安装软件包,处理实际的系统更改。传统工具,适合脚本编写。 apt-get install <package> apt-get remove <package> apt-get update apt-get upgrade 面向底层:输出信息相对简单,没有进度条。是 APT 工具家族中历史最悠久、功能最稳定的工具。
apt-cache 查询操作:负责查询本地缓存的软件包数据库(Metadata)。它不会更改系统或下载软件包。 apt-cache search <keyword> apt-cache show <package> apt-cache depends <package> 信息获取:仅用于搜索和显示软件包信息、依赖关系等。是用户了解软件包情况的强大工具。
apt 用户界面:结合了 apt-getapt-cache 的常用功能。推荐日常使用的现代工具。 apt install <package> apt remove <package> apt update apt upgrade apt search <keyword> 用户友好:提供美观的进度条、颜色高亮和更简洁的输出。它不是要取代 apt-get,而是提供一个更好的用户界面。

rpm

https://man.archlinux.org/man/rpm

1
rpm -ql openssh-server # 列出软件包中的文件。显示指定软件包在系统中安装的所有文件和目录的完整路径列表。

dnf/yum

https://man.archlinux.org/man/extra/dnf/dnf4

https://man.archlinux.org/man/extra/dnf5/dnf5

dnfyum 的改进和取代者。

1
dnf --version

查看系统安装了哪些软件包

查看软件包依赖了哪些包

查看软件包依赖了哪些动态库

查看软件包安装了哪些文件

查看文件来自哪个软件包

防火墙管理工具

前端管理工具

ufwfirewall-cmd 被称为前端管理工具(Frontend/Wrappers)。它们本身并不具备过滤数据包的能力,而是为了解决 iptablesnftables “语法太复杂、反人类” 的问题而诞生的。当在 ufwfirewall-cmd 中输入一条简单的命令时,它们会在后台自动翻译成一长串复杂的 iptablesnftables 规则并写入系统。

nftables

https://manpages.debian.org/trixie/nftables/nftables.8.en.html

nftables 在 Linux kernel 3.13 中被引入,旨在彻底解决 iptables 的架构缺陷,目前已被大多数现代 Linux 发行版(如 Debian 10+、RHEL 8+、Ubuntu 20.04+)作为默认防火墙。

核心架构:灵活与虚拟机机制

  • 没有预定义结构: 刚启动的 nftables 内部是完全空的。没有默认的表和链。你需要什么(例如只过滤本机 INPUT),就只创建对应的表和链。这极大地减少了内核的无用性能消耗。
  • 内核伪虚拟机(Pseudo-VM): 它的规则在用户空间被编译成类似字节码的指令,然后送入内核执行,这使得内核代码变得非常精简。
  • 集合(Sets)与映射(Maps): 这是 nftables 性能碾压 iptables 的关键。它可以将多个 IP 或端口放进一个“集合”或“字典”中进行 Hash 查找(O(1) 复杂度),无论你封禁了 10 个 IP 还是 10 万个 IP,匹配速度几乎一样快。

nftables 的主要优势

  1. 大一统: 一个 nft 命令行工具同时管理 IPv4, IPv6, ARP, 和 Bridge 规则。
  2. 原子更新: 增删规则是增量式的,不需要重启整个防火墙状态,瞬间完成。
  3. 语法极其友好: 抛弃了 iptables 繁琐的 -m-p 等参数,采用了类似 tcpdump 的可读性极强的语法。

iptables(过时)

https://man7.org/linux/man-pages/man8/iptables.8.html

iptables 自 Linux kernel 2.4 引入以来,统治了 Linux 防火墙领域二十多年。它通过一套固定的结构来处理网络流量。

核心架构:“四表五链”

iptables 的设计基于预定义的表(Tables)和链(Chains):

  • 表(Tables): 决定了规则的功能分类。
    • filter:默认表,用于决定数据包是否允许通过(防火墙核心)。
    • nat:用于网络地址转换(端口转发、路由伪装等)。
    • mangle:用于修改数据包的 IP 头信息(如 TTL、TOS)。
    • raw:用于绕过连接跟踪(Connection Tracking)。
  • 链(Chains): 代表数据包在内核中流经的具体位置。
    • INPUT:发往本机的数据包。
    • OUTPUT:本机发出的数据包。
    • FORWARD:经由本机转发的数据包(路由)。
    • PREROUTING:路由判断前。
    • POSTROUTING:路由判断后。

iptables 的主要痛点

  1. 性能瓶颈(线性匹配): 当有上万条规则时,iptables 会从第一条开始一条条向下匹配(O(n) 复杂度),规则越多,网络延迟越高。
  2. 工具碎片化: IPv4 用 iptables,IPv6 用 ip6tables,ARP 防火墙用 arptables,网桥用 ebtables。管理员需要学习和维护四套不同的工具。
  3. 规则更新效率低: 每次添加或删除一条规则时,iptables 需要将整个规则集从内核拉到用户空间,修改后再整个推回内核。这在规则极其庞大时会造成系统卡顿。

端口映射 和 端口转发

它们的维基百科是一样的:https://zh.wikipedia.org/wiki/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91

端口映射:“大楼的索引名录”(静态、直达),就像大楼门口贴着的固定告示牌,写着:“3楼 301室 = 财务部”

  • 动作: 外部人员进门一抬头,看到索引,就知道去 301 室找财务。
  • 特点:
    • 固定的: 关系是写死的,你来或不来,财务部就在那里。
    • 全局透明: 所有人只要看名录,都能直接找到对应房间。
    • 典型场景: 在路由器里设死,只要访问 8080 端口,就直接对应到家里的电脑。

端口转发:—— “前台的转办流程”(动态、中转),就像你走到前台,前台工作人员说:“你要找财务?先把文件给我,我帮你转交给里间的财务主管。”

  • 动作: 外部人员并不直接接触财务室,而是通过“前台”这个中间人进行数据包的拆解、重定向和再传递。
  • 特点:
    • 过程的: 强调的是“转交”这个动作,甚至前台可以临时决定把文件转给 3 楼还是 4 楼。
    • 隐藏性: 外部人员只跟前台打交道,不知道内部具体的房间号。
    • 典型场景: 你通过 SSH 隧道把原本访问本地的流量,偷偷“转发”到远端的服务器上。

总结:

端口映射静态的地址映射:强调的是“地址 A 就是地址 B”,是一种身份关联。

端口转发动态的流量重定向:强调的是“把 A 的东西挪给 B”,是一个搬运过程。

维度 端口映射 (Port Mapping) 端口转发 (Port Forwarding)
关注点 建立对应关系 (公网 IP:端口 <=> 内网 IP:端口) 流量传递 (拦截流量并改变其目的地)
发生位置 通常在网络边界(路由器、网关、防火墙) 可以发生在任何节点(网关、操作系统本身、应用软件)
底层技术 主要是 NAT(网络地址转换) NAT、路由规则、SSH 隧道、代理软件(如 Nginx、HAProxy)
是否同机 必定涉及跨设备(网关到内网机器) 可以是同机(本机的 A 端口转 B 端口),也可以跨设备

命令介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# REDIRECT 是 DNAT 的一种特例,用于将流量“重定向”到本机的某个端口。它会把数据包的 目标 IP 改为本机 IP,并可选地修改目标端口。方向:外部 → 本机。常用场景:本机透明代理、流量重定向/端口转发、端口映射、本机端口劫持。
# DNAT 改变数据包的目标地址和目标端口,使外部访问被转发到内网主机。方向:外部 → 内部。常见场景:外部访问内网服务、公网访问转发到私网服务器。
# SNAT(Source NAT)用于修改数据包的源IP(和可选的源端口),使外部服务器认为请求来自指定的公网IP。方向:内部 → 外部。常见场景:内网主机访问外网时共享一个公网IP、多网卡服务器的流量伪装、NAT 路由器出站流量转换。

# -t, --table 此选项指定命令应操作的数据包匹配表。部分表如下:
# filter: 这是默认表(如果没有传递 -t 选项)。
# INPUT
# FORWARD(用于通过设备路由的数据包):转发关卡,触发时机:内核通过“路由决策”发现,这个数据包的目的地 IP 不是本机,而是另外一台电脑,且本机开启了转发功能(ip_forward)。
# OUTPUT
# nat: 当遇到创建新连接的数据包时,会参考此表。它包含四个内置函数:
# PREROUTING(用于在数据包到达时立即对其进行修改):路由前关卡,触发时机:外部数据包刚刚到达网卡,内核甚至还不知道这个包是要发给谁的(还没开始看路由表)。
# INPUT(用于发往本地套接字的数据包):入站关卡,触发时机:内核通过“路由决策”发现,这个数据包的目的地 IP 就是本机自己。
# OUTPUT(用于在路由前修改本地生成的数据包):出站关卡,触发时机:本机自己运行的程序想要向外发送数据包。
# POSTROUTING(用于在数据包即将发出时对其进行修改):路由后关卡,触发时机:无论是本机发出的包(来自 OUTPUT),还是帮别人转发的包(来自 FORWARD),在即将真正飞出网卡、进入物理网线的一瞬间触发。
# -L, --list 列出指定链的所有规则
# -A, --append 追加规则到指定链的末尾
# -i 代表输入网卡接口名称。
# -o 代表输出网卡接口名称。lo 代表 Loopback(回环网卡,即本机本地网络)。
# -p, --protocol 要检查的规则或数据包的协议。指定的协议可以是 tcp、udp、udplite、icmp、icmpv6、esp、ah、sctp、mh 之一,也可以是特殊关键字“all”,或者是一个数值,代表这些协议之一或其他协议。也允许使用 /etc/protocols 中的协议名称。协议前的“!”参数会反转测试结果。数字 0 等同于 all。“all”将匹配所有协议,并且在省略此选项时用作默认值。
# -j, --jump 此选项指定规则的目标;即,如果数据包匹配该规则,则执行什么操作。
# -n 以数字方式显示 IP 和端口,不反查域名或服务名(更快更准确)
# --line-numbers 给每条规则显示一个编号,便于删除或修改特定规则

# 若忽略出站入栈网卡,则作用于所有网卡流量
iptables -t nat -A OUTPUT -o lo -p tcp --dport 8022 -j REDIRECT --to-port 22 # 端口转发,本机访问本机时触发
iptables -t nat -A PREROUTING -i ens33 -p tcp --dport 8022 -j REDIRECT --to-port 22 # 端口转发,外部访问本机时触发
iptables -t nat -A PREROUTING -i ens33 -p tcp --dport 8022 -j DNAT --to-destination 192.168.0.7:22 # 端口映射
iptables -t nat -A POSTROUTING -o ens33 -s 192.168.0.0/24 -j SNAT --to-source 114.114.114.114 # 内网 192.168.0.0/24 子网的主机访问外网时,源地址被替换为公网IP 114.114.114.114。

# 查看 NAT 表 (nat table) 中 PREROUTING 链 的所有规则,并以数字形式显示(不反查 DNS / 服务名)。
iptables -t nat -L PREROUTING -n --line-numbers
iptables -t nat -L OUTPUT -n --line-numbers

iptables -t nat -D PREROUTING 1 # 根据行号删除规则
iptables -t nat -D PREROUTING -i ens33 -p tcp --dport 8022 -j REDIRECT --to-port 22 # 按规则内容删除规则

开机执行

vim /data/local/bin/port-forward.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

# 外部访问本机时端口转发。先检查规则是否存在,若不存在则追加规则
iptables -t nat -C PREROUTING -p tcp --dport 8022 -j REDIRECT --to-port 22 2>/dev/null || \
iptables -t nat -A PREROUTING -p tcp --dport 8022 -j REDIRECT --to-port 22
# 本机访问本机时端口转发。先检查规则是否存在,若不存在则追加规则
iptables -t nat -C OUTPUT -o lo -p tcp --dport 8022 -j REDIRECT --to-port 22 2>/dev/null || \
iptables -t nat -A OUTPUT -o lo -p tcp --dport 8022 -j REDIRECT --to-port 22

iptables -t nat -C PREROUTING -p tcp --dport 8306 -j REDIRECT --to-port 3306 2>/dev/null || \
iptables -t nat -A PREROUTING -p tcp --dport 8306 -j REDIRECT --to-port 3306
iptables -t nat -C OUTPUT -o lo -p tcp --dport 8306 -j REDIRECT --to-port 3306 2>/dev/null || \
iptables -t nat -A OUTPUT -o lo -p tcp --dport 8306 -j REDIRECT --to-port 3306

iptables -t nat -C PREROUTING -p tcp --dport 8379 -j REDIRECT --to-port 6379 2>/dev/null || \
iptables -t nat -A PREROUTING -p tcp --dport 8379 -j REDIRECT --to-port 6379
iptables -t nat -C OUTPUT -o lo -p tcp --dport 8379 -j REDIRECT --to-port 6379 2>/dev/null || \
iptables -t nat -A OUTPUT -o lo -p tcp --dport 8379 -j REDIRECT --to-port 6379

iptables -t nat -C PREROUTING -p tcp --dport 8092 -j REDIRECT --to-port 9092 2>/dev/null || \
iptables -t nat -A PREROUTING -p tcp --dport 8092 -j REDIRECT --to-port 9092
iptables -t nat -C OUTPUT -o lo -p tcp --dport 8092 -j REDIRECT --to-port 9092 2>/dev/null || \
iptables -t nat -A OUTPUT -o lo -p tcp --dport 8092 -j REDIRECT --to-port 9092

向 crontab 添加简单定时任务,crontab -e

1
2
# 开机/重启后运行一次
@reboot /data/local/workspaces/mars-python/bin/port_forward.sh

核心差异对比图

特性 iptables nftables
命令行工具 多个 (iptables, ip6tables, arptables, ebtables) 单一工具:nft
表与链 内核硬编码,预先存在,无法删除 完全由用户自定义,按需创建
规则匹配机制 线性匹配(规则越多越慢) 支持高级数据结构(Sets/Maps),极速匹配
规则加载方式 每次拉取/推送整个规则集 原子化增量更新,不影响其他流量
语法风格 参数化、冗长(大量使用 -A, -m, -p 类似脚本语言,直观、结构化、支持嵌套
多重匹配 每次匹配都需要单独写一条规则 可以通过 {} 将多个端口/IP合并在一条规则中

流编辑器——sed

Sed 是一款非常强大且常用的 流编辑器(Stream Editor),非常适合做批量的替换、删除、插入、打印等文本变换。

简介与工作原理

  • 流编辑器: Sed 一次只处理一行内容。
  • 工作空间: Sed 处理文件时,会将当前处理的行读入一个临时缓冲区,称为 “模式空间” (pattern space)
  • 处理流程: Sed 命令会对模式空间中的内容进行处理,处理完成后,默认会将模式空间的内容输出到标准输出(屏幕)。接着清空模式空间,读取下一行,重复上述过程,直到文件末尾。
  • 非破坏性: 默认情况下,Sed 不会修改原文件内容,而是将结果输出到标准输出,除非使用重定向或特定的选项(如 -i)来保存更改。

sed 常用语法格式

最基本的 Sed 命令格式如下:

1
2
3
sed [选项] 'sed命令' 文件名
#
sed [选项] -f 脚本文件 文件名

常用选项 (Options)

选项 作用
-n 静默模式(或安静模式)。默认 Sed 会打印所有行,使用此选项后,只有被显式命令(如 p 打印命令)处理的行才会被打印。
-e 允许多个 Sed 命令/脚本。例如:sed -e 'command1' -e 'command2' file
-f 指定 Sed 脚本文件。例如:sed -f script.sed file
-i 直接修改原文件。这个选项非常重要,但使用时要小心,最好先备份文件。

地址(Address)

Sed 命令可以指定一个地址或一个地址范围,以限制命令的作用范围。

地址格式 作用 示例
无地址 作用于文件中的所有行 's/old/new/'
单行号 作用于指定的行 '5d' (删除第 5 行)
$ 代表文件的最后一行 '$d' (删除最后一行)
/regex/ 作用于匹配正则表达式的行。这是查找命令 '/error/d' (删除包含 “error” 的行)
地址范围 作用于从起始地址到结束地址之间的行(包含边界)。
addr1,addr2 1,5d (删除第 1 到 5 行)
addr1,/regex/ 3,/end/d (删除第 3 行到第一个匹配 /end/ 的行)
/regex1/,/regex2/ /start/,/end/d (删除第一个匹配 /start/ 到第一个匹配 /end/ 的行)

sed 常用命令 (Commands)

sed 命令通常紧跟在地址后面。

替换:s (substitute)

这是 sed 最常用、最强大的功能。

  • 格式: [地址]s/旧字符串/新字符串/标志
  • 示例:
    • s/foo/bar/:将每行中第一个 foo 替换为 bar
    • s/foo/bar/g全局替换,将行中所有 foo 替换为 bar
    • s/foo/bar/p:打印发生替换的行。通常配合 -n 选项使用。
    • s/foo/bar/w output.txt:将发生替换的行写入 output.txt 文件。
    • s#/#\\/#g:当替换内容或目标字符串中包含 / 时,可以使用其他分隔符,例如 #详见下文 sed 命令分隔符

删除:d (delete)

  • 格式: [地址]d
  • 示例:
    • 3d:删除第 3 行。
    • 1,5d:删除第 1 到 5 行。
    • /^#/d:删除以 # 开头的行 (常用于删除注释行)。
    • '/^$/d':删除所有空行。

打印:p (print)

  • 格式: [地址]p
  • 用途: 配合 -n 选项,只打印指定的行或匹配的行。
  • 示例:
    • sed -n '5p' file:只打印第 5 行。
    • sed -n '/config/p' file:只打印包含 config 关键字的行。
    • sed -n '1,10p' file:只打印前 10 行。

追加/插入/更改:a / i / c

  • a (append): 在匹配行之后追加新文本。
    • 示例: '/error/a\--- An Error Occurred ---' (在包含 error 的行后面追加文本)
  • i (insert): 在匹配行之前插入新文本。
    • 示例: '1i\# This is a header' (在文件第一行前插入一行)
  • c (change): 用新文本替换匹配行或匹配范围的所有内容。
    • 示例: '3c\Replacement Text' (用新文本替换第 3 行)

注意: aic 命令后的文本必须换行书写(在命令行中通常用 \ 来转义换行符,或者直接跟在命令后,用 \ 隔开)。

sed 命令分隔符

分隔符(delimiter)在 sed 命令中主要用于 替换命令查找命令 / ,尤其是涉及到正则表达式的部分。默认是 /

替换命令 (s)

这是最常见的使用自定义分隔符的场景。

  • 基本格式: s/查找模式/替换字符串/标志
  • 使用自定义分隔符的格式: sX查找模式X替换字符串X标志
    • 这里的 X 可以是任何非空格、非换行符的单个字符。
示例命令 分隔符 场景说明
s#/var/log#/tmp/log#g # 当查找或替换的内容中包含 / 时(例如文件路径),使用 # 可以避免对 / 进行转义。
s@^#@ @ @ 当查找或替换的内容中包含 # 时(例如配置文件中的注释),可以使用 @

查找/地址命令 (/regex/)

分隔符也可以用于定义地址(即指定 Sed 命令作用的行范围)。当您需要查找的模式中包含默认分隔符 / 时,可以自定义分隔符。

  • 基本格式: /正则表达式/命令
  • 使用自定义分隔符的格式: \X正则表达式X命令
    • 注意: 在自定义分隔符前需要加上反斜杠 \
示例命令 分隔符 场景说明
/\//d / (默认) 删除包含默认分隔符 / 的行,但需要转义 (\/)。
sed '\#/home/#d' file # (自定义) 删除包含 /home/ 路径的行,无需转义路径中的 /,但需在开头用 \ 声明自定义分隔符。

进阶示例

组合多个命令

使用 -e 选项或分号 ;

1
2
3
4
# 删除空行,并将所有的 'old' 替换为 'new'
sed -e '/^$/d' -e 's/old/new/g' file.txt
# 或者使用分号
sed '/^$/d ; s/old/new/g' file.txt

括号分组和后向引用

在替换命令 s 中,可以使用括号 () 对匹配模式的一部分进行分组,并在替换字符串中用 \1, \2, … 进行引用。注意: 括号需要用 \ 进行转义,即 \(\)

  • 示例: 交换行中两个单词的位置。
1
2
3
# 假设一行内容是 "apple banana"
# 将其转换为 "banana apple"
echo "apple banana" | sed 's/\(apple\) \(banana\)/\2 \1/'
  • \(apple\) 匹配并捕获第一个单词,标记为 \1
  • \(banana\) 匹配并捕获第二个单词,标记为 \2
  • 替换字符串 \2 \1 将它们的位置交换。

文件定位

whereis命令locate/slocate命令find命令which命令rpm命令

以 nginx 安装目录为例:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# whereis nginx
[root@localhost ~]# whereis -b nginx # 定位指令的二进制程序、源代码文件和man手册页等相关文件的路径
[root@localhost ~]# locate nginx # 搜索一个数据库/var/lib/locatedb,查找目录与文件,但查不到最新的变动,为避免这种情况应先使用updatedb命令
[root@localhost ~]# find / | grep nginx # 在指定目录下查找子目录与文件

[root@localhost ~]# which nginx # 查找并显示给定命令(可执行程序)的绝对路径。查看某个系统命令是否存在并显示命令位置。

# rpm命令是RPM软件包的管理工具
[root@localhost ~]# rpm -qa | grep nginx # 列出安装过的软件包,且包含字符串nginx
[root@localhost ~]# rpm -q nginx # 获取nginx软件包的全名
[root@localhost ~]# rpm -ql nginx # rpm包中文件的安装位置
[root@localhost ~]# rpm -qal | grep nginx

网络状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yum install -y lsof
lsof -i:8080 # 查看端口是否被占用,有列头,推荐使用

yum install -y net-tools
netstat -ntlp # 查看当前所有tcp端口
netstat -nulp # 查看当前所有udp端口
netstat -an | grep 80 # 查看所有包含80的端口使用情况
netstat -ntulp | grep 80 # 查看所有包含80的端口使用情况,包括PID(进程ID)、进程名。
netstat -tunlp | grep 80 # 查看所有包含80的端口使用情况,包括PID(进程ID)、进程名。
# -t : 指明显示TCP端口
# -u : 指明显示UDP端口
# -l : 仅显示监听套接字(所谓套接字就是使应用程序能够读写与收发通讯协议(protocol)与资料的程序)
# -p : 显示进程标识符和程序名称,每一个套接字/端口都属于一个程序。
# -n : 不进行DNS轮询,显示IP(可以加速操作)

进程状态

Linux ps 命令 - 菜鸟

1
2
3
4
5
6
7
8
[root@localhost ~]# ps aux
[root@localhost ~]# ps aux | grep nginx

[root@localhost ~]# ps -f
[root@localhost ~]# ps -ef
[root@localhost ~]# ps -ef | grep nginx

ps axw -o pid,ppid,user,%cpu,vsz,wchan,command | egrep '(nginx|PID)'

输出头(标题)

Linux head 命令 - 菜鸟

在使用Linux命令时,如果命令中有管道 | ,则输出的信息中,头(标题)信息丢失,要想看每一列代表什么意思很不方便。

例如 ps auxw

1
2
3
4
5
6
7
8
9
10
11
$ ps axuw
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.3 193984 6556 ? Ss 08:51 0:03 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
root 2 0.0 0.0 0 0 ? S 08:51 0:00 [kthreadd]
root 4 0.0 0.0 0 0 ? S< 08:51 0:00 [kworker/0:0H]
root 5 0.0 0.0 0 0 ? S 08:51 0:00 [kworker/u128:0]
root 6 0.0 0.0 0 0 ? S 08:51 0:00 [ksoftirqd/0]
root 7 0.0 0.0 0 0 ? S 08:51 0:01 [migration/0]
root 8 0.0 0.0 0 0 ? S 08:51 0:00 [rcu_bh]
root 9 0.0 0.0 0 0 ? S 08:51 0:03 [rcu_sched]
root 10 0.0 0.0 0 0 ? S< 08:51 0:00 [lru-add-drain]

再加上管道符后

1
2
3
4
5
$ ps axuw | grep redis
esuser 2636 0.0 0.0 192 4 ? Ss 08:52 0:00 /usr/bin/dumb-init -- /redis-commander/docker/entrypoint.sh
polkitd 2654 0.2 0.4 52956 9020 ? Ssl 08:52 0:09 redis-server *:6379
esuser 2810 0.0 1.9 272292 36548 ? Ssl 08:52 0:01 /usr/bin/node ./bin/redis-commander
root 39925 0.0 0.0 112824 980 pts/0 S+ 10:05 0:00 grep --color=auto redis

可以看到头(标题)已经丢失。

一个简单的办法,通过 2 条命令叠加,获取头和内容:

1
2
3
4
5
6
$ ps axuw | head -1;ps axuw | grep redis
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
esuser 2636 0.0 0.0 192 4 ? Ss 08:52 0:00 /usr/bin/dumb-init -- /redis-commander/docker/entrypoint.sh
polkitd 2654 0.2 0.4 52956 9020 ? Ssl 08:52 0:09 redis-server *:6379
esuser 2810 0.0 1.9 272292 36548 ? Ssl 08:52 0:01 /usr/bin/node ./bin/redis-commander
root 40905 0.0 0.0 112824 984 pts/0 S+ 10:07 0:00 grep --color=auto redis

也就是先用命令本身加 | head -1 取到头(标题),然后再使用该命令输出内容,两者叠加输出即得到所要结果。

排序——sort

Linux sort命令 - 菜鸟

排序命令——sort

按列排序,数字大的在前:

1
2
3
4
5
6
7
8
$ ps auxw | sort -rnk 2
root 44271 0.0 0.0 126840 928 pts/0 S+ 10:14 0:00 sort -rnk 2
root 44270 0.0 0.0 155448 1868 pts/0 R+ 10:14 0:00 ps auxw
root 44269 0.0 0.0 108052 612 ? S 10:14 0:00 sleep 1
201 44263 0.0 0.0 1560 248 ? SN 10:14 0:00 sleep 1
201 33288 0.0 0.0 2412 1400 ? SN 09:52 0:01 bash /usr/libexec/netdata/plugins.d/tc-qos-helper.sh 1
root 8204 0.0 0.0 0 0 ? S< 09:02 0:00 [kworker/11:1H]
......

该例子,将第 2 列进行排序,最大的数排前面。

若只想看前10条的内容:

1
$ ps auxw | sort -rnk 2 | head -10

将实际内存消耗最大的10个进程显示出来:

1
2
$ ps auxw | head -1; ps auxw | sort -rnk 6 | head -10
$ ps auxw --sort=-rss | head -11

统计——wc

统计命令——wc

切分——cut

切分命令——cut

去重——uniq

去重命令——uniq

强大的文本分析命令——awk

awk 命令 - 菜鸟
强大的文本分析命令——awk

1
ps -ef | grep mar-service.jar:60002 | grep -v grep | awk '{ print }'

服务管理

systemctl命令 :是系统服务管理器指令,它实际上将 servicechkconfig 这两个命令组合到一起。

1
2
3
4
5
6
7
8
9
10
11
# 查看所有可用的单元文件
[root@localhost ~]# systemctl list-unit-files | grep ''
# 查看所有已安装服务
[root@localhost ~]# systemctl list-unit-files --type=service | grep ''

# 输出激活的unit,下面两个命令等效
[root@localhost ~]# systemctl | grep ''
[root@localhost ~]# systemctl list-units | grep ''
# 输出激活的类型为service的unit
[root@localhost ~]# systemctl list-units --type=service | grep ''
[root@localhost ~]# systemctl list-units --type=service | grep nginx

vim 中查找和替换

vi / vim 键位图: https://www.runoob.com/linux/linux-vim.html

https://harttle.land/2016/08/08/vim-search-in-file.html

https://blog.csdn.net/ballack_linux/article/details/53187283

清空历史命令

history命令

.bash_history 默认可记录 500 条历史命令。

只有在正常退出当前 shell 时,在当前 shell 中运行的命令才会保存至 .bash_history 文件中。

1
2
3
[root@localhost ~]# history 10               # 显示最近使用的10条历史命令
[root@localhost ~]# history -c # 清空当前shell历史命令记录
[root@localhost ~]# rm -rf ~/.bash_history # 删除'.bash_history'文件

若想在每次登录后都清空历史记录,可以在登录后登出前执行 rm -rf ~/.bash_history 即可。

Wget 和 cURL

百科: Wgetwget命令cURLcurl命令

curl 与 wget

wget命令 用来从指定的URL下载文件。

curl命令 是一个利用URL规则在命令行下工作的文件传输工具。curl URL 默认将下载文件输出到stdout,将进度信息输出到stderr(默认也是终端),可以使用 -O(使用原文件名)或 -o(指定输出文件名)指定输出位置。curl支持更多的协议,还支持cookies、认证、限速、文件大小等特征。

1
2
[root@localhost ~]# wget -c URL
[root@localhost ~]# curl URL -O --progress

删除文件/文件夹

1
2
3
4
rm -rf # 文件或文件夹名
# -i 删除前逐一询问确认。
# -f 即使原档案属性设为唯读,亦直接删除,无需逐一确认。
# -r 将目录及以下之档案亦逐一删除。

解压缩

tar

1
2
3
# 解压
tar -zxvf jdk-7u80-linux-i586.tar.gz
tar -zxvf /mnt/hgfs/SharedFolders/openjdk-11+28_linux-x64_bin.tar.gz -C /data/

zip

1
2
3
4
5
6
7
8
9
10
11
12
# 安装zip、unzip应用
yum install zip unzip # Centos
apt install zip unzip # Ubuntu

# 解压
unzip /mnt/hgfs/SharedFolders/linuxx64_12201_database.zip -d /data
# 将 /home/html/ 这个目录下所有文件和文件夹打包为当前目录下的 html.zip
zip -q -r html.zip /home/html
# 如果在我们在 /home/html 目录下,可以执行该命令
zip -q -r html.zip *
# 从压缩文件 cp.zip 中删除文件 a.c
zip -dv cp.zip a.c

7z

Linux有问必答:Linux 中如何安装 7zip

支持 7Z,ZIP,Zip64,TAR,RAR,CAB,ARJ,GZIP,BZIP2,CPIO,RPM,ISO,DEB 压缩文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装
$ yum install p7zip p7zip-plugins
$ apt install p7zip-full p7zip-rar

# 将01.jpg和02.png压缩成一个7z包
$ 7z a pkg.7z 01.jpg 02.png
# 将所有的.jpg文件压缩成一个7z包
$ 7z a pkg.7z *.jpg
# 将文件夹folder压缩成一个7z包
$ 7z a pkg.7z folder
# 将pkg.7z中的所有文件解压出来,e是解压到当前路径
$ 7z e pkg.7z # 不实用
# 将pkg.7z中的所有文件解压出来,x是解压到压缩包命名的目录下
$ 7z x pkg.7z # 正确的解压方法

nohup

相关链接: 百科Linux nohup 命令 - 菜鸟shell中的特殊字符大全

nohup命令可以将程序以忽略挂起信号的方式运行起来,被运行的程序的输出信息将不会显示到终端。 如果不将 nohup 命令的输出重定向,输出将追加到当前目录的 nohup.out 文件中。

nohup command >/dev/null 2>&1 & 意思就是,将command保持在后台运行,并且将输出的日志忽略。

1
2
# 启动java服务
nohup java -jar -Dserver.port=9088 gateway.jar >/dev/null 2>&1 &

备份脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash

# 日志文件的存放路径。$0 代表当前脚本的路径,dirname "$0" 会提取出脚本所在的目录。这意味着该日志文件将始终生成在与该脚本相同的目录下。
LOG_FILE=$(dirname "$0")/backup.log

# 日志记录函数。$1 代表传入的参数。tee指令会从标准输入读取数据,将其内容传输到标准输出(屏幕),同时保存成文件,-a (append):追加到现有文件的末尾,而不是覆盖它。
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 核心备份函数
backup() {
local src="$1"
local dest="$2"
log "Starting backup: $src -> $dest"

if rsync -avz --progress --exclude='*.log' "$src" "$dest"; then
# 如果 rsync 成功执行(返回码为 0),则记录成功日志。
log "Backup succeeded: $src -> $dest"
else
# 如果失败,记录错误日志,并执行 exit 1。这非常重要,意味着只要有一个目录备份失败,整个脚本就会立刻终止,不会继续尝试备份后面的目录。
log "ERROR: Backup failed: $src -> $dest"
exit 1
fi
}

# 备份的目标地址,需要提前配置SSH信任
TARGET="192.168.0.79:/vg/backup/cleansource"

# --------------------------- 执行备份任务

# 备份SCA Web
backup "/ssd1" "$TARGET"
# 备份Minio
backup "/vg/cleansource/minio" "$TARGET"
# 备份知识库
backup "/vg/cleansource/kb" "$TARGET"

# 备份数据库正确的做法是:1、先使用数据库自带的导出工具(如 mysqldump 或 pg_dump)生成一个逻辑备份文件;2、然后再用 rsync 同步备份文件。

log "All backups completed successfully."
0%