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

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


  • 首页

  • 归档

  • 搜索

【Go语言入门150题】 L1-056 猜数字 (20 分)

发表于 2021-11-11

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

【题解】【PTA团体程序设计天梯赛】

L1-056 猜数字 (20 分) Go语言|Golang

一群人坐在一起,每人猜一个 100 以内的数,谁的数字最接近大家平均数的一半就赢。本题就要求你找出其中的赢家。

输入格式:

输入在第一行给出一个正整数N(≤10e4)。随后 N 行,每行给出一个玩家的名字(由不超过8个英文字母组成的字符串)和其猜的正整数(≤ 100)。

输出格式:

在一行中顺序输出:大家平均数的一半(只输出整数部分)、赢家的名字,其间以空格分隔。题目保证赢家是唯一的。

输入样例1:

1
2
3
4
5
6
7
8
in复制代码7
Bob 35
Amy 28
James 98
Alice 11
Jack 45
Smith 33
Chris 62

结尾无空行

输出样例1:

1
out复制代码22 Amy

结尾无空行

思路:

这道题可能对小白来说,有点困难,但其实只是障眼法罢了,我们只需要定义一个结构体,这个结构体里面放上名字和电话号码就可以了。然后我们可以通过定义一个该结构体的数组,然后一边输入一边进行计算求和。

注意的是这个sum一定要是float64类型,因为go语言是强类型的语言,直接当成int的类型这样是不行的!

所以我们必须要进行一个定义成0.0的float64类型,然后我们就可以计算出平均数,然后再除/2进行对半分解了。
之后我们就可以通过go语言中math包里面定义的math.Abs进行绝对值的求解。

然后就可以通过排序进行调整,最后输出最符合要求的一个即可。

代码如下:

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
go复制代码package main

import (
"fmt"
"math"
)

type person struct { //定义一个person结构体
Name string
Number float64
}

func main() {
var num float64
sum := 0.0
_,_=fmt.Scan(&num)
var personList [1000]person //定义一个该结构体的数组
for i:=0;i<int(num);i++ {
_, _ = fmt.Scan(&personList[i].Name, &personList[i].Number)
sum += personList[i].Number
}
sum = sum/num/2 // 平均数的一半
var ans person
minute := math.Abs(personList[0].Number-sum) // 计算绝对值
ans = personList[0]
for _,item := range personList[:int(num)]{ // 循环计算,最终答案
if math.Abs(item.Number-sum)<minute { // 如果比设定的值要小的话,就直接等于即可
minute = item.Number-sum // 进行一个转化
ans = item // 这里就可以是等于了
}
}
fmt.Printf("%d %s",int(sum),ans.Name) // 直接输出即可,不过注意不要有多余回车
}

L1-057 PTA使我精神焕发 (5 分) Go语言|Golang

输入格式:

本题没有输入。

输出格式:

在一行中按照样例输出,以惊叹号结尾。

输入样例1:

1
in复制代码无

结尾无空行

输出样例1:

1
out复制代码PTA shi3 wo3 jing1 shen2 huan4 fa1 !

结尾无空行

思路:

基本的输出语句,没有难度。但是注意的是空格,不要用println,要用print,所以这里是我们进行处理时候的一个空格的小小的细节。

代码如下:

1
2
3
4
5
6
7
8
9
go复制代码package main

import (
"fmt"
)

func main() {
fmt.Printf("PTA shi3 wo3 jing1 shen2 huan4 fa1 !") // 直接输出即可,不过注意不要有多余回车
}

本文转载自: 掘金

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

MDN 的搜索结果自动补全是怎么做的?(三)

发表于 2021-11-11

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

本文翻译自:hacks.mozilla.org/2021/08/mdn… ,作者:Peter Bengtsson

MDN Web Docs 中新添加了一个搜索结果自动补全的功能,这使你可以通过键入文档标题的一部分来快速直接跳转到您要查找的文档。这是关于这个功能如何做的的文章。如果你坚持到底,我将分享一个“复活节彩蛋”功能。

image.png

系列文章

MDN 的搜索结果自动补全是怎么做的?(一)

MDN 的搜索结果自动补全是怎么做的?(二)

实现细节

上一篇文章我们看到大致的代码结构,其中搜索部分的代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码(async function() { 
const response = await fetch('/en-US/search-index.json');
const documents = await response.json();
const inputValue = document.querySelector( 'input[type="search"]' ).value;
const flex = FlexSearch.create();
documents.forEach(({ title }, i) => {
flex.add(i, title);
});
const indexResults = flex.search(inputValue);
const foundDocuments = indexResults.map((index) => documents[index]);
displayFoundDocuments(foundDocuments.slice(0, 10));
})();

首先,使用了一个名为 downshift 的 React 库,它处理所有的交互、显示,并确保显示的搜索结果是可访问的。downshift 是一个成熟的库,它通过构建这样的小部件应对了无数挑战,特别是在可访问性方面。

downshift 地址:www.npmjs.com/package/dow…

其次,使用了 FlexSearch 库,它是一个第三方库,为了确保搜索标题时考虑到自然语言。它将自己描述为“Web 上最快、内存最灵活、零依赖的全文搜索库”。这比尝试简单地在其他字符串的长列表中查找一个字符串要高效和准确得多。

决定先展示哪个结果

公平地说,如果用户输入 foreac,那么将1万多个文档标题缩减到只包含 foreac 的标题并不难,然而我们需要决定先显示哪个结果。我们实现这一点的方法是对页面浏览量进行统计。我们记录,对于每一个 MDN URL,哪一个获得最多的页面浏览量,决定页面的“流行度”。大多数人决定到达的文档最有可能是用户正在搜索的文档。

这部分具体可以看:hacks.mozilla.org/2021/03/how…

源码地址

所有这些代码都在 Yari repo 中,Yari repo 是构建和预览所有 MDN 内容的项目。要找到确切的代码,请单击 client/src/search.tsx。你会发现所有的代码惰性加载,搜索,预加载,和显示自动完成搜索。

本文转载自: 掘金

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

Selenium必备知识 Selenium必备知识

发表于 2021-11-11

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

Selenium必备知识

浏览器窗口最大化

maximize_window()函数可将模拟浏览器窗口最大化

1
2
3
4
5
python复制代码from selenium import webdriver

w=webdriver.Chrome()
w.maximize_window()
w.get('https://www.baidu.com/')

QQ录屏20211110144428 00_00_00-00_00_30

XPath法定位

image-20211110144956682

打开百度

定位输入框并输入selenium

1
2
3
4
5
6
7
python复制代码from selenium import webdriver
from selenium.webdriver.common.by import By
w=webdriver.Chrome()
w.maximize_window()
w.get('https://www.baidu.com/')
# 定位输入框并输入selenium
w.find_element(By.XPATH,'//*[@id="kw"]').send_keys('selenium')

QQ录屏20211110145557

CSS法定位

image-20211110150010371

定位到百度一下并点击

1
2
3
4
5
6
7
8
9
10
11
python复制代码from selenium import webdriver
from selenium.webdriver.common.by import By

w=webdriver.Chrome()
w.maximize_window()
w.get('https://www.baidu.com/')
# 定位输入框并输入selenium
# .send_keys('selenium') 表示模拟键盘输入selenium
w.find_element(By.XPATH,'//*[@id="kw"]').send_keys('selenium')
# .click()表示模拟鼠标单击
w.find_element(By.CSS_SELECTOR,'#su').click()

切换浏览器同级页面

1
2
3
4
5
6
7
8
python复制代码# 获取浏览器所有窗口的句柄
handles = w.window_handles
# 切换到第一个窗口
w.switch_to.window(handles[0])
# 切换到最后一个窗口
w.switch_to.window(handles[-1])
# 切换到第二个窗口
w.switch_to.window(handles[1])

切换内嵌页面(网页中的网页)

image-20211111131531835

iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。

1
2
3
css复制代码Iframe标记又叫浮动帧标记,可以用它将一个HTML文档嵌入在一个HTML中显示。
它和Frame标记的最大区别是在网页中嵌入 的<Iframe></Iframe>所包含的内容与整个页面是一个整体,而<Frame>< /Frame>所包含的内容是一个独立的个体,是可以独立显示的。
另外,应用Iframe还可以在同一个页面中多次显示同一内容,而不必重复这段内 容的代码。
1
2
3
python复制代码w.switch_to.frame('子页面的name属性值或者id属性值')
# 跳回最外层的页面
w.switch_to.default_content()

如果遇到没有id属性和name属性为空的情况,这时候就需要先定位iframe。

1
2
python复制代码iframe=w.find_element(By.TAG_NAME,'iframe')
w.switch_to.frame(iframe)

本文转载自: 掘金

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

高频算法面试题(二十二)- 两数相加II

发表于 2021-11-11

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

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

链表中的两数相加

题目来源:LeetCode-445. 两数相加 II

题目描述

给定两个 非空链表 l1和 l2 来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表

可以假设除了数字 0 之外,这两个数字都不会以零开头

示例

示例 1

image.png

1
2
css复制代码输入:l1 = [7,2,4,3], l2 = [5,6,4]
输出:[7,8,0,7]

示例 2

1
2
css复制代码输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[8,0,7]

示例 3

1
2
css复制代码输入:l1 = [0], l2 = [0]
输出:[0]

提示:

  • 链表的长度范围为 [1, 100]
  • 0 <= node.val <= 9
  • 输入数据保证链表代表的数字无前导 0

进阶:如果输入链表不能修改该如何处理?换句话说,不能对列表中的节点进行翻转

解题

思路

这个题在前边已经做过类似的,就是大数相加。思路差不多,这个稍微不一样的地方就是,它的高位存在了链表的前边,低位在后边。因为是单向链表,我们不能说让指针指向尾部,然后往前遍历来进行求和

因为这跟我们求和是反着来的,很容易就想到用栈这种数据结构。先分别遍历这两个链表,然后将链表的元素分别压入各自的栈中,然后逐个从栈中弹出元素进行求和(求和过程很简单,注意进位就行了),将每次的计算结果创建一个结点,按照尾插法插入到新的链表尾部即可

代码

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
go复制代码//栈实现
func AddTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
if l1 == nil {
return l2
}
if l2 == nil {
return l1
}
stack1 := []int{}
stack2 := []int{}
//head1, head2 := l1, l2
for l1 != nil {
stack1 = append(stack1, l1.Val)
l1 = l1.Next
}
for l2 != nil {
stack2 = append(stack2, l2.Val)
l2 = l2.Next
}

//头插法创建新的链表
var newHead *ListNode
carry := 0
for len(stack1) > 0 || len(stack2) > 0 || carry != 0{
v1, v2 := 0, 0
if len(stack1) > 0 {
v1 = stack1[len(stack1)-1]
stack1 = stack1[:len(stack1)-1]
}
if len(stack2) > 0 {
v2 = stack2[len(stack2)-1]
stack2 = stack2[:len(stack2)-1]
}
sum := v1 + v2 +carry
carry = sum/10
sum = sum % 10
node := ListNode{sum, nil}
node.Next = newHead
newHead = &node
}
return newHead
}

本文转载自: 掘金

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

【Java入门100例】07各数字的和——取余运算

发表于 2021-11-11

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

又是一年双十一,看到好多博主都在给粉丝搞送书活动,那一条的粉丝也不能苦着,连夜联系出版社,软磨硬泡,终于给大家搞来一些免费书和优惠券。详情


🌲本文收录于专栏《Java入门练习100例》——试用于学完「Java基础语法」后的巩固提高及「LeetCode刷题」前的小试牛刀。

点赞再看,养成习惯。微信搜索【一条coding】关注这个在互联网摸爬滚打的程序员。

本文收录于技术专家修炼,里面有我的学习路线、系列文章、面试题库、自学资料、电子书等。欢迎star⭐️

题目描述

难度:简单

计算给定整数12345的各个位上数字的和。

知识点

  • 除法运算
  • 取余运算

解题思路

解题的关键在于如何拿到各个位上的数字。

举例:拿到34的个位和十位

1
2
3
4
5
java复制代码int a=34;
//整除运算,拿到3
int b=34/10;
//返回余数4
int c=34%10;

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* 计算给定整数12345的各数字的和。
*/
public class question_07 {
public static void main(String args[]) {
int y = 12345;
int r = 0 ;
int sum = 0;
while(y!=0) {
r = y % 10;
sum += r;
y = y / 10;
}
System.out.println("y = " + sum);
}
}

输出结果

总结

熟练掌握取余和整除运算,大有作用。

最后

独脚难行,孤掌难鸣,一个人的力量终究是有限的,一个人的旅途也注定是孤独的。当你定好计划,怀着满腔热血准备出发的时候,一定要找个伙伴,和唐僧西天取经一样,师徒四人团结一心才能通过九九八十一难。
所以,

如果你想学好Java

想进大厂

想拿高薪

想有一群志同道合的伙伴

请加入技术交流

本文转载自: 掘金

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

高频算法面试题(二十一)- 两个链表的第一个公共结点

发表于 2021-11-11

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

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

两个链表的第一个公共结点

题目来源:LeetCode-160. 相交链表

题目描述

输入两个链表,找出它们的第一个公共节点

如下面的两个链表:

1.png

在节点 c1 开始相交

示例

示例 1

2.png

1
2
3
css复制代码输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点

示例 2

image.png

1
2
3
css复制代码输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点

示例 3

image.png

1
2
3
4
css复制代码输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null

提示:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构
  • 可假定整个链表结构中没有循环
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存
  • 本题与主站 160 题相同:leetcode-cn.com/problems/in…

解题

解法一:散列表

思路

这种找公共节点,跟找环的入口点差不多,所以解题的思路也差不多,过程是

  • 先遍历链表l1,并将结点记录到散列表中
  • 再遍历l2,遍历的过程中,判断l2中的结点是否在散列表中,第一个在散列表中存在的结点,就是我们要找的第一个公共节点

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码type ListNode struct {
Val int
Next *ListNode
}
func getIntersectionNode(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
mapNode := make(map[*ListNode]bool)
for ;headA != nil; headA = headA.Next {
mapNode[headA] = true
}
for ;headB != nil;headB = headB.Next {
if _, ok := mapNode[headB]; ok {
return headB
}
}

return nil
}

解法二:双指针

思路

这种感觉很难想到,当花一定的时间还是想不到怎么做的时候,可以去看别人的做法。毕竟有很多解法很难想到,主要是理解思路,知道这种问题,有这种解法,不必过于死磕

首先我们知道,如果两个链表相交,那么它们第一个相交点之后的长度是相等的。只要我们能消除相交之前部分的长度差,当两个指针相等的时候,就是它们的相交点,具体过程如下:

  • 指针pA指向 A 链表,指针 pB 指向 B 链表,依次往后遍历
  • 如果 pA 到了末尾,则让pA指向B链表的头结点( pA = headB )继续遍历
  • 如果 pB 到了末尾,则则让pB指向A链表的头结点( pB = headA )继续遍历
  • 比较长的链表指针指向较短链表head时,长度差就消除了

文字描述很抽象,看图

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码//双指针实现
func getIntersectionNode1(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}

pA, pB := headA, headB
for pA != pB {
if pA == nil {
pA = headB
} else {
pA = pA.Next
}
if pB == nil {
pB = headA
} else {
pB = pB.Next
}
}

return pA
}

本文转载自: 掘金

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

C++-各种回调函数和相关参数写法 回调函数

发表于 2021-11-11

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

回调函数

给一个函数传递一个函数指针,在该函数中调用该指针指向的函数,这个被调用的函数称为回调函数。
在学习了可调用对象的概念以后,我们可以传入各种各样的可调用对象,来实现回调功能。

直接上代码

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
cpp复制代码class TestClass
{
public:
int add(int a, int b) { return a + b; }//public成员函数
};

class TestClass2
{
public:
static int add(int a, int b) { return a + b; }//静态成员函数
};

class TestClass3
{
public:
int operator()(int a, int b) { return a + b; }//重载了调用运算符的类
};

int callFunc(int a, int b, int (*p)(int,int)) {//传入函数指针
return p(a, b);
}

int callFunc2(int a, int b, function<int(int,int)> func) {//传入function类型函数
return func(a, b);
}

int callFunc3(int a, int b, int (TestClass::*p)(int, int)) {//传入特定类的方法
TestClass obj;
return (obj.*p)(a, b);
}

int callFunc4(int a, int b, TestClass3 &callableObj) {//传入可调用对象
return callableObj(a, b);
}

int add(int a, int b) {//普通函数
return a + b;
}

cout << callFunc(2, 3, add) << endl;//传入函数,自动转为函数指针,传入&add也可
cout << callFunc(2, 3, [](int a, int b) {return a + b; }) << endl;//传入lambda表达式
cout << callFunc(2, 3, TestClass2::add) << endl;//传入静态成员成员函数
cout << callFunc2(4, 5, bind(add, _1, _2))<<endl;//传入function<int(int,int)>类型的对象
cout << callFunc3(6, 7, &TestClass::add) << endl;//传入类成员函数
TestClass obj;
cout << callFunc2(8, 9, bind(&TestClass::add,&obj,_1,_2))<<endl;//传入function<int(int,int)>类型的对象
TestClass3 obj3;
cout << callFunc4(10, 11, obj3)<<endl;//传入可调用对象
cout << callFunc2(12, 13, bind(&TestClass3::operator(),&obj3,_1,_2))<<endl;//传入function<int(int,int)>类型的对象

通过以上代码我们可以发现,callFunc2形式的函数接口具有很好的通用性,而我们之所以要用回调函数,就是希望多一点“通用性”,让接口调用者决定传入的函数是什么。

callFunc2可以传入任意一种可调用对象进行回调,其参数function<int(int,int)> func表明了其要求的调用形式或者说函数类型。
并且我们可以发现,如果希望成员函数可以用作回调函数,声明为static是最为方便的,因为这时候不需要提前创建一个对象来进行bind,可以直接当作普通函数传入函数接口。

注:尴尬的是,上面例子我写到一半时,被安全软件提示我写的exe是个病毒,估计和回调函数的某些特征有关,直接误报了。

本文转载自: 掘金

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

Spring Security自定义用户认证

发表于 2021-11-11

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

Spring Security支持我们自定义认证的过程,如处理用户信息获取逻辑,使用我们自定义的登录页面替换Spring Security默认的登录页及自定义登录成功或失败后的处理逻辑等。这里将在上一节的源码基础上进行改造。

自定义认证过程

自定义认证的过程需要实现Spring Security提供的UserDetailService接口,该接口只有一个抽象方法loadUserByUsername,源码如下:

1
2
3
java复制代码public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername方法返回一个UserDetail对象,该对象也是一个接口,包含一些用于描述用户信息的方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public interface UserDetails extends Serializable {

Collection<? extends GrantedAuthority> getAuthorities();

String getPassword();

String getUsername();

boolean isAccountNonExpired();

boolean isAccountNonLocked();

boolean isCredentialsNonExpired();

boolean isEnabled();
}

这些方法的含义如下:

  • getAuthorities获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;
  • getPassword和getUsername用于获取密码和用户名;
  • isAccountNonExpired方法返回boolean类型,用于判断账户是否未过期,未过期返回true反之返回false;
  • isAccountNonLocked方法用于判断账户是否未锁定;
  • isCredentialsNonExpired用于判断用户凭证是否没过期,即密码是否未过期;
  • isEnabled方法用于判断用户是否可用。

实际中我们可以自定义UserDetails接口的实现类,也可以直接使用Spring Security提供的UserDetails接口实现类org.springframework.security.core.userdetails.User。

说了那么多,下面我们来开始实现UserDetailService接口的loadUserByUsername方法。

首先创建一个MyUser对象,用于存放模拟的用户数据(实际中一般从数据库获取,这里为了方便直接模拟):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class MyUser implements Serializable {
private static final long serialVersionUID = 3497935890426858541L;

private String userName;

private String password;

private boolean accountNonExpired = true;

private boolean accountNonLocked= true;

private boolean credentialsNonExpired= true;

private boolean enabled= true;

// get,set略
}

接着创建MyUserDetailService实现UserDetailService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Configuration
public class UserDetailService implements UserDetailsService {

@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模拟一个用户,替代数据库获取逻辑
MyUser user = new MyUser();
user.setUserName(username);
user.setPassword(this.passwordEncoder.encode("123456"));
// 输出加密后的密码
System.out.println(user.getPassword());

return new User(username, user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}

这里我们使用了org.springframework.security.core.userdetails.User类包含7个参数的构造器,其还包含一个三个参数的构造器User(String username, String password,Collection<? extends GrantedAuthority> authorities),由于权限参数不能为空,所以这里先使用AuthorityUtils.commaSeparatedStringToAuthorityList方法模拟一个admin的权限,该方法可以将逗号分隔的字符串转换为权限集合。

此外我们还注入了PasswordEncoder对象,该对象用于密码加密,注入前需要手动配置。我们在BrowserSecurityConfig中配置它:

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
...
}

PasswordEncoder是一个密码加密接口,而BCryptPasswordEncoder是Spring Security提供的一个实现方法,我们也可以自己实现PasswordEncoder。不过Spring Security实现的BCryptPasswordEncoder已经足够强大,它对相同的密码进行加密后可以生成不同的结果。

这时候重启项目,访问http://localhost:8080/login,便可以使用任意用户名以及123456作为密码登录系统。我们多次进行登录操作,可以看到控制台输出的加密后的密码如下:

QQ截图20180712210522.png

可以看到,BCryptPasswordEncoder对相同的密码生成的结果每次都是不一样的。

替换默认登录页

默认的登录页面过于简陋,我们可以自己定义一个登录页面。为了方便起见,我们直接在src/main/resources/resources目录下定义一个login.html(不需要Controller跳转):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
<link rel="stylesheet" href="css/login.css" type="text/css">
</head>
<body>
<form class="login-page" action="/login" method="post">
<div class="form">
<h3>账户登录</h3>
<input type="text" placeholder="用户名" name="username" required="required" />
<input type="password" placeholder="密码" name="password" required="required" />
<button type="submit">登录</button>
</div>
</form>
</body>
</html>

要怎么做才能让Spring Security跳转到我们自己定义的登录页面呢?很简单,只需要在BrowserSecurityConfig的configure中添加一些配置:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// http.httpBasic() // HTTP Basic
.loginPage("/login.html")
.loginProcessingUrl("/login")
.and()
.authorizeRequests() // 授权配置
.antMatchers("/login.html").permitAll()
.anyRequest() // 所有请求
.authenticated(); // 都需要认证
}

上面代码中.loginPage("/login.html")指定了跳转到登录页面的请求URL,.loginProcessingUrl("/login")对应登录页面form表单的action="/login",.antMatchers("/login.html").permitAll()表示跳转到登录页面的请求不被拦截,否则会进入无限循环。

这时候启动系统,访问http://localhost:8080/hello,会看到页面已经被重定向到了http://localhost:8080/login.html:

QQ截图20180713211112.png

输入用户名和密码发现页面报错:

QQ截图20180713212002.png

我们先把CSRF攻击防御关了,修改BrowserSecurityConfig的configure:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// http.httpBasic() // HTTP Basic
.loginPage("/login.html") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.and()
.authorizeRequests() // 授权配置
.antMatchers("/login.html").permitAll() // 登录跳转 URL 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}

重启项目便可正常登录。

假如现在有这样一个需求:在未登录的情况下,当用户访问html资源的时候跳转到登录页,否则返回JSON格式数据,状态码为401。

要实现这个功能我们将loginPage的URL改为/authentication/require,并且在antMatchers方法中加入该URL,让其免拦截:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// http.httpBasic() // HTTP Basic
.loginPage("/authentication/require") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 登录跳转 URL 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}

然后定义一个控制器BrowserSecurityController,处理这个请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@RestController
public class BrowserSecurityController {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

@GetMapping("/authentication/require")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html"))
redirectStrategy.sendRedirect(request, response, "/login.html");
}
return "访问的资源需要身份认证!";
}
}

其中HttpSessionRequestCache为Spring Security提供的用于缓存请求的对象,通过调用它的getRequest方法可以获取到本次请求的HTTP信息。DefaultRedirectStrategy的sendRedirect为Spring Security提供的用于处理重定向的方法。

上面代码获取了引发跳转的请求,根据请求是否以.html为结尾来对应不同的处理方法。如果是以.html结尾,那么重定向到登录页面,否则返回”访问的资源需要身份认证!”信息,并且HTTP状态码为401(HttpStatus.UNAUTHORIZED)。

这样当我们访问http://localhost:8080/hello 的时候页面便会跳转到 http://localhost:8080/authentication/require ,并且输出”访问的资源需要身份认证!”,当我们访问http://localhost:8080/hello.html 的时候,页面将会跳转到登录页面。

处理成功和失败

Spring Security有一套默认的处理登录成功和失败的方法:当用户登录成功时,页面会跳转会引发登录的请求,比如在未登录的情况下访问http://localhost:8080/hello ,页面会跳转到登录页,登录成功后再跳转回来;登录失败时则是跳转到Spring Security默认的错误提示页面。下面我们通过一些自定义配置来替换这套默认的处理机制。

自定义登录成功逻辑

要改变默认的处理成功逻辑很简单,只需要实现org.springframework.security.web.authentication.AuthenticationSuccessHandler接口的onAuthenticationSuccess方法即可:

1
2
3
4
5
6
7
8
9
java复制代码@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(mapper.writeValueAsString(authentication));
}
}

其中Authentication参数既包含了认证请求的一些信息,比如IP,请求的SessionId等,也包含了用户信息,即前面提到的User对象。通过上面这个配置,用户登录成功后页面将打印出Authentication对象的信息。

要使这个配置生效,我们还的在BrowserSecurityConfig的configure中配置它:

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
java复制代码@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSucessHandler authenticationSucessHandler;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// http.httpBasic() // HTTP Basic
.loginPage("/authentication/require") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.successHandler(authenticationSucessHandler) // 处理登录成功
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 登录跳转 URL 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
}

我们将MyAuthenticationSucessHandler注入进来,并通过successHandler方法进行配置。

这时候重启项目登录后页面将会输出如下JSON信息:

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
json复制代码{
"authorities": [
{
"authority": "admin"
}
],
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": "8D50BAF811891F4397E21B4B537F0544"
},
"authenticated": true,
"principal": {
"password": null,
"username": "mrbird",
"authorities": [
{
"authority": "admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "mrbird"
}

像password,credentials这些敏感信息,Spring Security已经将其屏蔽。

除此之外,我们也可以在登录成功后做页面的跳转,修改MyAuthenticationSucessHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

@Autowired
private ObjectMapper mapper;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
}
}

通过上面配置,登录成功后页面将跳转回引发跳转的页面。如果想指定跳转的页面,比如跳转到/index,可以将savedRequest.getRedirectUrl()修改为/index:

1
2
3
4
5
6
7
8
9
10
java复制代码@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
redirectStrategy.sendRedirect(request, response, "/index");
}
}

然后在TestController中定义一个处理该请求的方法:

1
2
3
4
5
6
7
java复制代码@RestController
public class TestController {
@GetMapping("index")
public Object index(){
return SecurityContextHolder.getContext().getAuthentication();
}
}

登录成功后,便可以使用SecurityContextHolder.getContext().getAuthentication()获取到Authentication对象信息。除了通过这种方式获取Authentication对象信息外,也可以使用下面这种方式:

1
2
3
4
5
6
7
java复制代码@RestController
public class TestController {
@GetMapping("index")
public Object index(Authentication authentication) {
return authentication;
}
}

重启项目,登录成功后,页面将跳转到http://localhost:8080/index:

QQ截图20180714103649.png

自定义登录失败逻辑

和自定义登录成功处理逻辑类似,自定义登录失败处理逻辑需要实现org.springframework.security.web.authentication.AuthenticationFailureHandler的onAuthenticationFailure方法:

1
2
3
4
5
6
java复制代码@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
}
}

onAuthenticationFailure方法的AuthenticationException参数是一个抽象类,Spring Security根据登录失败的原因封装了许多对应的实现类,查看AuthenticationException的Hierarchy:

QQ截图20180714104551.png

不同的失败原因对应不同的异常,比如用户名或密码错误对应的是BadCredentialsException,用户不存在对应的是UsernameNotFoundException,用户被锁定对应的是LockedException等。

假如我们需要在登录失败的时候返回失败信息,可以这样处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

@Autowired
private ObjectMapper mapper;

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
}
}

状态码定义为500(HttpStatus.INTERNAL_SERVER_ERROR.value()),即系统内部异常。

同样的,我们需要在BrowserSecurityConfig的configure中配置它:

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复制代码@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSucessHandler authenticationSucessHandler;

@Autowired
private MyAuthenticationFailureHandler authenticationFailureHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// http.httpBasic() // HTTP Basic
.loginPage("/authentication/require") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.successHandler(authenticationSucessHandler) // 处理登录成功
.failureHandler(authenticationFailureHandler) // 处理登录失败
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 登录跳转 URL 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
}

重启项目,当输入错误的密码时,页面输出如下:

QQ截图20180714105620.png

本文转载自: 掘金

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

Java 经典垃圾回收器详解

发表于 2021-11-11

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

垃圾回收器性能指标

  • 吞吐量:程序运行时间占总运行时间(总运行时间=程序运行时间+垃圾回收时间)的比例,垃圾回收时间越少,吞吐量越高;
  • 暂停时间:STW的时间;
  • 内存占用:Java堆所占的大小。

以上三点构成不可能三角,即一款垃圾回收器不可能同时满足三点。随着硬件水平的提升,内存占用不再是我们关注的重点,评估垃圾回收器性能时,重点关注吞吐量和暂停时间。吞吐量和暂停时间是相互矛盾的,目前我们追求的效果是:在最大吞吐量优先的情况下,减小暂停时间。

垃圾回收器发展历史

  • 1999年JDK 1.3.1 发布第一款串行方式的Serial GC,ParNew垃圾回收器是Serial回收器的多线程版本;
  • 2002年2月26,Parallel GC和Concurrent Mark Sweep GC(CMS)跟随JDK 1.4.2一起发布;
  • Parallel GC在JDK 1.6后称为HotSpot默认GC;
  • 2012年,在JDK 1.7u4版本中,G1可用;
  • 2017年,JDK 9中,G1成为默认垃圾回收器,CMS被标记为过时;
  • 2018年3月,JDK 10中提升G1并行性;
  • 2018年9月,JDK 11引入了Epsilon垃圾回收器,同时引入ZGC(实验版本);
  • 2019年3月,JDK 12发布,增强G1,并引入Shenandoah GC(实验版本);
  • 2019年9月,JDK 13发布,增强ZGC;
  • 2020年3月,JDK 14发布,删除CMS,拓展ZGC在MAC和Windows上的应用。

垃圾回收器组合

7款经典垃圾回收器间的组合关系:

gc07.png

说明:

  1. 两个回收器间有连线,说明它们可以搭配使用;
  2. Serial Old作为CMS出现“Concurrent Mode Failure”失败的后备预案;
  3. G1可用于新生代和老年代;
  4. 红色虚线连线:JDK 8将这两组组合声明为废弃,并在JDK 9中完全移除;
  5. 绿色虚线连线:JDK 14中,弃用了该组合;
  6. 绿色虚线边框:JDK 14中,删除了CMS。

默认垃圾回收器查看

编写一段简单的java程序:

1
2
3
4
5
java复制代码public class Test {
public static void main(String[] args) {
System.out.println("hello");
}
}

添加-XX:+PrintCommandLineFlagsJVM参数配置,在JDK 8环境下程序输出:

1
2
java复制代码-XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
hello

-XX:+UseParallelGC说明JDK 8默认的垃圾回收器为Parallel。

在JDK 9环境下输出:

1
2
java复制代码-XX:G1ConcRefinementThreads=10 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC 
hello

-XX:+UseG1GC说明JDK 9默认的垃圾回收器为G1。

经典垃圾回收器介绍

Serial、Serial Old回收器

Serial垃圾回收器为单线程串行回收器,为HotSpot中Client模式下默认的新生代垃圾回收器,采用复制算法、串行回收和STW机制进行内存回收;

Serial Old垃圾回收器为Serial提供的老年代垃圾回收器,采用标记压缩算法、串行回收和STW机制进行内存回收:

  • Serial Old是运行在Client模式下默认的老年代垃圾回收器;
  • Serial Old在Server模式下主要有两个用途:与新生代的Parallel Scavenge配合使用;作为老年代CMS回收器的后备垃圾收集方案。

Serial适用于运行在Client模式下的虚拟机或者内存不大(几十MB到一两百MB)的环境下,因为是串行的,有较长时间的STW,所以并不适用于要求快响应、交互较强的应用。

可以通过XX:+UseSerialGC参数启用Serial回收器,表示新生代使用Serial,老年代使用Serial Old。

ParNew回收器

ParNew是Parallel New两个词的简写,是Serial的多线程版本垃圾回收器。ParNew是很多JVM运行在Server模式下新生代的默认垃圾回收器,采用复制算法,并行回收和STW机制进行内存回收。

可以通过XX:+UseParNewGC参数启用ParNew回收器,表示新生代使用ParNew,老年代不受影响。

Serial、ParNew搭配Serial Old回收器示意图:

gc08.jpg

图片来自于codertw.com/%E7%A8%8B%E…

Parallel、Parallel Old回收器

Parallel Scavenge回收器也是作用于新生代,同样采用复制算法,并行回收和STW机制。

Parallel Scavenge和ParNew对比:

  • Parallel Scavenge为吞吐量优先的垃圾回收器;
  • Parallel Scavenge具有自适应调节策略。

JDK 1.6提供了用于老年代的并行垃圾回收器 —— Parallel Old回收器,用于替代Serial Old回收器。Parallel采用标记压缩、并行回收和STW机制。

可以通过-XX:+UseParallelGC指定新生代使用Parallel Scavenge回收器;-XX:+UseParallelOldGC指定老年代使用Parallel Old回收器,它们是成对存在的,开启一个另一个也会开启。

此外还可以通过-XX:ParallelGCThreads=设置并行回收器的线程数:

  • 默认情况下,当CPU数量小于8个时,-XX:ParallelGCThreads=的值等于CPU数量;
  • 当CPU数量大于8个,-XX:ParallelGCThreads=的值等于3+5*CPU_COUNT/8。

-XX:+UseAdaptiveSizePolicy开启Parallel Scavenge的自适应调节策略:

  • 该模式下,年轻代大小、伊甸园区和幸存者区的比例、晋升老年代的对象年龄阈值都会自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。

CMS回收器

JDK 1.5 HotSpot推出了一款真正意义上的并发回收器 —— CMS(Concurrent-Mark-Sweep),第一次实现了让垃圾回收线程和用户线程同时工作。CMS的关注点在于尽可能缩短垃圾收集时用户线程停顿的时间。

CMS作为一款老年代的垃圾回收器,不能和新生代垃圾回收器Parallel Scavenge搭配使用,只能和ParNew或者Serial搭配使用。

CMS回收器示意图:

gc09.png

图片来自于codertw.com/%E7%A8%8B%E…

主要分为以下几个步骤:

  1. 初始标记(Initial-Mark):所有用户线程暂停(STW),这个阶段仅仅标记出GC Roots能直接关联到的对象,所以速度非常快,STW时间很短;
  2. 并发标记(Concurrent-Mark):该阶段从GC Roots直接关联对象开始遍历整个对象链,虽然这个过程耗时较长,但并不需要暂停用户线程,并发执行,没有STW;
  3. 重新标记(Remark):由于上一步用户线程也在执行,所以这一步用于修正因用户线程继续运行而导致标记发生变动的那一部分对象的标记记录。这个阶段会比初始标记阶段耗时长一点,但远比并发标记阶段低;
  4. 并发清除(Concurrent-Sweep):该阶段清理删除垃圾,回收空间。由于没有移动对象,所以该阶段也不需要STW。

CMS的优缺点都很明显:

优点:

  • 并发收集;
  • 低延迟。

缺点:

  • 会产生碎片。因为清理阶段用户线线程还在执行,所以只能采用不移动对象的标记-清除算法,而该算法会产生碎片问题;
  • 对CPU资源敏感。CPU资源除了用于用户线程外,还需分配一部分用于处理垃圾回收,降低了吞吐量;
  • 无法处理浮动垃圾。并发标记阶段,用户线程并未停止,该阶段也会产生垃圾, CMS无法对这些垃圾进行标记,只能留到下次GC时处理。

此外,CMS在回收过程中,因为用户线程并没有中断,所以还需确保用户线程有足够的内存可用。换句话说,CMS回收器不能等老年代即将被填满时才去回收,而应当堆内存使用率到达一定阈值时,便开始进行回收。如果CMS运行期间预留内存不足,就会出现一次“Concurrent Mode Failure”失败,虚拟机会启动后备方案,临时启用Serial Old回收器来完成老年代的垃圾回收。

CMS回收器可设置参数:

  • -XX:+UseConcMarkSweepGC,开启CMS GC,开启后,-XX:+UseParNewGC会自动打开;
  • -XX:CMSInitiatingOccupanyFraction=,设置堆内存使用率阈值,一旦达到这个阈值,CMS开始进行回收(JDK5及之前,默认值为68,JDK6及以上版本默认值为92%);
  • -XX:+UseCMSCompactAtFullCollection,指定在CMS回收完老年代后,对内存空间进行压缩处理,以避免碎片化问题;
  • -XX:CMSFullGCsBeforeCompaction,设置执行多少次CMS GC后,对内存空间进行压缩整理;
  • -XX:ParallelCMSThreads=,设置CMS的线程数。默认启动的线程数为(ParallelGCThreads+3)/4。我们知道,当CPU个数小于8时,ParallelGCThreads的默认值为CPU个数,所以对于一个8核CPU,默认启动的CMS线程数为3,换句话说只有62.5%的CPU资源用于处理用户线程。所以CMS不适合吞吐量要求高的场景。

G1回收器

G1(Garbage First)回收器把堆内存分割成很多不相关的区域(region,物理上不连续),使用不同区域来表示伊甸园区,幸存者区和老年代。

G1会避免对整个Java堆进行垃圾收集,它会跟踪各个region里垃圾回收的价值大小(回收所获得的空间大小及所需时间的经验值),在后台维护一个优先列表,每次根据允许收集时间,优先回收价值最大的region。

region的说明

gc10.png

图片来自于tech.meituan.com/2016/09/23/…

  • E表示伊甸园区,S表示幸存者区、O表示老年代,空白表示未使用的内存区域;
  • 一个region在同一时间内只能属于一种角色;
  • G1新增了一个全新的内存区域——Humongous,主要用于存放大对象。

G1回收垃圾过程如下图所示:

gc11.png

图片来自于codertw.com/%E7%A8%8B%E…

主要分为以下几个步骤:

  1. 初始标记:仅仅是标记GC Roots能直接关联的对象,需要STW,但这个过程非常快;
  2. 并发标记:从GC Roots出发,对堆中对象进行可达性分析,找出存活对象,该阶段耗时较长,但是可与用户线程并发执行;
  3. 最终标记:主要修正在并发标记阶段因为用户线程继续运行而导致标记记录产生变动的那一部分对象的标记记录,需要STW;
  4. 筛选回收:将各个region分区的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划。这阶段停顿用户线程,STW。

G1回收器的优缺点:

优点:

  • 并行与并发;
  • 分代收集,可以采用不同的算法处理不同的对象;
  • 空间整合,标记压缩算法意味着不会产生内存碎片;
  • 可预测的停顿时间,能让使用者明确指定一个长度为M毫秒时间片段内,消耗在垃圾回收的时间不超过N毫秒(根据优先列表优先回收价值最大的region)。

缺点:

  • 在小内存环境下和CMS相比没有优势,G1适合大的堆内存;
  • 在用户程序运行过程中,G1无论是为了垃圾回收产生的内存占用,还是程序运行时的额外执行负载都要比CMS高。

G1回收器相关参数设置:

  • -XX:+UseG1GC,开启G1 GC;
  • -XX:G1HeapRegionSize=,设置region的大小。值为2的幂,范围是1MB到32MB之间,目标是根据最小堆内存大小划分出约2048个区域。所以如果这个值设置为2MB,那么堆最小内存大约为4GB;
  • -XX:MaxGCPauseMillis=,设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值为200ms;
  • -XX:ParallelGCThread=,设置STW时GC线程数值,最多设置为8;
  • -XX:ConcGCThreads=,设置并发标记的线程数,推荐值为ParallelGCThread的1/4左右;
  • -XX:InitiatingHeapOccupancyPercent=,设置触发并发GC周期的Java堆占用率阈值,超过这个值就触发GC,默认值为45。

总结

上面这几款经典的垃圾回收器各有特点,具体使用的时候需要根据具体的情况选用不同的垃圾回收器:

gc12.png

垃圾回收器 分类 作用位置 使用算法 特点 适用场景
Serial 串行 新生代 复制算法 响应速度优先 适用于单CPU环境下的Client模式
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境Server模式下与CMS配合使用
Parallel 并行 新生代 复制算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
Serial Old 串行 老年代 标记-压缩算法 响应速度优先 单CPU环境下的Client模式
Parallel Old 并行 老年代 标记-压缩算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
CMS 并发 老年代 标记-压缩算法 响应速度优先 适用于互联网或B/S业务
G1 并行与并发 新生代、老年代 复制算法 标记-压缩算法 响应速度优先 面向服务端应用

新垃圾回收器

Epsilon回收器、Shenandoah回收器、ZGC回收器

本文转载自: 掘金

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

两个 99% 的人都遇到过的 Kubernetes 故障处理

发表于 2021-11-11

随着微服务的不断推进,使用 k8s 集群越来越多,越来越深入,随之而来会遇到一系列的问题,本文向大家介绍实际使用 k8s 遇到的一些问题以及解决方法。

1问题一:修复 K8S 内存泄露问题

问题描述

当 k8s 集群运行日久以后,有的 node 无法再新建 pod,并且出现如下错误,当重启服务器之后,才可以恢复正常使用。查看 pod 状态的时候会出现以下报错。

1
perl复制代码applying cgroup … caused: mkdir …no space left on device

或者在 describe pod 的时候出现 cannot allocate memory。

这时候你的 k8s 集群可能就存在内存泄露的问题了,当创建的 pod 越多的时候内存会泄露的越多,越快。

具体查看是否存在内存泄露

1
shell复制代码$ cat /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo

当出现 cat: /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo: Input/output error 则说明不存在内存泄露的情况 如果存在内存泄露会出现

1
2
arduino复制代码slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>

解决方案

解决方法思路:关闭 runc 和 kubelet 的 kmem,因为升级内核的方案改动较大,此处不采用。

kmem 导致内存泄露的原因:

内核对于每个 cgroup 子系统的的条目数是有限制的,限制的大小定义在 kernel/cgroup.c #L139,当正常在 cgroup 创建一个 group 的目录时,条目数就加 1。我们遇到的情况就是因为开启了 kmem accounting 功能,虽然 cgroup 的目录删除了,但是条目没有回收。这样后面就无法创建 65535 个 cgroup 了。也就是说,在当前内核版本下,开启了 kmem accounting 功能,会导致 memory cgroup 的条目泄漏无法回收。

2.1 编译 runc

配置 go 语言环境

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码$ wget https://dl.google.com/go/go1.12.9.linux-amd64.tar.gz
$ tar xf go1.12.9.linux-amd64.tar.gz -C /usr/local/

# 写入bashrc
$ vim ~/.bashrc
$ export GOPATH="/data/Documents"
$ export GOROOT="/usr/local/go"
$ export PATH="$GOROOT/bin:$GOPATH/bin:$PATH"
$ export GO111MODULE=off

# 验证
$ source ~/.bashrc
$ go env

下载 runc 源码

1
2
3
4
5
shell复制代码$ mkdir -p /data/Documents/src/github.com/opencontainers/
$ cd /data/Documents/src/github.com/opencontainers/
$ git clone https://github.com/opencontainers/runc
$ cd runc/
$ git checkout v1.0.0-rc9 # 切到v1.0.0-rc9 tag

编译

1
2
3
4
ini复制代码# 安装编译组件
$ sudo yum install libseccomp-devel
$ make BUILDTAGS='seccomp nokmem'
# 编译完成之后会在当前目录下看到一个runc的可执行文件,等kubelet编译完成之后会将其替换

2.2 编译 kubelet

下载 kubernetes 源码

1
2
3
4
5
shell复制代码$ mkdir -p /root/k8s/
$ cd /root/k8s/
$ git clone https://github.com/kubernetes/kubernetes
$ cd kubernetes/
$ git checkout v1.15.3

制作编译环境的镜像(Dockerfile 如下)

1
2
3
4
5
6
7
8
bash复制代码FROM centos:centos7.3.1611

ENV GOROOT /usr/local/go
ENV GOPATH /usr/local/gopath
ENV PATH /usr/local/go/bin:$PATH

RUN yum install rpm-build which where rsync gcc gcc-c++ automake autoconf libtool make -y \
&& curl -L https://studygolang.com/dl/golang/go1.12.9.linux-amd64.tar.gz | tar zxvf - -C /usr/local

在制作好的 go 环境镜像中来进行编译 kubelet

1
2
3
4
shell复制代码$ docker run  -it --rm   -v /root/k8s/kubernetes:/usr/local/gopath/src/k8s.io/kubernetes   build-k8s:centos-7.3-go-1.12.9-k8s-1.15.3   bash
$ cd /usr/local/gopath/src/k8s.io/kubernetes
#编译
$ GO111MODULE=off KUBE_GIT_TREE_STATE=clean KUBE_GIT_VERSION=v1.15.3 make kubelet GOFLAGS="-tags=nokmem"

3.替换原有的 runc 和 kubelet

将原有 runc 和 kubelet 备份

1
2
shell复制代码$ mv /usr/bin/kubelet /home/kubelet
$ mv /usr/bin/docker-runc /home/docker-runc

停止 docker 和 kubelet

1
2
arduino复制代码$ systemctl stop docker
$ systemctl stop kubelet

将编译好的 runc 和 kubelet 进行替换

1
2
3
shell复制代码$ cp kubelet /usr/bin/kubelet
$ cp kubelet /usr/local/bin/kubelet
$ cp runc /usr/bin/docker-runc

检查 kmem 是否关闭前需要将此节点的 pod 杀掉重启或者重启服务器,当结果为 0 时成功

1
shell复制代码$ cat /sys/fs/cgroup/memory/kubepods/burstable/memory.kmem.usage_in_bytes

检查是否还存在内存泄露的情况

1
shell复制代码$ cat /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo

2问题二:k8s 证书过期问题的两种处理方法

前情提要

公司测试环境的 k8s 集群使用已经很长时间了,突然有一天开发联系我说 k8s 集群无法访问,开始以为是测试环境的机器磁盘空间不够了,导致组件异常或者把开发使用的镜像自动清理掉了,但是当登上机器去查验的时候发现不是这个原因。当时觉得也很疑惑。因为开发环境使用人数较少,不应该会出问题,所以就去查验 log 的相关报错信息。

问题现象

出现 k8s api 无法调取的现象,使用 kubectl 命令获取资源均返回如下报错:

1
vbscript复制代码$ Unable to connect to the server: x509: certificate has expired or is not yet valid

经网上搜索之后发现应该是 k8s 集群的证书过期了,使用命令排查证书的过期时间

$ kubeadm alpha certs check-expiration

发现确实是证书过期了

相关介绍以及问题解决

因为我们是使用 kubeadm 部署的 k8s 集群,所以更新起证书也是比较方便的,默认的证书时间有效期是一年,我们集群的 k8s 版本是 1.15.3 版本是可以使用以下命令来更新证书的,但是一年之后还是会到期,这样就很麻烦,所以我们需要了解一下 k8s 的证书,然后我们来生成一个时间很长的证书,这样我们就可以不用去总更新证书了。

1
2
3
4
css复制代码$ kubeadm alpha certs renew all --config=kubeadm.yaml
$ systemctl restart kubelet
$ kubeadm init phase kubeconfig all --config kubeadm.yaml
# 然后将生成的配置文件替换,重启 kube-apiserver、kube-controller、kube-scheduler、etcd 这4个容器即可

另外 kubeadm 会在控制面板升级的时候自动更新所有证书,所以使用 kubeadm 搭建的集群最佳的做法是经常升级集群,这样可以确保你的集群保持最新状态并保持合理的安全性。但是对于实际的生产环境我们可能并不会去频繁的升级集群,所以这个时候我们就需要去手动更新证书。

下面我们通过调用 k8s 的 api 来实现更新一个 10 年的证书

首先在 /etc/kubernetes/manifests/kube-controller-manager.yaml 文件加入配置

1
2
3
4
5
6
7
ini复制代码spec:
containers:
- command:
- kube-controller-manager
# 设置证书有效期为 10年
- --experimental-cluster-signing-duration=87600h
- --client-ca-file=/etc/kubernetes/pki/ca.crt

修改完成后 kube-controller-manager 会自动重启生效。然后我们需要使用下面的命令为 Kubernetes 证书 API 创建一个证书签名请求。如果您设置例如 cert-manager 等外部签名者,则会自动批准证书签名请求(CSRs)。否者,您必须使用 kubectl certificate 命令手动批准证书。以下 kubeadm 命令输出要批准的证书名称,然后等待批准发生:

1
2
css复制代码# 需要将全部 pending 的证书全部批准
$ kubeadm alpha certs renew all --use-api --config kubeadm.yaml &

我们还不能直接重启控制面板的几个组件,这是因为使用 kubeadm 安装的集群对应的 etcd 默认是使用的 /etc/kubernetes/pki/etcd/ca.crt 这个证书进行前面的,而上面我们用命令 kubectl certificate approve 批准过后的证书是使用的默认的 /etc/kubernetes/pki/ca.crt 证书进行签发的,所以我们需要替换 etcd 中的 ca 机构证书:

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
yaml复制代码# 先拷贝静态 Pod 资源清单
$ cp -r /etc/kubernetes/manifests/ /etc/kubernetes/manifests.bak
$ vi /etc/kubernetes/manifests/etcd.yaml
......
spec:
containers:
- command:
- etcd
# 修改为 CA 文件
- --peer-trusted-ca-file=/etc/kubernetes/pki/ca.crt
- --trusted-ca-file=/etc/kubernetes/pki/ca.crt
......
volumeMounts:
- mountPath: /var/lib/etcd
name: etcd-data
- mountPath: /etc/kubernetes/pki # 更改证书目录
name: etcd-certs
volumes:
- hostPath:
path: /etc/kubernetes/pki # 将 pki 目录挂载到 etcd 中去
type: DirectoryOrCreate
name: etcd-certs
- hostPath:
path: /var/lib/etcd
type: DirectoryOrCreate
name: etcd-data
......

由于 kube-apiserver 要连接 etcd 集群,所以也需要重新修改对应的 etcd ca 文件:

1
2
3
4
5
6
7
8
9
bash复制代码$ vi /etc/kubernetes/manifests/kube-apiserver.yaml
......
spec:
containers:
- command:
- kube-apiserver
# 将etcd ca文件修改为默认的ca.crt文件
- --etcd-cafile=/etc/kubernetes/pki/ca.crt
......

除此之外还需要替换 requestheader-client-ca-file 文件,默认是 /etc/kubernetes/pki/front-proxy-ca.crt 文件,现在也需要替换成默认的 CA 文件,否则使用聚合 API,比如安装了 metrics-server 后执行 kubectl top 命令就会报错:

1
2
shell复制代码$ cp /etc/kubernetes/pki/ca.crt /etc/kubernetes/pki/front-proxy-ca.crt
$ cp /etc/kubernetes/pki/ca.key /etc/kubernetes/pki/front-proxy-ca.key

这样我们就得到了一个 10 年证书的 k8s 集群,还可以通过重新编译 kubeadm 来实现一个 10 年证书的,这个我没有尝试,不过在初始化集群的时候也是一个方法。

本文转载自: 掘金

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

1…372373374…956

开发者博客

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