开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

详解,NIO中的通道(Channel) 一、什么是通道 二、

发表于 2021-11-15
  • J3 - 白起
  • 技术(NIO # 通道 # Channel)

这是 IO 相关的第三篇通道,主要讲解一下通道是什么,在 Java NIO 中的体系及使用。能被称为 NIO 中的三大组件之一作用肯定是不言而喻的,所以对于通道的掌握还是很重要的,那我们往下看把!

以下内容“通道”一词就是“Channel”,只是我更喜欢用中文表示而已,仅我个人喜好,并无它意。

一、什么是通道

百度词贴:

Snipaste_2021-11-14_17-22-59.png
从百度解释来看:通道主要用来传输数据的一条道路。

==而在 NIO 中,通道的作用也是如此:传输数据,将“原缓冲区”与“目标缓冲区”要交换的数据进行传输==。

很明显通道是作用与缓冲区的,所以读了上篇本人写得《详解,NIO中的缓冲区》那么我们得出下面这张图:

Snipaste_2021-11-14_17-31-22.png

二、NIO中的通道体系

在 IDEA 中我观察 Channel 的类继承关系时,发现好复杂呀,我点进源码看它的继承接口和实现类,发现超复杂,最后还是放弃通过 IDEA 看了。

所以我去看了 JDK8 的 API 文档找出了 Channel 的相关信息,如下。

1、父接口

  • AutoCloseable:自动关闭流,而不需要显式地调用 close ()方法。
  • Closeable:关闭 IO 流,释放系统资源。

2、直接子接口

  • AsynchronousByteChannel:支持异步 IO 操作,单位时字节。
  • AsynchronousChannel:支持异步 IO 操作。
  • ByteChannel:继承 ReadableByteChannel 和 WritableByteChannel 接口允许对baty进行读写操作。
  • GatheringByteChannel:使接口可以将多个缓冲区中的数据写入通道。
  • InterruptibleChannel:使通道能以异步的方式进行关闭与中断。
  • MulticastChannel:使通道支持一个多播的功能,可以理解同时向多个主机发送数据。
  • NetworkChannel:主要作用是使通道与 Socket 进行关联,是通道中的数据能在 Socket 技术上进行传输。
  • ReadableByteChannel:是通道允许对字节进行读操作。
  • ScatteringByteChannel:主要作用时可以从通道中读取字节到多个缓冲区中。
  • SeekableByteChannel:主要作用是在字节通道中维护 position ,以及允许 position 发生改变。
  • WritableByteChannel:使通道允许对字节进行写操作。

3、所有已知实现类

  • AbstractInterruptibleChannel:提供一个可以被中断的通道基本实现。
  • AbstractSelectableChannel:可选通道的基本实现,该类定义了处理通道注册、注销和关闭机制的方法。
  • AsynchronousFileChannel:可以以异步的方式从文件读取或往文件写入数据。
  • AsynchronousServerSocketChannel:用于面向流的服务端的异步通道。
  • AsynchronousSocketChannel:用于面向流的客户端的异步通道。
  • DatagramChannel:面向无连接的套接字的可选通道。
  • FileChannel:继承 AbstractInterruptibleChannel 类,主要作用时读取、写入、映射和操作文件的通道。该通道永远是阻塞的操作。
  • Pipe.SinkChannel:一个代表Pipe的可写端的通道。
  • Pipe.SourceChannel:一个代表Pipe的可读端的通道。
  • SelectableChannel:可通过Selector复用的通道。
  • ServerSocketChannel:面向连接的服务端通道。
  • SocketChannel:面向连接的客户端通道。

Channel 体系确实很庞大,所以我们不需要全部的去深入它们,只需要知道其中的几个就行,比如:FileChannel、ServerSocketChannel、SocketChannel等。

那下面就先看看 FileChannel 也是用的最多的一个。

三、FileChannel 类使用

先看类结构图:

Snipaste_2021-11-14_18-43-12.png

再看 API 图:

Snipaste_2021-11-14_18-44-31.png

一、获取文件通道及读取操作

方式一:

1
java复制代码FileChannel fileChannel = FileChannel.open(new File("src/channel/j3.txt").toPath(), StandardOpenOption.WRITE, StandardOpenOption.READ);

解释:根据一个指定的文件获取一个可读写的文件通道

StandardOpenOption 枚举可以指定通道的读写权限。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public enum StandardOpenOption implements OpenOption {
READ, // 读
WRITE, // 写
APPEND, // 在写模式下,进行追加写
TRUNCATE_EXISTING, // 如果文件已经存在,并且它被打开以进行WRITE访问,那么它的长度将被截断为0。如果文件仅以READ访问方式打开,则忽略此选项。
CREATE, // 如果文件不存在,请创建一个新文件。如果还设置了CREATE_NEW选项,则忽略此选项。与其他文件系统操作相比,检查文件是否存在以及创建文件(如果不存在)是原子性的。
CREATE_NEW, // 创建一个新文件,如果文件已经存在则失败。与其他文件系统操作相比,检查文件是否存在以及创建文件(如果不存在)是原子性的。
DELETE_ON_CLOSE, // 关闭时删除文件
SPARSE, // 稀疏文件。当与CREATE_NEW选项一起使用时,此选项将提示新文件将是稀疏的。当文件系统不支持创建稀疏文件时,该选项将被忽略。
SYNC, // 要求对文件内容或元数据的每次更新都以同步方式写入底层存储设备。
DSYNC; // 要求对文件内容的每次更新都以同步方式写入底层存储设备。
}

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Test
public void channelTest() throws IOException {
// 获得一个根据指定文件路径的读写权限文件通道
FileChannel fileChannel = FileChannel.open(new File("src/channel/j3.txt").toPath(), StandardOpenOption.WRITE, StandardOpenOption.READ);
// 获得一段有指定内容的缓冲区
ByteBuffer source = ByteBuffer.wrap("HelloWorld,J3-baiqi".getBytes(StandardCharsets.UTF_8));
// 空的缓冲区
ByteBuffer target = ByteBuffer.allocate(50);
log.info("fileChannel.position():{}", fileChannel.position());
// 将缓冲区中的内容写入文件通道
fileChannel.write(source);
// 通道大小
log.info("fileChannel.position():{}", fileChannel.position());
// 设置读写位置
fileChannel.position(0);
// 将通道中的内容写到空缓冲区
fileChannel.read(target);
// 转换缓冲区读写模式
target.flip();
log.info("target:,{}", new String(target.array(), 0, target.limit()));
//关闭资源
fileChannel.close();
}

方式二:

1
2
java复制代码FileInputStream fileInputStream = new FileInputStream("src/channel/j3.txt");
FileChannel channel = fileInputStream.getChannel();

解释:根据一个文件流获取对应的文件通道,通道的读写权限由流的输入输出决定。

输入 ==》读

输出 ==》写

案例:

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
java复制代码@Test
public void channelTest02() throws IOException {
// 获取输出流
FileOutputStream outputStream = new FileOutputStream("src/channel/j3.txt");
// 根据输出流获得一个 “写” 权限的通道
FileChannel outChannel = outputStream.getChannel();
// 获得一个有指定内容的缓冲区
ByteBuffer source = ByteBuffer.wrap("HelloWorld,J3-baiqi".getBytes(StandardCharsets.UTF_8));
// 将缓冲区内容写入到通道
outChannel.write(source);

// ===============================================================

// 获取输入流
FileInputStream fileInputStream = new FileInputStream("src/channel/j3.txt");
// 根据输入流获得一个 “读” 权限的通道
FileChannel inChannel = fileInputStream.getChannel();
// 获得一个空内容的缓冲区
ByteBuffer target = ByteBuffer.allocate(50);
// 将通道中的内容读到缓冲区
inChannel.read(target);
// 转换缓冲区读写模式
target.flip();
// 读出缓冲区中的内容
log.info("target:,{}", new String(target.array(), 0, target.limit()));
//关闭资源
outChannel.close();
inChannel.close();
}

上面介绍的两个案例实现了通道的基本操作获取、读、写。如果细心的人可以发现一点非常别扭的地方就是通道的读与写的理解,write 是写操作,但被 Channel 调用后就变成了将数据写入通道有点读取数据的意思,反之亦然。

对于这种别扭的地方,我们要如何区理解呢!我说说我的理解:

通道由空变成非空就是写,缓冲区向通道写入了数据;

通道由非空变成空就是读,缓冲区从通道读取了数据。

二、文件复制操作

下面介绍 FileChannel 中两个文件复制操作 API ,非常方便好用,在实际项目中也是有运用。

  • transferTo:将数据复制到目标对象中。
  • transferFrom:将数据从目标对象中复制给自己。

这两个 API 的作用一样,就是作用对象不一样,如果调用方是有数据的那就用 transferTo,反之则用 transferFrom 。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Test
public void copyTest() throws IOException {
/*
需求:将一个视频文件从F:\\Channel\\a.mp4复制到F:\\Channel\\b.mp4
*/
// 准备输入流(源文件)
FileInputStream fileInputStream = new FileInputStream("F:\\Channel\\a.mp4");
// 准备输出流(目标文件)
FileOutputStream fileOutputStream = new FileOutputStream("F:\\Channel\\b.mp4");

// 根据流获取通道
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();

// 指向复制方法
// outputStreamChannel.transferFrom(inputStreamChannel, 0, inputStreamChannel.size());
inputStreamChannel.transferTo(0, inputStreamChannel.size(), outputStreamChannel);
// 关闭资源
fileInputStream.close();
fileOutputStream.close();
}

再来对比一下原生 BIO 复制文件操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Test
public void copyTest2() throws IOException {
/*
需求:将一个视频文件从F:\\Channel\\a.mp4复制到F:\\Channel\\b.mp4
*/
// 准备输入流(源文件)
FileInputStream fileInputStream = new FileInputStream("F:\\Channel\\a.mp4");
// 准备输出流(目标文件)
FileOutputStream fileOutputStream = new FileOutputStream("F:\\Channel\\b.mp4");
//存储数据的字节数组
byte[] b = new byte[1024];
while (true) {
//从输入流中读取数据到字节数组中
int res = fileInputStream.read(b);
//判断是否读到文件末尾,是就跳出循环
if (res == -1) {
break;
}
//将字节数组中的数据通过输出流,写到目标文件中
fileOutputStream.write(b, 0, res);
}
fileInputStream.close();
fileOutputStream.close();
}

两者对比一下,是不是 NIO 的方式较为简单。

不过通过案例对比虽然 NIO FileChannel 方式操作较为简单,但是,在效率上并不比 InputStream 或 OutputStream 高很多,这是因为 NIO 的出现最主要的就是解决阻塞问题,通过 NIO 把线程变成非阻塞这样就提高效率。

而 NIO 的非阻塞与 Socket 相关的通道有关即网络 IO,这些后面会说,在这里只是提一嘴。

四、最后

通篇下来,难点基本上是没有的,主要就是理解 Channel 的作用:==传输数据的通道==。

然后就简单介绍了通道中用的比较多的文件通道(FileChannel)的基本使用,对于操作文件使用它还是比传统的方式简单的,至少在 API 方面是有体现,而我也做了相关案例。

那这就是 Channel 相关的内容了,虽然本片讲的简单了点,但毕竟不是专业 API 讲解,有时间还是建议看看通道中其他方法的使用。

好了,今天的内容到这里就结束了,关注我,我们下期见

查阅或参考资料:

《NIO与Socket编程指南》高洪严著

联系方式:

QQ:1491989462,做个好友,来个点赞之交。


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

CSDN:J3 - 白起

掘金:J3-白起

知乎:J3-白起

这是一个技术一般,但热衷于分享;经验尚浅,但脸皮够厚;明明年轻有颜值,但非要靠才华吃饭的程序员。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

每天一个 Linux 命令(10)—— netstat 命令

发表于 2021-11-15

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

命令简介

netstat 命令 用于显示网络连接、内核路由表和网络接口等各种网络统计数据与状态信息等,一般用于检验本机各端口的网络连接情况。 netstat 是在内核中访问网络及相关信息的程序,它能提供 TCP 连接,TCP 和 UDP 监听,进程内存管理的相关报告。

netstat 命令在日常工作中有哪些应用场景呢?一个简单的例子是:突然有大量的请求失败,无法建立 TCP 连接,我们就可以查看某个端口上,比如 8080 上有多个 TCP 连接,这些连接都是什么状态,等等。然后判断系统异常的原因。

命令格式

1
css复制代码netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]

命令参数

参数 解释
-g,--groups 显示 IPv4 与 IPv6 的广播组成员信息。
-i,-I=iface,--interfaces=iface 显示指定或所有网络接口的统计数据及状态信息等。
-r,--route 显示内核路由表。
-s,--statistics 按照协议分类显示每个协议的汇总统计数据。
-a,--all 显示所有套接字(包括正在监听和未监听的套接字)的状态信息。
-c,--continuous 每秒一次,连续显示指定的信息。
-e,--extend 显示附加的信息。连续指定 -e 选项两次将会显示更多的内容。
-l,--listening 仅显示监听的套接字。
-n,--numeric 只需显示 IP 地址或端口号即可,不必把 IP 地址解析成相应的主机名或端口名等。
-o,--timers 显示时增加网络时钟方面的信息。
-p,--program 表示显示每个套接字所属程序的名字与进程 ID。
-v,--verbose 显示命令的处理过程与动作。
-t,--tcp 仅显示 TCP 套接字的状态信息。
-u,--udp 仅显示 UDP 套接字的状态信息。
-w,--raw 仅显示原始套接字的状态信息。
-x,--unix 仅显示 UNIX 域套接字的状态信息。
--inet,--ip 显示 TCP、UDP 及原始套接字的状态信息。

输出字段

1
2
3
4
5
css复制代码Active Internet connections
Proto Recv-Q Send-Q Local Address Foreign Address (state)

tcp4 0 0 localhost.54676 10.225.138.92.9575 ESTABLISHED
tcp4 0 0 localhost.54673 10.225.133.153.9717 ESTABLISHED

Active Internet connections

字段 解释
Proto 套接字采用的协议,如 tcp、udp 或 raw。
Recv-Q 本地主机当前套接字接收队列中网络应用程序尚未读取的数据字节计数。
Send-Q 本地主机当前套接字发送队列中尚未确认远程主机是否已读取的数据字节计数。
Local Address 活动套接字的本地主机地址与端口号,其表示形式为 主机:端口号,“主机”是主机名或 IP 地址。“端口号”是一个网络服务,可以是 /etc/services 文件定义的端口号,也可以是相应的服务名(如 telnet)。除非指定了 -n 选项,本地主机地址字段通常是主机名(或规范域名)与服务名的组合。
Foreign Address 活动套接字的远程主机地址与端口号。表示方法同上。
State 表示网络连接(套接字)的状态。即常见的 TCP 状态,例如 ESTABLISHED、SYN_SENT 等。(注意,raw 模式与 UDP 协议不提供网络连接状态信息,故其状态列通常为空)。
User 套接字文件属主的用户名或用户 ID。
PID/Program name 打开或使用套接字的进程的 PID 与进程名字。注意,仅当指定了 -p 选项后才会输出此项,而且只有超级用户才能看到完整的信息。

Active UNIX domain Sockets

字段 解释
Proto 套接字采用的协议(通常为 unix)。
RefCnt 引用计数,表示加接到相应套接字的进程数量。
Flags 标志字段。其中 ACC(SO_ACCEPTON)、W(SO_WAITDATA) 或 N(SO_NOSPACE) 是可能出现的标志之一。ACC 标志表示一个套接字尚未连接,其相应的进程正在等待网络连接请求。
Type 套接字访问类型。
PID/Program name 打开套接字的进程的 PID 与进程名字。注意,仅当指定了 -p 选项后才输出此项,而且只有超级用户才能看到完整的信息。
Path 套接字的文件路径名。

Kernel Interface table(“netstat -i”命令)

1
2
3
4
5
css复制代码netstat -i
Name Mtu Network Address Ipkts Ierrs Opkts Oerrs Coll
lo0 16384 <Link#1> 20089910 0 20089910 0 0
lo0 16384 127 localhost 20089910 - 20089910 - -
lo0 16384 localhost ::1 20089910 - 20089910 - -
字段 解释
Iface 网络接口的名字。
MTU 网络接口当前支持的最大传输单位。MTU 是 IP 模块的设备驱动程序一次收发时能够处理的最大字节数量。对于常规的以太网络接口、回环网络接口(loopback)和 IEEE 802.3 网络接口,MTU 的默认值分别为 1500、8232 和 1492。
Met 网络接口当前支持的路由度量值。
RX-OK 正确无误地接收分组数据的数量。
RX-ERR 接收的分组数据本身有误的分组数据数量。

应用实例

  1. 列出所有端口
1
css复制代码netstat -a
  1. 显示当前 UDP 连接状况
1
复制代码netstat -nu
  1. 显示 UDP 端口号的使用情况
1
复制代码netstat -apu
  1. 显示网卡列表
1
css复制代码netstat -i
  1. 显示组播组的关系
1
复制代码netstat -g
  1. 显示网络统计信息
1
复制代码netstat -s
  1. 显示监听的套接口
1
复制代码netstat -l
  1. 显示所有已建立的有效连接
1
复制代码netstat -n
  1. 显示关于以太网的统计数据
1
复制代码netstat -e
  1. 显示关于路由表的信息
1
复制代码netstat -r
  1. 列出所有 tcp 端口
1
复制代码netstat -at
  1. 统计机器中网络连接各个状态个数
1
css复制代码netstat -a | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
  1. 把状态全都取出来后使用 uniq -c 统计后再进行排序
1
bash复制代码netstat -nat |awk '{print $6}'|sort|uniq -c
  1. 找出程序运行的端口
1
perl复制代码netstat -ap | grep ssh

参考文档

  • netstat命令
  • 《Linux 常用命令简明手册》—— 邢国庆编著

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

环境变量、HISTORY和SHELL(二) bash程序参数

发表于 2021-11-15

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。

我们经常会发现输入过的shell指令从history里面找不到,你知道为什么吗?

我们也经常觉得设置的环境变量没有生效,一顿操作后又好了,你仔细想过其中的原理吗?

本文将深入分析其中的原理。

这是讲shell系列的第二篇,第一篇为SSH、SHELL和终端(一),有兴趣可以看看。

bash程序参数和返回值

程序就是一个黑盒,有输入输出,shell程序有些自定义变量

  • $$ 当前进程ID
  • $0 当前进程启动命令
  • $1 当前输入参数
1
2
3
4
5
shell复制代码# ps $$
PID TTY STAT TIME COMMAND
8898 pts/1 Ss 0:00 -bash
# echo $$
8898
  • exit命令: status 返回0表示成功, 1表示失败

history1.png

  • 进程有配置文件,一般初始化的时候都会先读取配置文件
  • bash有配置文件/root/.bash_profile 等
  • history也有配置文件
  • 其他各种程序都有配置文件

环境变量和SHELL

系统和程序都用

环境变量是给操作系统或者应用程序使用的变量信息。

1
2
shell复制代码# echo $HISTFILE
/root/.bash_history
  • 可以设置整体的环境变量
  • 进程也可以单独设置自己独有的环境变量

环境变量的隔离

  • 使用shell内置的export设置的环境变量,当前bash可生效;export命令可新增,修改或删除环境变量

history3.png

  • export设置的环境变量在bash具有隔离性,A 只在bash1生效,B只在bash2生效。

这就是为什么我们经常觉得设置的环境变量没有生效的原因。

环境变量的共享

需要把export写到bash配置文件才能全局生效:

  • 其他同时运行的bash执行source才能看到其他bash设置的环境变量。
  • 新开的bash启动的时候会先将配置文件里面的命令执行一遍。

如下图:

history4.png

  • alias设置别名也是这样的生效原理,这是因为也是bash的默认配置文件。
1
2
shell复制代码 #echo "alias ll=\"ls -l\"" >>.bash_profile
#. .bash_profile
  • source和.是一样的,是重新从配置文件读取并执行配置文件中的命令。
  • 这里提到的命令都是shell的内置命令,他们都是在shell进程里面运行的命令(不会启动一个新的进程)。

环境变量可继承

  • 因为子进程继承父进程的数据空间,所以子进程在启动时可以继承父进程的环境变量
  • 在bash的终端里,设置的环境变量终端执行的程序都可以看到,因为都是bash的子进程。
    如下启动一个新的bash,还是可以读到$AAA。
1
2
3
4
5
6
shell复制代码# export AAA=a
# echo $AAA
a
# bash
# echo $AAA
a
  • 在守护进程里,同理。

SHELL历史HISTORY

我们经常会发现输入过的shell指令从history里面找不到,你知道为什么吗?

这里讲先述history(history [n])命令。

基本用法和相关环境变量

  • n 的参数仅列出最后 n 行。
  • 如果设置了 shell 变量 HISTTIMEFORMAT可以显示时间
    如下演示:
1
2
3
4
5
6
7
8
9
10
11
shell复制代码# echo $HISTTIMEFORMAT
%F %T
# history 2
2989 2021-11-14 08:45:35 whereis java
2990 2021-11-14 08:45:48 ll -a /usr/bin/java
#echo $HISTFILE
/root/.bash_history
# echo $HISTSIZE
3000
# history |wc -l
3000
  • HISTFILE是history历史文件的记录文件。
  • HISTSIZE是HISTFILE最大行数
  • 还有一些不常用的变量

选项和功能

推拉原理

history有几个重要的选项,提供了以下功能:

  • -a 追加“新”历史行,从当前 bash开始输入到历史文件。
  • -n 将尚未从历史文件中读取的历史行读入当前历史列表。这些是自当前 bash 会话开始以来附加到历史文件的行。

history6.png

  • -a 执行逻辑,交互式输入的,不重复录入history命令,也可以指定要忽略的一些常见命令
  • -n 将尚未从历史文件中读取的历史行读入当前历史列表

我们经常会发现输入过的shell指令从history里面找不到,就是因为我们不清楚shell的历史记录是保存在内存里的,需要执行命令或正常退出的时候才保存到文件;而有太多的异常退出了。

异常退出

bash进程exit退出的时候会保留曾经执行过的history,很多异常都会导致shell被中断,比如:

  • 断电
  • 断网
  • 终端强行被关闭

也有可能bash存在定期保持历史的行为,但是如果有重要的命令要及时保存,不能心存侥幸。
history2.png

所以这就能解释我们为什么history总找不到曾经执行过的命令了。

其他选项

还有一些其他选项的功能,但基本不重要了,了解或不了解都可。

  • history -c 删除所有条目来清除历史列表,一切归零。
  • -d offset: 删除位置偏移处的那条历史记录。
  • -r 读取历史文件的内容并将其用作当前历史
  • -w 将当前历史写入历史文件,覆盖历史文件的内容

快捷键

  • CTRL+R: 搜索最新的历史命令

#(reverse-i-search)\do`:`

  • 上下箭头:查找刚执行的上面一个或几个命令。

shell脚本有没有history记录?

shell脚本

上面提到,shell脚本是通过bash来运行的一个脚本文件。
就像前面提到的那样,bash程序就是一个黑盒,有输入输出:
$$ 当前bash进程ID,
$0 当前shell指令,
$1 当前输入参数…

但是shell脚本有没有history记录呢?

做了一个小实验,
开启的终端运行的shell是/bin/bash(PID=9923),

小实验1

  • hello脚本文件里面不指定shell,以下是全部内容:
1
2
3
shell复制代码echo "Hello World !"
sleep 15
echo "done"

运行chmod a+x hello && ./hello
则会启动一个新的bash,在新bash里面
会依次执行这些命令
如下效果:

1
2
3
4
5
6
yaml复制代码# ps -ef |grep 9923
root 9923 9920 0 16:59 pts/1 00:00:00 -bash
root 11686 9923 0 17:04 pts/1 00:00:00 -bash
# ps -ef |grep 11686
root 11686 9923 0 17:04 pts/1 00:00:00 -bash
root 11687 11686 0 17:04 pts/1 00:00:00 sleep 15

history9.png

可以看出,

  • 11686是新启动的bash进程,正在执行sleep 15这个指令。
  • 11686读取hello文件lr-x------ 1 root root 64 11月 15 21:43 254 -> /root/hello
  • 等bash2的指令执行完毕,则11686退出
  • 退出后,执行记录没有保存到history文件

小实验2

  • 以#!/bin/bash开头
1
2
3
shell复制代码#!/bin/bash
echo "Hello World !"
...

效果:

1
2
3
4
5
6
shell复制代码# ps -ef |grep 9923
root 9923 9920 0 16:59 pts/1 00:00:00 -bash
root 14967 9923 0 17:15 pts/1 00:00:00 /bin/bash ./hello
# ps -ef |grep 14967
root 14967 9923 0 17:15 pts/1 00:00:00 /bin/bash ./hello
root 14968 14967 0 17:15 pts/1 00:00:00 sleep 15

history8.png

可以看出

  • 使用/bin/bash ./hello的方式运行脚本,这个进程是14967,里面运行的指令是它的子进程。
  • 以#!/bin/sh开头的脚本效果和上面一样,只不过运行的进程换成sh-/bin/sh ./hello
  • 还可以看出sleep进程的pid只比运行它的shell大1,因为我们进程ID是单调递增的,说明echo命令没有创建新的进程。
    • Shell在执行外部命令或子脚本,会向系统申请创建新进程
    • Shell的内部命令不会创建新进程
    • Built-in命令很多,包括 alias, bg,cd, echo, exit, export, fg, help, history, jobs, kill, pwd, read, set, source,type,ulimit等,echo也是内置命令
      1
      2
      shell复制代码# type echo
      echo 是 shell 内嵌

小实验3

在bash里面输入bash

1
2
3
4
5
6
shell复制代码# bash
# whoami
root
# exit
exit
# history -n
  • 子进程bash执行whoami
  • 子进程bash exit
  • 父进程执行history -n
  • 父进程bash上下能翻到whoami这个历史命令

但是之前的shell脚本却没有history记录,这是为什么呢?

可以猜想是因为那个shell不是交互式的,而是读取的文件,所以不记录到历史文件。

  • shell bash通过-i 选项,或者不读文件,或存在 -c 选项(从 string 中读取命令),则 shell 是交互式的,否则不是。
  • history功能是为交互式设计的,非交互式的命令不记录history文件的。

所以回到问题,shell脚本有没有history记录?

答案就是没有。

Next

Shell系统的下一篇,会讲讲exec,以及Docker中的exec和shell两种命令启动模式。


如果这篇文章对您有所帮助,或有所启发的话,可以点赞收藏或关注:),您的支持是我坚持写作最大的动力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

简单聊聊HashMap HashMap底层

发表于 2021-11-15

HashMap底层

在jdk1.8之前,HashMap底层是基于数组和链表实现

在jdk1.8后,HashMap底层基于数组+链表+红黑树实现

原因是随着数据量的不断增加,出现hash碰撞的概率大幅上升,此时会形成一个很长的线性链表,导致数据结构线性化严重,效率随之降低。因此在jdk1.8引入了红黑树(当数组大小超过64,链表节点超过8个,底层会自动将链表结构转为红黑树结构,下面会详细分析数组、链表、红黑树的创建时机和创建规则)

1.1数组的作用是什么?什么时候会创建数组?

数组本质上就是用来存储键值对的,当我们首次调用put方法时,会创建一个默认长度为16,加载因子为0.75的数组。

1.2链表的作用是什么?什么时候会创建链表?

HashMap存储数据时,是通过hash(key) 方法计算出key的哈希值,再将次哈希值和数组最大索引进行位运算,得到一个小于等于最大索引的值(这里的二级制位运算可以保证计算的索引值一定是小于等于数组的最大索引,确保了不会出现索引越界)。了解了获取索引的方式,那么就要来看看key的哈希值!!我们都知道,不同的key,可能会存在相同的hash值,那么此时就会出现哈希冲突(计算出来的数组索引位置处已经有key-V的数据存在了)。此时就需要判断本次新增数据的key,是否与原本的key一致(相当于调用hash+equal方法比较key),如果不一致,此时会创建一个链表,将本次的数据以链表的形式,挂载在当前索引下。总结一下,就是当出现hash冲突时,并且两个key不一致,会创建链表。

1.3红黑树的作用是什么?什么时候会创建?

在上面介绍过,当数据量很大时,hash冲突会越来越多,会导致线性的长链表。当链表节点超过8个,底层会将链表转为红黑树。

1.4数组扩容为什么是2倍?

这个问题,其实和hash值与最大索引位运算得到索引值有异曲同工之妙。举个例子,当前数组长度是16,2倍扩容后就是32,那么最大索引就是32-1=31,对应二进制0001 1111,仔细观察,不论哈希值是多少,与0001 1111进行与位运算,得到的值一定是小于等于31的,这样就保证了不会出现索引越界。

2.1关于put方法

2.1.1 根据key进行哈希运算 得到哈希值

底层会将key作为参数,调用hash(key)方法,计算出一个int类型的哈希值(底层在计算时,还会与运算h>>>16,将哈希值散列,尽量避免哈希冲突)

2.1.2 创建默认数组

首次会默认创建一个长度为16,加载因子为0.75的数组。

2.1.3 哈希值与数组最大索引进行与运算

哈希值与数组最大索引进行与运算,得到一个小于等于最大索引的值,这个值就是数据存储到数组的索引

2.1.4 判断与运算结果,是否出现hash冲突

如果上面计算的索引处,已经有值,那么久出现了哈希冲突。此时我们需要去判断key是否真的一致,那就是hash+equal方法比较key。如果key一致,那么覆盖原有的数据,如果key不一致,此时就会做链表挂载或者红黑树新增节点

2.1.5 出现哈希冲突,存在的是链表还是红黑树,如果是链表判断是否需要转为红黑树

这里到底是挂载链表还是新增红黑树节点,取决于目前的数据结构(没有达到转红黑树条件时,都是链表)。如果当前是链表挂载,需要判断当前挂载完成后,长度是否超过8,以此来判断是否需要转为红黑树。

2.1.6 判断是否需要数组扩容

如果当前数据没有产生哈希冲突,那么需要考虑到数组的扩容问题。例如当前数组长度是16,本次数据保存后,数组被使用的长度是否超过12,如果超过,就进行两倍扩容。

当然,在我们日常使用中,可能没有去在意底层的一些执行逻辑,有兴趣的同学可以看看源码。关于get方法,我们下次再聊。

以上是作者对HashMap底层的一些看法,有不对或者不严谨的地方,希望大家多多指正。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记录一次微服务中使用SpringSecurity集成企业微信

发表于 2021-11-15

记录一次微服务中使用SpringSecurity集成企业微信扫码登录-校验请求来源错误问题的解决
最近在项目中遇到一个平时不太注意的问题,记录一下方面后期继续深入跟踪,再近期项目中需要使用企业微信扫码登录,在将企业微信二维码嵌入到访问页面中的时候,结果二维码加载失败(校验请求来源错误),结果如下图所示。
失败1.png

场景复现

接入企业微信二维码方式

  1. 企业微信管理端添加访问白名单;
  2. 企业微信管理端设置可信域名(域名要保持和企业微信回调的域名相同)
  3. 必须要通过页面跳转打开访问生成企业微信二维码的链接

实现代码

登录页面(login.html)和跳转链接(忽略掉html布局略丑,单纯为了记录没有做美化)
web1.png

1
2
3
4
html复制代码<a href="https://testapi.xxxx.com/java/troy">企业微信</a>
<p></p>
<a href="https://testapi.xxxx.com/java/troy1">企业微信1</a>
<p></p>

nginx配置

1
2
3
4
5
6
7
8
9
10
11
nginx复制代码 location /java/login {
proxy_pass http://192.168.201.118:9999/java/login;
}

location /java/troy {
proxy_pass http://192.168.201.118:9999/java/troy;
}

location /java/troy1 {
proxy_pass http://192.168.201.118:9999/java/troy1;
}

springboot中springsecurity配置(这里省略掉其他配置)

1
2
3
4
5
6
7
8
java复制代码@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.headers().hsts().disable().frameOptions().mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN)
.contentSecurityPolicy("default-src https: data: 'unsafe-inline' 'unsafe-eval'")
.and()//默认ReferrerPolicy是ReferrerPolicy.NO_REFERRER策略
.and()
.cors().configurationSource(corsFilter())

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Controller
public class LoginController {

@GetMapping("/java/login")
public String loginPage(Model model) {
return "login";
}
}


@Slf4j
@Controller
public class TestController {

@GetMapping("/java/troy")
public String sso3(ServerWebExchange exchange) {
log.info("come in /troy");
String path = "redirect:https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect?appid=xxxxx&redirect_uri=https%3A%2F%2Ftestapi.xxxxx.com%2Fwxwork%2Fsso3-callback&state=073524&usertype=member";
log.info("end /troy");
return path;
}
}

操作流程:

  1. 首先浏览器打开登录地址LoginController(“/java/login”)
  2. 进入登录页面login.html
  3. 点击页面链接进入TestController(“/java/troy”)进行跳转到企业微信API,然后企业微信内部调起生成二维码,页面呈现二维码用户扫码

抛析问题

在使用springsecurity方式失败后,项目组有同学通过go来调用,采用同样的操作流程缺直接打开了企业微信二维码扫码页面,期间也和同事在网上搜了一些二维码加载不出来的问题,但是可用的解决方案没有找到,提到的一些点又太粗,后面通过对比java和go的跳转链接发现,通过跳转后的链接,失败的时候没有http referer属性。既然发现不同点了,那么就好办了看一下springsecurity框架对于referer的处理,再看看能不能加的上,想到这里直接开动起来。

跳转请求对比

我们通过登录页面,执行操作流程中的第3步时,可以通过跳转链接抓到请求查看区别:

1
2
3
4
5
sh复制代码#失败请求
https://aegis.qq.com/collect/pv?id=XfN&uin=&version=1.34.46&aid=xxxxx&platform=4&netType=4&sessionId=session-1198&from=https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect?appid=xxxx&redirect_uri=https%3A%2F%2Ftestapi.xxxx.com%2Fwxwork%2Fsso3-callback&state=073524&usertype=member&referer=

#成功请求
https://aegis.qq.com/collect/pv?id=N&uin=&version=1.34.46&aid=xxxx&platform=4&netType=4&sessionId=session-10763&from=https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect?appid=xxxx&redirect_uri=https%3A%2F%2Ftestapi.xxxx.com%2Fwxwork%2Fsso3-callback&state=073524&usertype=member&referer=https%3A%2F%2Ftestapi.xxxx.com%2F

通过对比发现失败的请求中缺少referer参数,通过查找springsecurity文档和源码发现,springsecurity会在response header中添加http referer策略,springsecurity默认使用的http referrer策略是ReferrerPolicy.NO_REFERRER。http referrer的介绍可以查看链接。那么到这里问题就简单了修改springsecurity配置,我是用的配置http referrer策略ReferrerPolicy.ORIGIN,大家可以按照自己项目需要选择合理的设置,这里简单讲一下这两个策略的意思,详细解释可以查看链接。

ReferrerPolicy.NO_REFERRER:no-referrer
整个 Referer 首部会被移除。访问来源信息不随着请求一起发送。(这也就解释了为什么失败的时候没有referer的值);

ReferrerPolicy.ORIGIN:origin 不管什么时候只使用origin作为引用地址。例如 www.troyqu.com/page.html的页面只会将www.troyqu.com作为引用地址。

解决问题

修改springboot中springsecurity配置

添加referrerPolicy(ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.ORIGIN)

1
2
3
4
5
6
7
8
java复制代码@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.headers().hsts().disable().frameOptions().mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN)
.contentSecurityPolicy("default-src https: data: 'unsafe-inline' 'unsafe-eval'")
.and().referrerPolicy(ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.ORIGIN).and()//默认ReferrerPolicy是ReferrerPolicy.NO_REFERRER策略
.and()
.cors().configurationSource(corsFilter())

重启服务后访问发现,企业微信二维码可以打开了,到这里所有问题都彻底解决了。

成功1.png
那么既然springsecurity有设置http referer那么我们也可以对比下看看是不是更改配置后,对应的参数也进行了更新呢?

默认配置的控制台http参数

可以看到response header中的referer策略已经更新,设置为no-referrer(再次验证)
失败2.png
内部LoginController跳转到TestController接口的时候,request header中无referer参数。
失败3.png
修改后的控制台http参数

可以看到response header中的referer策略已经更新,设置为origin
成功2.png
内部LoginController跳转到TestController接口的时候,request header中referer参数值也和origin一样,没有其他的http path信息。
成功3.png

总结

  1. springsecurity默认使用ReferrerPolicy.NO_REFERRER:no-referrer的策略,当我们需要访问一些其他厂商的API需要验证referrer的时候,会因为请求缺少referrer导致请求被验证不合法,从而导致请求处理失败。
  2. 之前对于http referrer参数没有进行太深入了解,后面需要补一下相关知识。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java异常类型及其总结

发表于 2021-11-15

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

前言

  相信小伙伴在编写代码的过程中都或多或少的遇到过各种各样的异常,那么你知道都有哪些异常吗?下面带领大家一起回顾一下Java中的异常。

什么是异常

  异常是在我们开发的程序中的一些错误,但是并不是所有的错误都是异常,并且错误有时候是可以避免的。在Java等面向对象的编程语言中异常属于对象。

  异常出现的情况也是很多,包括用户输入非法的字符,在程序中没有校验,就会出现参数异常;在程序运行时,出现了除数0的变量,也会出现异常;打开某个路径下的文件,而文件不存在也会出现异常信息。那么我们面对不同的异常该如何处理呢?哪些异常可以让程序继续运行下去哪些异常需要让程序终止运行?这就是我们在编写开发业务逻辑过程中需要注意的地方。

检查时异常:检查时异常是指程序在编译的过程中,进行词法、语法、语句、依赖等编译过程中的异常,程序员是无法预见的异常,是在编译中提示的异常。例如:缺少依赖文件等。

运行时异常:运行时异常是指在编译后,程序运行阶段出现的异常,这类异常在刚步入开发人员的时候出现的比较多,这一类运行时的异常,往往是可以被程序员代码编写所避免的。例如:空指针异常、

错误:是严重的异常,错误在代码中通常被忽略,例如内存溢出等情况。错误在编译的过程中是检测不到的。

Java中的异常

  作者是Java开发者,所以基于Java中的异常进行了一下简单的总结。如下图是根据javaJDK中列出的一些异常信息。

ThrowableErrorExceptionIOExceptionRuntimeExceptionBeansExceptionIIOException….NoSuchClassErrorClassFormatErrorIOError…
  从图中可以看到Throwable是Error和Exception的超类,所有的异常类都是Exception的子类,所有的错误都是Error的子类。当我们的程序中出现了异常,需要针对不同的异常进行操作。其中以Throwable 类中的基本方法给大家简单介绍。

getMessage()

  getMessage()方法返回关于发生的异常的详细信息。这个消息在Throwable类的构造函数中初始化了。在我们的程序中可以直接通过异常信息去获取Exception e示例如下:

1
js复制代码e.getMessage();

getCause()

  getCause()返回一个Throwable 对象代表异常原因。

toString()

  toString()方法使用getMessage()的结果返回类的串级名字。

printStackTrace()

  printStackTrace()方法打印toString()结果和栈层次到System.err,即错误输出流。一般使用printStackTrace()将异常日志输出到控制台中。

getStackTrace()

  getStackTrace()方法返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。

异常处理

  既然在我们开发程序的中那么多的异常,我们怎么去捕获这些异常并进行处理呢,以减少不必要的错误,那么现在就开始介绍。

throws/throw

  throws/throw关键字的作用是如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明。其简单格式如下:一般在方法的末尾使用。

1
2
3
js复制代码    public void testException() throws Exception{

}

  也可以使用throw 关键字抛出一个异常。使用throw 关键字抛出的异常,在方法名之后需要有throws的关键字声明异常来接受。

1
2
3
js复制代码  public void testException() throws Exception{
throw new Exception();
}

try/catch关键字

  在我们的程序中,针对可能出现的异常代码块可以使用try和catch关键字来捕获异常。try/catch 代码块放在异常可能发生的地方。其中被try/catch代码块中的代码称为保护代码,当保护代码中出现异常之后,可以根据业务需要是否继续执行后续代码,还是针对异常信息进行处理并抛出异常,是一种灵活的异常处理方式,其代码块的格式如下:

1
2
3
4
5
js复制代码try{
// 业务代码
}catch(Exception e){
//Catch 块
}

  当然针对try/catch关键字中多个 catch的方式也是可以的,称为多重捕获。其常用的格式如下:

1
2
3
4
5
6
7
8
9
js复制代码try{
// 业务代码
}catch(异常类型1 e1){
// 业务代码
}catch(异常类型2 e2){
// 业务代码
}catch(异常类型3 e3){
// 业务代码
}

  在try中的代码发生异常的情况下,优先匹配第一个catch块中的内容,由近及远挨着匹配,直到匹配到相关异常。

finally关键字

  finally关键字用来创建在try/catch代码块最后。无论try/catch代码块是否发生异常finally代码块中的代码总会被执行。在finally代码块中,可以运行清理类型等收尾善后性质的语句。其语法格式如下:

1
2
3
4
5
6
7
8
js复制代码
try{
// 业务代码
}catch(Exception e){
//Catch 块
}finally{
// 业务代码
}

结语

  好了,以上就是Java异常类型及其总结,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java import 导入包时,我们需要注意什么呢?

发表于 2021-11-15

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

本文被《从小工到专家的 Java 进阶之旅》收录。

你好,我是看山。

这篇文章起因是 code review 时和同事关于 import 导入声明的分歧。

用过 IDEA 的都知道,默认情况下,通过 import 导入类时,当数量达到设置数量(类 5 个、静态变量 3 个),就会改为按需导入方式,也就是使用使用*号折叠导入。

同事建议不要采用按需导入,要使用单类型导入 (single-type-import)。而我是觉得既然 IDEA 作为宇宙级的 IDE,不会在这种地方出现纰漏,所以想继续按照 IDEA 默认配置来。

所以总结一下这两种方式的差异。如果对 java import 不熟悉,可以从 这里 看看。

import 的两种导入声明

在 java 中,通过 import 导入类的方式有两种:

  • 单类型导入 (single-type-import),例如 import java.io.File:这种方式比较容易理解,而且大部分时候我们用的都是这种方式。通过明确指明类和接口路径,将他们导入进来。
  • 按需类型导入 (type-import-on-demand),例如 import java.io.*:通过通配符*定义导入方式,但是并不是直接导入这个包下的所有类,而是可以导入所有类。也就是说,如果需要就导入,不需要就不导入。

有如下属性:

  1. java 以这样两种方式导入包中的任何一个public的类和接口(只有 public 类和接口才能被导入)
  2. 上面说到导入声明仅导入声明目录下面的类而不导入子包,这也是为什么称它们为类型导入声明的原因。
  3. 导入的类或接口的简名(simple name)具有编译单元作用域。这表示该类型简名可以在导入语句所在的编译单元的任何地方使用。这并不意味着你可以使用该类型所有成员的简名,而只能使用类型自身的简名。例如:java.lang 包中的 public 类都是自动导入的,包括Math和System类。但是,你不能使用它们的成员的简名PI()和gc(), 而必须使用Math.PI()和System.gc(). 你不需要键入的是java.lang.Math.PI()和java.lang.System.gc()。
  4. 程序员有时会导入当前包或java.lang包,这是不需要的,因为当前包的成员本身就在作用域内,而java.lang包是自动导入的。java 编译器会忽略这些冗余导入声明 (redundant import declarations)。

按需导入机制

按需类型导入在大部分情况用起来更加方便,一个通配符可以导入包下的所有类,就不用费劲写一堆导入了。

但是,根据能量守恒,在敲代码时节省下来的能量,必然会在其他地方消耗。

比如,Date类,如果完全使用按需类型导入,可以写做import java.util.*。当这个类恰好需要,PrepareStatement时,又需要加上import java.sql.*导入,这个时候,编译器不知道Date类是要用java.util包里的还是java.sql里面的了,就会报出Reference to 'Date' is ambiguous, both 'java.util.Date' and 'java.sql.Date' match异常,也就是所说的命名冲突。

解决办法就是指明Date类的全路径,也就是使用单类型导入:import java.util.Date。

除了命名冲突,还有一些不太明显的缺点:

  1. 编译速度:因为按需导入机制的特性,需要在 CLASSPATH 下找到所有符合包名的类,在编译时会消耗性能。在小项目中,这个速度可以忽略。如果在大项目中,就会有明细差异。
  2. 可读性:在使用 IDE 开发过程中,我们很少会在import中查看类的路径。但是如果需要我们在其他环境编辑文件,比如 vim,从import查看类的路径就很便捷了。

导入不需要的类会发生什么呢

从理性讲,java 编译器一定会在这里做优化,不会把不需要的导入声明加入到 class 文件中,但是之前没有看到哪里有说明,所以动手做一下实验:

先定义 java 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package cn.howardliu;

// 需要用到的单类型导入
import java.util.Date;
// 需要用到的按需类型导入
import java.math.*;
// 不需要用到的单类型导入
import java.sql.PreparedStatement;
// 不需要用到的按需类型导入
import java.awt.*;

public class Main {
    private Date date1;
    private BigDecimal num1;

    public void test(){
        Date date2 = new Date();
        BigDecimal num2 = new BigDecimal(0);
    }
}

通过命令javac Main.java编译,然后通过javap -verbose Main.class查看编译结果:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
less复制代码Classfile /path/to/Main.class
Last modified 2021-1-31; size 439 bytes
MD5 checksum 81e13559f738197b4875c2c2afd6fc41
Compiled from "Main.java"
public class cn.howardliu.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // java/util/Date
#3 = Methodref #2.#19 // java/util/Date."<init>":()V
#4 = Class #21 // java/math/BigDecimal
#5 = Methodref #4.#22 // java/math/BigDecimal."<init>":(I)V
#6 = Class #23 // cn/howardliu/Main
#7 = Class #24 // java/lang/Object
#8 = Utf8 date1
#9 = Utf8 Ljava/util/Date;
#10 = Utf8 num1
#11 = Utf8 Ljava/math/BigDecimal;
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 test
#17 = Utf8 SourceFile
#18 = Utf8 Main.java
#19 = NameAndType #12:#13 // "<init>":()V
#20 = Utf8 java/util/Date
#21 = Utf8 java/math/BigDecimal
#22 = NameAndType #12:#25 // "<init>":(I)V
#23 = Utf8 cn/howardliu/Main
#24 = Utf8 java/lang/Object
#25 = Utf8 (I)V
{
public cn.howardliu.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0

public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class java/util/Date
3: dup
4: invokespecial #3 // Method java/util/Date."<init>":()V
7: astore_1
8: new #4 // class java/math/BigDecimal
11: dup
12: iconst_0
13: invokespecial #5 // Method java/math/BigDecimal."<init>":(I)V
16: astore_2
17: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 17
}
SourceFile: "Main.java"

从 class 文件内容可以看出:

  1. 按需类型导入方式在 class 文件中的表现形式,与按类型导入一样,也会找到需要的类导入,不会导入包中的所有类。
  2. 不需要的类导入声明,最终都会被优化掉,不会出现在 class 文件中。
  3. java 中的import与 C 语言中的include不同,不会将导入声明的类写入到 class 文件中,各自还是独立的 class 文件。

JDK 推荐哪种方式

JDK 绝对是 java 编程的标杆,我们很多都可以从 JDK 中学习:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.io.OutputStreamWriter;
import java.io.BufferedWriter;
import java.security.AccessController;
import java.security.PrivilegedAction;

import sun.util.spi.XmlPropertiesProvider;

这是java.util.Properties中的 import 声明,可以看出,使用了单类型导入声明,所以,在没有其他要求的情况下,我们尽量还是使用单类型导入。

文末思考

  1. java 的import是类导入声明,不会将文件写入到编译后的 class 文件中
  2. java 的import有两种导入方式:单类型导入、按需类型导入
  3. 按需类型导入只会在编译过程中有性能损失,在运行期与单类型导入无差别
  4. JDK 源码中,大部分使用了单类型导入。

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM 垃圾收集算法

发表于 2021-11-15

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

这是根据周志明老师的《深入理解Java虚拟机》整理的学习笔记。

本文被《从小工到专家的 Java 进阶之旅》收录。

从如何判断对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两大类也常被称作“直接垃圾收集”和“间接垃圾收集”。

主流 GC 算法均未涉及引用计数式垃圾收集,所以本文所有算法均为追踪式垃圾收集的范畴。

  1. 分代收集理论

分代收集(Generational Collection),建立在三个假设上:

1.弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。2.强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难消亡。3.跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

根据前两个分代假说,奠定了多款常用垃圾收集器的一致的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据年龄分配到不同的区域中存储。

一般至少将 Java 堆划分为新生代(Young Generation)和老年代(Old Generation)。

分代收集理论除了划分内存区域,还需要解决其他问题,比如:对象不是孤立的,对象之间会存在跨代引用。为了解决这个问题,就出现了第三条假说。

第三条假说是根据前两条假说逻辑推理得出的隐含推论:存在相互引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

依据这条假说,我们就不应再为少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每个对象是否存在及存在哪个跨代引用,只需在新生代上建立一个全局的数据结构,被称为记忆集(Remembered Set),这个结构把老年代划分成若干小块,标识老年代哪一块内存会存在跨代引用。当发生 Minor GC 时,只有包含了跨代引用的小块内存里的对象才会被加入到 GC Roots 进行扫描。

名词解释:

•部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集

•新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集•老年代收集(Major GC/Old GC):值目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。

•混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。

•整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

  1. 标记-清除算法

标记-清除(Mark-Sweep)算法:标记所有需要回收对象,在标记完成后,统一回收掉所有被标记的对象;或者,标记存活的对象,统一回收所有未被标记的对象。

缺点:

1.执行效率不稳定,如果 Java 堆中包含大量对象,而且其中大部分需要被回收,这时必须进行大量标记和清除的动作,导致标记和清除的执行效率随对象数量增长而降低;2.内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后程序运行过程中需要分配较大对象时无法找到足够连续的内存而不得不触发另一次垃圾收集动作。

图片

  1. 标记-复制算法

半区复制(Semispace Copying)算法:将可用内存按容量划分大小相等的两块,每次只使用其中一块。当一块内存用完了,就将还存活着的对象复制到另外一块上,然后再把已使用过的内存空间一次清理掉。

图片

Appel 式回收:把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。如果 Survivor 空间不足以容纳 Minor GC 后存活对象时,这些对象通常分配进入老年代。

HotSpot 虚拟机默认 Eden 和 Survivor 大小比例是 8:1,即每次新生代中可用内存空间为整个新生代容量的 90%,只有一个 Survivor 空间被浪费。

Appel 式回收的理论依据:IBM 公司的研究表明,新生代中的对象有 98%熬不过第一轮收集。

  1. 标记-整理算法

标记-整理(Mark-Compact)算法,其中标记过程与“标记-清除”算法一样,整理过程是让所有存储对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。

图片

标记整理算法的弊端是移动存活对象并更新所有引用工作量比较大,而且需要 Stop The World,会造成延迟。

标记清除算法的弊端是由于空间碎片化,需要复杂的内存分配器和内存访问器,会影响吞吐量。

HotSpot 虚拟机里面关注吞吐量的的 Parallel Scavenge 收集器是基于标记-整理算法的,关注延迟的 CMS 收集器是基于标记-清除算法的。

还有一种“和稀泥”的方式,大部分时间使用标记-清除算法,容忍内存碎片,当内存碎片影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis复制的实现

发表于 2021-11-15

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

Redis复制的实现

向从服务器发送SLAVEOF命令,可以让从服务器复制一个主服务器 命令格式SLAVEOF 主服务器ip 主服务器port

1. 设置主服务器地址和端口

客户端向从服务器发送SLAVEOF命令后,从服务器会将客户端给定的主服务器的ip和port保存到redisServe的masterhost属性和masterport属性中

SLAVEOF命令是异步命令,完成masterhost属性和masterport属性的设置工作后,从服务器向客户端返回OK,实际的复制工作在OK返回之后才真正开始执行

2. 建立套接字连接

在SLAVEOF命令执行后,从服务器将根据命令设置的IP和port创建连向🔗主服务器的套接字,如果从服务器创建的套接字成功连接到主服务器,从服务器会为这个套接字关联一个文件事件处理器,这个处理器将负责执行后续的复制工作。主服务接受从服务器的套接字后,为该套接字创建客户端状态。

3. 发送PING命令

从服务器成为主服务器的客户端后,向主服务器发送PING命令,检查套接字的读写状态是否正常,检查主服务器是否正常处理命令请求。

4. 身份验证

从服务器收到主服务器返回的PONG后,如果从服务器设置了masterauth则需要进行身份验证,如果主从服务器设置的密码相同,则可以进行复制,否则进行重试

5. 发送端口信息

身份验证通过后从服务器执行命令REPLCONF listening-port 端口号,向主服务器发送从服务器的监听端口号,主服务收到命令后记录redisClient的slave_listening_port属性中

6. 同步

从服务器向主服务器发送PSYNC命令,执行同步,将自己数据库更新和主服务器一样

7.命令传播

完成同步后,主从服务器进入命令传播阶段,主服务器将自己执行的写命令发送给从服务器,从服务器接收并执行写命令就可以保证数据一致。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从JDK中学习设计模式——装饰模式

发表于 2021-11-15

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

概述

装饰模式(Decorator Pattern)动态地给一个对象增加一些额外的职责,就增加对象的功能来说,装饰模式比生成子类更灵活。

装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为。例如,张三可以在水壶上加上哨子,使水壶具有提醒功能。

结构

装饰模式UML.png

● Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法。它的引入可以使客户端以一致的方式处理未被装饰的对象和装饰之后的对象。

● ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责。

● Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。

● ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。

优点

  1. 装饰类和被装饰类可以独立发展,不会相互耦合,符合开闭原则。
  2. 装饰模式是继承关系的一个替代方案,解决了继承带来的类膨胀与强侵入问题。
  3. 装饰模式可以动态的扩展一个实现类的功能。

缺点

实现类的功能用多个装饰类进行装饰,会使系统比较复杂,增加了实现类的耦合度。

应用场景

  1. 需要扩展一个类的功能,或者是给一个类增加附加功能。
  2. 需要动态的给一个对象增加功能,这些功能可以再动态的撤销。
  3. 需要为一批兄弟类进行改装和添加功能。

JDK 中的应用

在 JDK 中 java.io.InputStream 中使用了装饰模式。 java.io.InputStream 是抽象构件, java.io.FileInputStream 是具体构件, java.io.FilterInputStream 是抽象装饰, java.io.DataInputStream 是具体装饰。

InputStream.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…333334335…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%