wks藏在这里

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 日程表

  • 站点地图

  • 公益 404

vertx初探

发表于 2019-11-17

背景

vertx是一个我去年看完netty就一直想看看的工具,但因为拖延加懒,最近才看了看文档写了写demo, 算是对它有了一点点了解,之所以想写一点,其实是想自己给自己总结下vertx的一些核心的概念。

vertx core

vertx core 提供了一些 vertx的基本操作,如经常用到的

  1. 编写TCP客户端和服务器
  2. 编写HTTP客户端和服务器
  3. EventBus
  4. file操作
  5. HA
  6. 集群

先上一段代码看下vertx创建一个httpServer:

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
        //创建一个vertx实例
VertxOptions vo = new VertxOptions();
vo.setEventLoopPoolSize( 1);
Vertx vertx = Vertx.vertx(vo);

vertx.deployVerticle(MyFirstVerticle.class.getName());
DeploymentOptions().setInstances(2)

//MyFirstVerticle.java
public class MyFirstVerticle extends AbstractVerticle {
public void start() {

vertx.createHttpServer().requestHandler(req -> {
System.out.println(Thread.currentThread());
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
req.response()
.putHeader("content-type", "text/plain")
.end("Hello World!");
this.deploymentID();
Context c=vertx.getOrCreateContext();
}).listen(8080);

}
}

vertx实例是最核心的一个对象 是宁做几乎一切事情的基础,包括创建客户端和服务器、获取事件总线的引用、设置定时器等等。
是不是很想说一句,嗨,就这,不就nodejs吗

EventLoop

EventLoop算是vertx的基本模型了,简单的讲就是所有的操作都是以 触发 的方式来进行,将IO的操作完全交给vertx,开发者真正要关心的是IO各个阶段的 事件 ,讲的朴素一点就像是js的回调函数一样。
个人觉得vertx的EventLoop 基本等同于Netty的模型,如果真要探索起来,怕是要从Linux的多路复用,select函数,java的NIO,和netty一路将过来了,所以尝试用画图的方式来更形象的描绘:
eventLoop

其实EventLoop 就是一条线程,上面挂的每一个channel就是一个IO连接,底层利用的就是IO多路复用加select 或poll,epoll,来保证每一个线程可以保证控制很多个IO连接,而每一个连接都会挂一个handler,来处理这个连接的 每一个事件 例如:init,connected,read.writer,close。
这个模型的左半部分同样适用于netty。右半部分有一个workPool是一个线程池,是vertx新增的东西,是用来专门处理耗时操作的,如 file,阻塞的DB操作,为什么要加这个概念哪,因为不要阻塞EventLoop是NIO的基本守则。
阻塞操作操作代码如下:

1
2
3
4
5
6
7
8
9
10
executor.executeBlocking(future -> {
System.out.println("Action Thread"+Thread.currentThread());
// 调用一些需要耗费显著执行时间返回结果的阻塞式API
String result = blockingMethod("hello");
future.complete(result);
}, res -> {
System.out.println("Handler Thread"+Thread.currentThread());
System.out.println("The result is: " + res.result());
executor.close();
});

verticle

verticle 其实一直是让我困惑的一个概念,因为vertx的主要运行,基本都是围绕vertx实例来进行的,后面我为verticle找到了一个合理的角色,叫他为vertx实例的一个发布单元,什么是一个 发布单元哪,举个例子,一个HttpServer的发布是一个发布单元。
verticle具有以下几个特点:

  1. 每个verticle占用一个EventLoop线程,且只对应一个EventLoop
  2. 每个verticle中创建的HttpServer,EventBus等等,都会在这个verticle回收时同步回收
  3. 在多个verticle中创建同样端口的httpServer,不会有错误,会变成两个EventLoop线程处理同一个HttpServer的连接,所以多核机器,肯定是需要设置多个verticle的实例来加强性能的。

可以这么想,vertx实例就是一台服务器,而verticle就是上面跑的进程。

EventBus

EventBus 是沟通verticle的桥梁,且可沟通集群中不同vertx实例的verticle,操作很简单。这个似乎概念很简单,就是队列呗,上段代码看看:

1
2
3
4
5
6
7
8
9
10
11
12
//创建一个EventBus
EventBus eb = vertx.eventBus();

req.bodyHandler(totalBuffer -> {
eb.send("news.uk.sport", totalBuffer.toString("UTF-8"));
});

//消费
EventBus eb = vertx.eventBus();
eb.consumer("news.uk.sport", message -> {
System.out.println("前台传入的内容:" + message.body()+""+this);
});

模型如图:
eventBus

vertx web

vertx core已经提供了基本的HttpServer的操作,但实际上功能远远不够正常的开发,所以vertx web作为一个拓展包,是web开发必须的。他提供了一个基本概念router,来进行各种匹配,使得请求能进入正确的handler,其实就是有点springMvc的各种路径匹配。使用起来代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HttpServer server = vertx.createHttpServer();


Router router = Router.router(vertx);

router.route("/some/path/").handler(routingContext -> {

HttpServerResponse response = routingContext.response();
// 如果不能一次输出所有的response,就需要设置为true
response.setChunked(true);

response.write("route1\n");

});
server.requestHandler(router).listen(8080);

web包括的功能很多,有各种匹配,content-type匹配,路径规则匹配,CROS,错误处理,文件传输 等等

vertx fileSystem

vretx中的文件操作主要采用了javaNio包中的文件操作,通过看源码我们可以发现文件操作也是运行在workPool中的,看下代码:

1
2
3
4
5
6
7
8
9
fs.copy("C:\\Users\\Administrator\\Desktop\\Untitled-4.txt", "C:\\Users\\Administrator\\Desktop\\ss.txt", res -> {
System.out.println("file copy handle" + System.currentTimeMillis());
System.out.println("file copy handle" + Thread.currentThread());
if (res.succeeded()) {
System.out.println("success");
} else {
System.out.println("error");
}
})

源码:

1
2
3
4
5
6
7
8
//FileSystemImpl.java

/**
* Run the blocking action using a thread from the worker pool.
*/
public void run() {
context.executeBlockingInternal(this, handler);
}

vertx DB

vertx其实提供了 两种数据库操作。一种是正常的jdbc操作,一种是异步非阻塞的数据库操作,但是只限于PG和mysql。

jdbc操作

看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JDBCClient client = JDBCClient.createShared(vertx, new JsonObject()
.put("url", "jdbc:postgresql://localhost:5432/postgres")
.put("driver_class", "org.postgresql.Driver")
.put("max_pool_size", 30).put("user","postgres").put("password","postgres"));

client.getConnection(res -> {
if (res.succeeded()) {
SQLConnection connection = res.result();

connection.query("SELECT * FROM userb", res2 -> {
if (res2.succeeded()) {
ResultSet rs = res2.result();
System.out.println(rs.toJson());
}
});
} else {
System.out.println("连接失败"); }
});

点进去发现:其实做的还是阻塞操作,

1
2
3
4
5
6
//AbstractJDBCAction.java
public void execute(Connection conn, TaskQueue statementsQueue, Handler<AsyncResult<T>> resultHandler) {
this.ctx.executeBlocking((future) -> {
this.handle(conn, future);
}, statementsQueue, resultHandler);
}

异步数据库操作

这我一直很好奇的,因为jdbc在天然上就是阻塞的,所以要想做到数据库的异步,就得放弃厂商提供的driver,自己做一套编解码,看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PgConnectOptions connectOptions = new PgConnectOptions()
.setPort(5432)
.setHost("localhost")
.setDatabase("postgres")
.setUser("postgres")
.setPassword("postgres");

// Pool options
PoolOptions poolOptions = new PoolOptions()
.setMaxSize(5);

// Create the client pool
PgPool client = PgPool.pool(vertx, connectOptions, poolOptions);

client.query("SELECT * FROM userb ", ar -> {
if (ar.succeeded()) {
RowSet<Row> result = ar.result();
result.forEach((r) -> response.write((String)r.getValue("name")));
response.end();

} else {
System.out.println("Failure: " + ar.cause().getMessage();
}
});

这样就能做到数据库的异步操作,且可以包含事务的控制。简直丝滑

关于数据库异步的思考

数据库异步看起来十分黑科技,但是我们应该明白的是,异步非阻塞的NIO最大的优点就在于利用很少的线程来控制更多的IO连接,这使得在超多连接,IO密集的操作中有更好的表现,但是数据库连接本来一个java系统就不会占用几个,记得以前看过一个文章讲连接数设置最好=CPUcore2磁盘数,那么数据库的NIO在一般的业务系统中,似乎并不必要。
插一个知乎上大佬的回答:

https://www.zhihu.com/question/23084473

vertx模型图

模型
简单明了

vertx的缺点

说到缺点,明显就是回调地狱了,主要还是看过 python的协程,所以难免拿来做个比较,

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
摘抄自廖雪峰的python教程
import asyncio

from aiohttp import web

async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')

async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))

async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv

loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()

可以看到协程 可以将本该做回调的response handler变成了更符合编程习惯的方式,还是比较舒服的。

讲点废话

本来还想把demo放到github上来给像我一样懒的人直接clone下来跑的,但是确实都是官网的例子,放上去太羞耻了。
这文章写的很纠结,写详细了东西太多,写总结性的东西吧,太抽象,没看过的觉得一脸懵,懂得觉得写的没啥意义。
就这吧,如果有大佬想和我讨论或是哪里写的不对,请积极发炎。

java加密

发表于 2019-07-22 | 更新于 2019-08-05

证书概述

证书主要包括颁发者和被办法者的信息,以及被颁发者的公钥,和CA机构对这些信息的认证,
主要内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
**版本**  
识别用于该证书的 X.509 标准的版本,这可以影响证书中所能指定的信息。迄今为止,已定义的版本有三个。
**序列号**
发放证书的实体有责任为证书指定序列号,以使其区别于该实体发放的其它证书。此信息用途很多。例如,如果某一证书被撤消,其序列号将放到证书撤消清单 (CRL) 中。
**签名算法标识符**
用于识别 CA 签写证书时所用的算法。
**签发人姓名**
签写证书的实体的 X.500 名称。它通常为一个 CA。 使用该证书意味着信任签写该证书的实体(注意:有些情况下(例如根或顶层 CA 证书),签发人会签写自己的证书)。
**有效期**
每个证书均只能在一个有限的时间段内有效。该有效期以起始日期和时间及终止日期和时间表示,可以短至几秒或长至一世纪。所选有效期取决于许多因素,例如用于签写证书的私钥的使用频率及愿为证书支付的金钱等。它是在没有危及相关私钥的条件下,实体可以依赖公钥值的预计时间。
**主体名**
证书可以识别其公钥的实体名。此名称使用 X.500 标准,因此在Internet中应是唯一的。它是实体的特征名 (DN),例如,
CN=Java Duke,OU=Java Software Division,O=Sun Microsystems Inc,C=US
(这些指主体的通用名、组织单位、组织和国家)。
**主体公钥信息**
这是被命名实体的公钥,同时包括指定该密钥所属公钥密码系统的算法标识符及所有相关的密钥参数。

PEM DER 只是编码方式,注意并不指定是证书的编码方式,也可以是密钥的编码方式

各种名词和文件后缀

  1. X.509 - 这是一种证书标准,主要定义了证书中应该包含哪些内容.

  2. 两种编码方式:

  • PEM - Privacy Enhanced Mail,打开看文本格式,以”—–BEGIN…”开头, “—–END…”结尾,内容是BASE64编码.
    查看PEM格式证书的信息:openssl x509 -in certificate.pem -text -noout
    Apache和*NIX服务器偏向于使用这种编码格式.

  • DER - Distinguished Encoding Rules,打开看是二进制格式,不可读.
    查看DER格式证书的信息:openssl x509 -in certificate.der -inform der -text -noout
    Java和Windows服务器偏向于使用这种编码格式.

  1. 各种文件拓展名:
  • CRT - CRT应该是certificate的三个字母,其实还是证书的意思,常见于*NIX系统,有可能是PEM编码,也有可能是DER编码,大多数应该是PEM编码
  • CER - 还是certificate,还是证书,常见于Windows系统,同样的,可能是PEM编码,也可能是DER编码,大多数应该是DER编码.
  • KEY - 通常用来存放一个公钥或者私钥,并非X.509证书,编码同样的,可能是PEM,也可能是DER.

  • CSR - Certificate Signing Request,即证书签名请求,这个并不是证书,而是向权威证书颁发机构获得签名证书的申请,其核心内容是一个公钥(当然还附带了一些别的信息),在生成这个申请的时候,同时也会生成一个私钥

  • PFX/P12 - predecessor of PKCS#12,对*nix服务器来说,一般CRT和KEY是分开存放在不同文件中的,但Windows的IIS则将它们存在一个PFX文件中,(因此这个文件包含了证书及私钥)这样会不会不安全?应该不会,PFX通常会有一个”提取密码”

总结起来:crt cer约等于x509证书,key保存公钥或私钥,csr是证书签名请求,pfx包含证书和私钥

  1. PKCS系列
    当我发现还有PKCS系列时,我很凌乱,
    PKCS系列是 Public-Key Cryptography Standards ,是RSA制定的一系列的标准,注意前面写的什么文件后缀啥的,都不算是标准,只有X509和PKCS可以称为标准,
    PKCS中经常使用的就是:PKCS1 PKCS8 PKCS12
  • PKCS#1:定义RSA公开密钥算法加密和签名机制,主要用于组织PKCS#7中所描述的数字签名和数字信封。

  • PKCS#8:描述私有密钥信息格式,该信息包括公开密钥算法的私有密钥以及可选的属性集等。注意pkcs8不只是能表示RSA,所以比PKCS1更具有通用性

  • PKCS#12:描述个人信息交换语法标准。描述了将用户公钥、私钥、证书和其他相关信息打包的语法。

总结:PKCS1,8,12都可以在某些情况下当作文件格式,PKCS1描述基础的密钥格式,PKCS8也描述密钥,但格式和1不同,PKCS12等价于PFX文件,包含证书和私钥

openSSL

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
# 生成PKCS#1的公私钥
openssl genrsa -out pkcs1_private.pem 1024
openssl rsa -in pkcs1_private.pem -RSAPublicKey_out -out pkcs1_public.pem

查看私钥
openssl rsa -in rsa_private_key.pem -text -noout
查看公钥
openssl rsa -pubin -in rsa_public_key.pem -text

# 由PKCS#1的私钥,生成PKCS#8的公私钥
openssl pkcs8 -topk8 -inform PEM -in pkcs1_private.pem -outform PEM -nocrypt -out from_pkcs1_private_to_pkcs8_private.pem

openssl rsa -in pkcs1_private.pem -pubout -out from_pkcs1_private_to_pkcs8_public.pem

# 由PKCS#8的私钥,生成PKCS#1的公私钥
openssl rsa -in from_pkcs1_private_to_pkcs8_private.pem -out from_pkcs8_private_to_pkcs1_private.pem

openssl rsa -in from_pkcs1_private_to_pkcs8_private.pem -RSAPublicKey_out -out from_pkcs8_private_to_pkcs1_public.pem

# 由PKCS1公钥生成PKCS#8公钥:
openssl rsa -RSAPublicKey_in -in pkcs1_public.pem -pubout -out from_pkcs1_public_to_pkcs8_public.pem

# 由PKCS8公钥生成PKCS#1公钥:
openssl rsa -pubin -in from_pkcs1_private_to_pkcs8_public.pem -RSAPublicKey_out -out from_pkcs8_public_to_pkcs1_public.pem

产生证书请求 注意PKCS1 8都可以
openssl req -new -key private_key.pem -out rsaCerReq.csr

产生证书 注意PKCS1 8都可以
openssl x509 -req -days 3650 -in rsaCerReq.csr -signkey private_key.pem -out rsaCert.crt

从证书获得公钥:
openssl x509 -in rsaCert.crt -noout -pubkey > public_key.pem

生成PKCS12
openssl pkcs12 -export -inkey serverprikey.pem -in server.pem -password pass:"123456" -out server_nocret.pfx


从PKCS12获得证书和私钥
openssl pkcs12 -in server_nocret.pfx -nocerts -nodes -out alicekey.pem

openssl pkcs12 -in server_nocret.pfx -nokeys -out cert.pem

查看pkcs12内容 -nodes:因为私钥在在输出前会输出加密结果,所以需要nodes来保证不用打密码和不加密
openssl pkcs12 -in server_nocret.pfx -nocerts -nodes -out alicekey.pem

java 加密体系

RSA

  1. 随机找两个质数 P 和 Q ,定义n= p*q
  2. 计算 n 的欧拉函数 m= φ(n) =(p-1)*(1-1),为什么是这样我就不懂了 ,即表示在小于n的数中有多少个与n构成互质,
  3. 随机选择一个整数 e,条件是1< e < m,且 e 与 m 互质。
  4. 计算d : e*d % m=1 ,虽然是二元一次方程,但通过拓展欧几里得算法可以得出,反正这个算法我也不懂,能算出来就能算出来吧
  5. 公钥 (n,e) 私钥(n,d)

为什么难以破解:
在已知 公钥 N E 的情况下,想要知道 私钥的额D 就需要知道m,而 m=(p-1)(Q-1),想要知道P Q
就只能对 N 进行分解,而大整数的因式分解是难以破解的,所以保证了安全

PKCS的结构

之前我一直奇怪为什么私钥可以转换出公钥,以为是RSA算法的原理所导致,但看起来原理并不满足私钥算出公钥的操作,所以我觉得问题出在PKCS内容上:

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
PKCS1 的公钥结构:
RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e
}

PKCS1 的私钥结构:
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}

PKCS1 公钥:
PublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
PublicKey BIT STRING
}

AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}


PKCS8私钥
PrivateKeyInfo ::= SEQUENCE {
version Version,
algorithm AlgorithmIdentifier,
PrivateKey BIT STRING
}

AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}

可以看出PKCS1 的私钥包含了密钥产生的所有元素,所以能算出公钥就不奇怪了,至于PKCS8 看起来不包含,但为什么也可以,我想应该只是结构不同,内容应该都是有的

参考

https://blog.csdn.net/xy010902100449/article/details/52145009
注意这个文章的漫画有一个错误的地方,证书并不是CA私钥对公司公钥加密的结果
而应该是如下面这个文章的说法,证书=s_KeyPub + s_Info + ca_Info + enc_s_Hash
s_KeyPub:公司公钥
s_Info:公司信息
ca_Info:ca机构
enc_s_Hash:ca对公司的认证,=ca_pri(hash(s_KeyPub + s_Info + ca_Info))

证书在通讯中如何加签和验签,

https://zhuanlan.zhihu.com/p/36832100

阐述PKCS1 PKCS8的区别,以及PKCS的结构

https://www.shangyang.me/2017/05/24/encrypt-rsa-keyformat/

描述PKCS

https://qsiofttt.iteye.com/blog/1189487

密钥之间转换:

https://xuanxuanblingbling.github.io/ctf/web/2019/05/10/rsa/

RSA:

https://www.zhihu.com/question/25038691/answer/81565068

红黑树删除

发表于 2019-07-21

删除情况图

删除情况

treemap 删除代码

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
 
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;

// 将 有两个子树的 情况 变成 单子树 或叶子
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}

//
Entry<K,V> replacement = (p.left != null ? p.left : p.right);

// 一个子树 的情况
if (replacement != null) {

replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;

// 等于删掉这个节点
p.left = p.right = p.parent = null;

// 开始平衡
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { //如果删除根节点
root = null;
} else { // 删除叶子节点
if (p.color == BLACK)
fixAfterDeletion(p);

if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
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
// 入参是 黑叶子,或  单子树里用来代替的子节点
//主要是处理 黑叶子情况,因为单子树的情况连循环都不进
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {// 左子树
Entry<K,V> sib = rightOf(parentOf(x)); //兄弟节点

if (colorOf(sib) == RED) { //兄弟是红色
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}

if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) { //兄弟节点此时是黑色, 且两个侄子节点是黑色,
setColor(sib, RED);
x = parentOf(x);
} else {//兄弟节点是黑色, 两个侄子节点有一个是红色
if (colorOf(rightOf(sib)) == BLACK) { //近侄子是红色
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
} //处理 远侄子是红色的情况,
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // 反转情况,操作一样
Entry<K,V> sib = leftOf(parentOf(x));

if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}

if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}

setColor(x, BLACK);
}

HashMap解析

发表于 2019-07-15 | 更新于 2019-07-21

重要的参数

  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ,默认的初始容量
  2. static final int MAXIMUM_CAPACITY = 1 << 30 ,最大容量
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f 默认的装载因子
  4. static final int TREEIFY_THRESHOLD = 8 在链长度达到这个长度时转换为树结构
  5. static final int UNTREEIFY_THRESHOLD = 6 当长度达到这个长度,会从树转换成链
  6. static final int MIN_TREEIFY_CAPACITY = 64; 想要转换成树的话,table的最小容量

field

  1. Node<K,V>[] table;
  2. Set<Map.Entry<K,V>> entrySet;
  3. int size;
  4. int modCount; 记录table的修改次数,保证iterators可以快速失败,see ConcurrentModificationException
  5. int threshold 下次扩容的size
  6. float loadFactor; 装载因子

内部类

  1. Node 基础的node元素

    1
    2
    3
    4
    5
    6
    static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    }
  2. KeySet

    1
    final class KeySet extends AbstractSet<K> {}
  3. Values

    1
    final class Values extends AbstractCollection<V> {}
  4. EntrySet

    1
    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {}
  5. HashIterator

    1
    2
    3
    4
    5
    6
    abstract class HashIterator{
    Node<K,V> next; // next entry to return
    Node<K,V> current; // current entry
    int expectedModCount; // for fast-fail
    int index; // current slot
    }
  6. KeyIterator

    1
    2
    final class KeyIterator extends HashIterator
    implements Iterator<K>
  7. ValueIterator

    1
    2
    final class ValueIterator extends HashIterator
    implements Iterator<V>
  8. EntryIterator

    1
    2
    final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>>
  9. KeySpliterator

  10. ValueSpliterator

  11. EntrySpliterator

  12. TreeNode

主要行为

put

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {

//n为length, p为 要放入的位置的指针
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //如果长度为0 则初始化
if ((p = tab[i = (n - 1) & hash]) == null) //取位置的方式时(len-1) & hash 因为len是2的幂次,所以与运算结果一定小于len
tab[i] = newNode(hash, key, value, null);
else { //else 发现位置上已经有Node
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //如果完全一致
else if (p instanceof TreeNode) //如果p 是TreeNode
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //如果p就是正常的Node ,则进行链式的放置
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果达到了树化的大小 ,则进行树化
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //遍历中如果找到equals的Node
break;
p = e;
}
}
//此时 e 指向的对象已经是有正确值的Node
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //判断能否直接替代
e.value = value;
afterNodeAccess(e); //后续操作
return oldValue;
}
}
++modCount; //这段不是很理解,按理来说在链上增加Node也属于改变Mapping数量,但这段代码看起来只会在直接在table上放值时才会走到
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

未完。。。

Class文件结构

发表于 2019-05-12 | 更新于 2019-07-21

魔数与版本

魔数,四个字节,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,Class文件的魔数是
0xCAFEBABE,
版本号,四个字节,前两个字节是此版本号,后两个字节是主版本号,Java的版本号从45开始,JDK1.1 之后每个大版本号在主版本上向上加1,可以推算jdk8 的主版本号是52,这让我想起来曾经某个JDK版本报错内容: unsupport version 52 ,
总览
图为在jdk1.8编译出的class文件的二进制内容

常量池

1
2
3
4
5
6
7
8
9
此处常量池并不是 Java中的常量,而是class文件中的资源仓库,是占用空间最大的数据项目之一,表数据类型项目,在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值

常量池主要存储的是 字面量 和符号引用 ,字面量是类似与Java中常量的存在,类似字符串常量,final, 符号引用是:

类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符

当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中

访问标识

紧接着的两个字节代表访问标志

操作数栈

Java虚拟机的解释执行引擎被称为”基于栈的执行引擎”,其中所指的栈就是指-操作数栈。

不同于程序计数器,Java虚拟机没有寄存器,程序计数器也无法被程序指令直接访问。Java虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的,因此它的运行方式是基于栈的而不是基于寄存器的。虽然指令也可以从其他地方取得操作数,比如从字节码流中跟随在操作码(代表指令的字节)之后的字节中或从常量池中,但是主要还是从操作数栈中获得操作数。

对于基于栈的指令集来说,最大的优势是可移植,因为它的实现不依赖硬件,不足之处在于其运行效率相对于寄存器指令集来说会慢一点;而寄存器指令集的优缺点则刚好与基于栈的指令集相反。

1
2
3
4
5
6
7
8
9
基于寄存器的:
mov ax, 1 ;把 1 放入寄存器 ax
add ax, 2 ;用 ax 的内容和 2 相加后存入 ax


基于栈的
iconst_1 //把整数 1 压入操作数栈
iconst_2 //把整数 2 压入操作数栈
iadd //栈顶的两个数相加后出栈,结果入栈

指令的操作数分两种:一种是嵌入在指令中的,通常是指令字节后面的若干个字节;另一种是存放在操作数栈中的。为了区别,我们把前者叫做嵌入式操作数,把后者叫做栈内操作数。这两者的区别是:嵌入式操作数是在编译时就已经确定的,运行时不会改变,它和指令一样存放于类文件方法表的 Code 属性中;而操作数是运行时确定的,即程序在执行过程中动态生成的

  1. 基于栈的执行引擎 是别于寄存器的 执行方式

未完。。。

性能监控与故障处理工具

发表于 2019-04-27 | 更新于 2019-05-03

jdk 所使用的监控工具,主要都是由tools.jar的实现

JPS

主要作用:用来列出正在执行的虚拟机进程,并显示虚拟机执行的主类名称,以及这些进程在本地虚拟机唯一ID(与操作系统的PID一致),

1
2
3
4
常用参数:
-m 输出启动时的main参数
-v 输出启动时的jvm参数
-l 输出全类名,如果是jar包启动,输出jar路径

jstat

是用于监视虚拟机各种运行状态信息的命令行工具,可显示本地或远程的虚拟机进程的 类加载,内存,垃圾回收,JIT编译等运行数据,
命令格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

options:
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation

参考:

https://blog.csdn.net/zhaozheng7758/article/details/8623549

jinfo

可以实时的查看和修改虚拟机的各项参数,jps -v也可以看到启动参数,但如果想看一些默认参数,可以使用 jinfo -flag 打印,还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来,-flag name=value修改一部分运行期可写的虚拟机参数值

jmap

可以打印memory dump文件,比较关键的一个工具,曾经面试被问过,但我那时候只知道+HeapDumpOnOutOfMemoryError 来打印dump
其实还有 +HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成dump文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“吓唬”一下虚拟机,也能拿到dump文件。
jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等

1
2
jmap -heap 来获得 堆内存情况,
jmap -dump:format=b,file=aaa.dump 18267 打印jvm堆快照文件

此处需要下载 hotspot对应的 debugInfo ,注意小版本也需要相等,

wget http://debuginfo.centos.org/7/x86_64/java-1.8.0-openjdk-debuginfo-1.8.0.212.b04-0.el7_6.x86_64.rpm

jhat

是一个用来分析jmap生成的内存dump文件的工具,但一般用不到,不如直接使用eclipse MAT

jstack

用来生成 当前的线程快照, 可以用来确认线程长时间停顿 死锁 占用CPU时间长的原因,会打印出所有的线程的运行状况,

问题:

我起了一个springboot的示例程序来观察内存,
使用jstat 查看到:

1
2
3
4
5
6
NGCMN    NGCMX     NGC     S0C   S1C       EC      OGCMN      OGCMX       OGC         OC       MCMN     MCMX      MC     CCSMN    CCSMX     CCSC    YGC    FGC 
5440.0 83968.0 9984.0 960.0 960.0 8064.0 10944.0 167936.0 19848.0 19848.0 0.0 1081344.0 35456.0 0.0 1048576.0 4480.0 80 2

top:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
18267 wks 20 0 2242.5m 127.8m 13.0m S 0.0 13.1 0:54.99 java

问题在于 OGC + NGC + MC !=top 里看到的Java内存大小
19848+9984+35456 = 63.7578125‬M
这让我很困扰

Conclusion:
java进程占用的内存大小并不只是堆内存,且我们没有办法估算 Java 进程占用的RAM内存,因为有太多的因素,
比如 栈内存,对外内存,垃圾回收器的RS,

1
2
3
4
Total memory = Heap + Code Cache + Metaspace + Symbol tables +
Other JVM structures + Thread stacks +
Direct buffers + Mapped files +
Native Libraries + Malloc overhead + ...

参考:

https://stackoverflow.com/questions/53451103/java-using-much-more-memory-than-heap-size-or-size-correctly-docker-memory-limi

G1学习

发表于 2019-04-21 | 更新于 2019-05-03

之所以单独写一篇是觉得 深入理解JVM虚拟机这本书,对G1细节讲的太少,而且在网上查的资料看起来也很模糊,甚至很有误导性,所以整理了下,但说实话我还是没有理解这个G1完整的过程,将来还是需要慢慢理解整理。

内存分配描述

G1 的内存不再是像以前一样的连续的内存分布,而是非连续的内存分布
g1收集器内存分布
每一块是一个region,可以通过-XX:G1HeapRegionSize指定大小,只能是2的幂等次,
H表示的是巨型对象,巨型对象默认是分配在老年区,但是如果是短期的,那么会对老年区造成影响,所以G1划分了H区,如果一个H区放不下,那么会分配连续的空间存放

GC 模式

共有三种GC模式:

  1. young gc
  2. mixed gc
    分为两个部分:
    1. global concurrent marking
    2. 混合垃圾回收 STW
  3. full gc

young gc一般是当所有的eden region都被耗尽,就触发一次young gc,执行完一次之后,对象被拷贝survivor 或者old

mixed gc 并不是一个old gc,这是重点, 他会回收整个的young,和部分的old,当老年代达到了内存的阙值时触发

young gc

STW

阶段分为:

  1. 根扫描, 静态和本地对象被扫描
  2. 更新RS, 处理dirty card队列更新RS
  3. 处理RS, 检测从年轻代指向年老代的对象
  4. 对象拷贝,复制到survivor 和old
  5. 处理引用队列,软引用,弱引用,虚引用处理

其实不是很理解步骤2,3

这里思考一个问题,如果要只收集young, 但young 和old肯定是会有互相引用,难道要扫描全部内存?G1使用了remember set (rset) ,每个region有一个rset,记录谁引用了这块region(point-in),这个概念在其他收集器中也有体现(资料说是CMS,但我觉得只要分代收集就要用这个),但在之前rset记录的是老年代引用了哪些新生代对象(point-out),所以直接扫描这块,就不用扫描全部,

在G1中使用的是ponit-in,记录的是谁引用了这块,因为分区太多,这个rset只记录年老代到新生代的引用,因为每次GC eden都被扫描,

这就又产生一个问题,一个region有很多对象,记录可能会很多 ,所以又产生一个概念,cardTable, 其实就是把一个分区划分为多个区域,rset只记录指向哪个区域,而不是精确到对象

rset和cardTable

mixed gc

选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region

分为两个步骤:

  1. 全局并发标记(global concurrent marking)
  2. 混合垃圾回收

global concurrent marking主要为mix gc提供标记服务,并不是必须环节,分为五个步骤:

  1. 初始标记(initial mark,STW)
    stop-the-world,它伴随着一次普通的 Young GC 发生,然后对 Survivor 区(root region)进行标记,因为该区可能存在对老年代的引用,因为 Young GC 是需要 stop-the-world 的,所以并发标记直接重用这个阶段
  2. 根区域扫描(root region scan)
    扫描 Survivor 到老年代的引用,该阶段必须在下一次 Young GC 发生前结束
  3. 并发标记(Concurrent Marking)
    寻找整个堆的存活对象,该阶段可以被 Young GC 中断
  4. 最终标记(Remark,STW)
    stop-the-world,完成最后的存活对象标记。使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法
  5. 清除垃圾(Cleanup,STW)
    清除空Region,即没有存活对象的region,所以这步不能看作mixgc的清理阶段

并发标记结束后是混合垃圾回收周期,不仅进行年轻代垃圾收集,而且回收之前标记出来的老年代的垃圾最多的部分区块。

混合垃圾回收周期会持续进行,直到几乎所有的被标记出来的分区(垃圾占比大的分区)都得到回收,然后恢复到常规的年轻代垃圾收集,最终再次启动并发标记。

所以看起来young gc 和mix gc并不是互斥进行,可以说是同时在进行。

full gc

什么情况会触发full gc(STW收集):

  1. concurrent mode failure:并发模式失败,CMS 收集器也有同样的概念。G1 并发标记期间,如果在标记结束前,老年代被填满,G1 会放弃标记
  2. 晋升失败:并发周期结束后,是混合垃圾回收周期,伴随着年轻代垃圾收集,进行清理老年代空间,如果这个时候清理的速度小于消耗的速度,导致老年代不够用,那么会发生晋升失败
  3. 疏散失败:年轻代垃圾收集的时候,如果 Survivor 和 Old 区没有足够的空间容纳所有的存活对象。这种情况肯定是非常致命的,因为基本上已经没有多少空间可以用了,这个时候会触发 Full GC 也是很合理的。

三色标记法

TODO 待补全

常用参数

TODO 待补全

参考:

http://blog.jobbole.com/109170/
http://ifeve.com/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3g1%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/
https://tech.meituan.com/2016/09/23/g1.html 讲的最好
https://juejin.im/entry/5af0832c51882567244deb44

垃圾回收器

发表于 2019-04-15 | 更新于 2019-05-03

使用场景总览

总览

serial 收集器

  1. 可以使用在新生代
  2. 单线程
  3. 进行收集操作的时候 stop the world
  4. 使用复制算法

serial_old

  1. 老年代版本的serial 收集器
  2. 单线程
  3. 使用标记-整理算法
  4. 主要用途是在jdk1.5 之前parallel scavenge配合,还有作为CMS的后备收集器,在并发收集时发生concurrent Mode Failure时使用,

ParNew 收集器

  1. 用在新生代,使用复制算法
  2. serial的多线程版本,
  3. 新生代的首选,除了serial,只有他可以和CMS配合使用,实际上,在CMS作为老年代收集器后新生代只能选择serial和parNew
  4. -XX:ParallelGCThreads可以限制回收的线程数

Parallel Scavenge

  1. 用在新生代,使用复制算法,使用多线程
  2. 别的收集器关注点在于降低STW的时间,而Parallel Scavenge关注的是吞吐量,吞吐量=运行用户代码时间/(用户代码时间+垃圾回收时间)
  3. Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
  4. -XX:+UseAdaptiveSizePolicy 就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
  5. 无法与CMS配合使用,原因是Parallel Scavenge没有使用原本HotSpot其它GC通用的那个GC框架,所以不能跟使用了那个框架的CMS搭配使用

parallel scavenge old

  1. 用在老年代,多线程,标记整理算法
  2. 在1.6后提供,在此之前都只能使用serial_old作为老年代来配合parallel scavenge,效率并不高

CMS

  1. 目的时减少停顿时间,用于老年区
  2. 阶段包括:
    1.初始标记。标记下GC ROOT直接可达的对象,速度快
    2.并发标记。进行GC root trace
    3.重新标记. 是为了修正并发标记期间用户程序运行产生的垃圾变动,
    4.并发清除
    初始标记,重新标记仍然需要STW,

  3. 缺点:
    1. 无法处理浮动垃圾,即在并发清除时产生的垃圾,出现concurrent mode failure,所以CMS需要给并发清除预留一部分内存,所以CMS不能像别的收集器一样满了之后才运行,而是在达到阙值后就启动,-XX:CMSInitiatingOccupancyFraction可以设置阙值
    2. 由于基于标记清除算法,所以可能出现太多碎片,导致大的对象无法分配内存,为了处理这种情况,提供-XX:+UseCMSCompactAtFullCollection ,在每次FUll GC之前进行,碎片整理,和XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的

G1收集器

在内存上将内存分为大小相等的region,新生代和老年代都是一部分不需要连续的的region,G1 可以有计划的避免在整个堆中进行回收,而是维护一个优先队列,每次根据运行的时间,优先回收价值最大的region,

这里作者提出一个问题:如果分为region,那么每个region里的对象肯定不是孤立的,而是互相有依赖,那么如果想回收新生代,岂不是也要扫描老年代?实际上G1和其他收集器在虚拟机中都是使用remembered set 来记录引用关系,如果虚拟机检测到对引用的修改,那么就立刻更新remembered set ,这样就可以保证不需要扫描全内存。

  1. G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果
  2. G1从整体来看是基于标记—整理算法实现的收集器,从局部(两个Region之间)上来看是基于复制算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片
  3. 停顿时间可预测,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒 ,
  4. 操作主要包括:
    1.初始标记,标记GC root直接可达的对象,并改变next top at mark start,STW,时间很短
    2.并发标记,进行可达性分析,将引用的变化情况记录在remembered set log
    3.最终标记,将remember set log 和remember set合并,STW
    4.筛选回收,也可以停顿也可以并行,主要对region回收价值排序,并在用户指定的停顿时间来制定回收计划

  5. 相对于CMS,碎片更少空间利用更高,停顿时间可控,

关于吞吐率和停顿时间  

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,
看到parallel scavenge 注重的时吞吐率,而其他收集器注重停顿时间,觉得这两个不应该是同一种东西吗,减少了停顿时间不就提高了吞吐率吗,但这俩个应该算是两个维度,GC的时间应该是停顿时间的超集。

判断对象已死-垃圾回收算法-hotspot实现

发表于 2019-04-15 | 更新于 2019-05-04

判断对象已死

引用计数法

缺点:无法判断循环引用

可达性分析

定义:称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的

GC roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

关于引用

在jdk1.2 之前,对引用的概念太过于狭窄,我们希望描述一种类型:当内存足够时可以保留,当内存不够时可以抛弃,

jdk1.2之后通过扩展,有了:

  • 强引用

    正常使用的引用,只要引用还在就永远不会回收

  • 软引用

    软引用描述还有用但并非必须的对象,在即将抛出OOM之前进行回收,如果回收之后还无法获得足够的空间就OOM,提供了softReference来实现软引用

  • 弱引用

    被弱引用关联的对象只存活到下一次GC就会被回收,提供weakReference来实现

  • 虚引用

    最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,提供PhantomReference来实现。

finalize

当一个对象被可达性分析判断可以GC,那么将会被标记,如果该对象覆盖了finalize方法,那么将会把对象放在F-queue队列,等待一个低优先级的Finalize线程来执行finalize方法,但注意如果对象的finalize方法中又使得对象和应用链中的对象重新建立了联系,
注意的点:

  1. 这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行
  2. finalize方法的用途是为了处理关闭外部资源,所以大多数情况并不需要主要这个方法

永久区(方法区)对象判断

方法区的回收主要是常量和类对象的回收,判断常量是否可回收,是检测是否还有对象引用常量,而判断类的可回收,条件如下

  1. 该类的所有对象都被回收。
  2. 加载该类的classLoardor被回收
  3. 该类的class对象没有被引用

    HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持

对于大量使用classLoader ,如反射,cglib的框架,都需要设定这个参数来保证永久区不会溢出(这让我回想起曾经的热加载)

垃圾回收算法

标记清除

是最基础的回收算法,别的很多算法都基于这个思想,分为两个阶段,标记-清除,具体过程是标记后统一清除。
缺点:

  1. 效率不高
  2. 空间问题,如果没有整理,会产生大量的空间碎片,影响到分配大对象

复制算法

为了解决效率问题和碎片问题,将内存分为两个部分,每次new对象只放在一边,满了之后将还存活的对象复制到另一边,再清理掉旧的一边的内存,解决了碎片问题。现代垃圾回收采用这种算法来处理新生代区域内存。

原因是:新生代的内存朝生夕死,意味着它的存活很短,大部分对象都是需要被gc的,所以并不需要1:1的方式划分新生代,

而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”

标记整理算法

对于老年代,对象存活时间长,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法

分配担保指的是复制算法中如果一边内存的对象被复制去另一边的时候发现另一边空间不够,就需要有另一块内存来保证可以被使用,

所以提出标记整理算法,过程是先标记,然后让所有存活的对象都像另一端移动,然后清理掉端边界以外的内存,

分代回收

根据对象的存活周期将内存划分为不同区域(新生代,老年代),对不同的区域采用不同的回收算法

对于新生代,对象存活时间短,所以可以使用复制算法,而对于老年代,存活时间长,可以使用标记清除或者标记整理算法。

Hotspot的实现

枚举根节点

可达性分析的难点:

  • 目前方法区的范围太大,检查引用会消耗太大的时间
  • 因为并发,导致检查这个步骤必须stop the world

hotspot的实现上,使用准确式GC,即JVM知道内存当中某位置存放什么样的数据,使用oopMap(ordinary Object Pointer Map)普通对象指明Map,在类加载完成之后,JVM 就记录了这个对象实例的多少偏移量存放的是什么类型数据,这样GC的时候就可以直接使用oopMap 来得到引用信息。

安全点

维护oopMap 非常困难(因为并发,而且改变引用是很容易的事),所以hotspot 使用了安全点的概念,安全点可以抽象的看作当前系统运行的一个时间点,在这个时间点进行oopMap 更新和GC,安全点的选择是是否具有让程序长时间执行的特征为标准选择

因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint

这就产生一个问题,在进行GC时会stop the world 来进行可达性分析,那么现在使用了安全点来标记gc的时间点,所以就需要所有线程在进行gc前跑到安全点上(但是否是”所有”线程),有两种方案:

  • 抢先式中断
  • 主动式中断

抢先式中断在GC发生时把所有线程中断,如果发现有线程中断的地方不在安全点,就激活线程,这种方式几乎没有虚拟机使用

主动式中断是当需要GC的时候,不主动中断线程,而是设置标志位,线程轮询标准位来判断是否挂起,而轮询的位置就是安全点位置,和分配对象内存的位置,

安全区域

安全点没办法解决当程序不分配CPU的时候,即程序sleep或blocked的时候,这时无法到达安全点和响应中断,就需要安全区域的概念

简单描述就是在一段代码中,引用关系不变的话,就属于safe region ,在这个区域中进行GC都是可以的,线程会标识自己进入safe region ,这样当进行GC的时候,就不用管这些线程了

wks

9 日志
4 标签
© 2019 wks
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Muse v7.1.0