diff --git a/docs/Apollo/Apollo简单入门.md b/docs/Apollo/Apollo简单入门.md index b45a890..3e1d91a 100644 --- a/docs/Apollo/Apollo简单入门.md +++ b/docs/Apollo/Apollo简单入门.md @@ -8,8 +8,7 @@ categories: - Apollo keywords: Apollo,配置中心。 description: Apollo简单入门及和SpringBoot集成。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img_003/Apollo/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/Apollo/logo.png' abbrlink: 10d32fba date: 2020-12-29 11:31:58 --- @@ -77,7 +76,7 @@ date: 2020-12-29 11:31:58 不过,解决一个问题的同时,往往会诞生出很多新的问题,所以微服务化的过程中伴随着很多的挑战,其中一个挑战就是有关服务(应用)配置的。当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了,不仅如此,分散中还包含着冗余,如下图: - + 配置中心将配置从应用中剥离出来,统一管理,优雅的解决了配置的动态变更、持久化、运维成本等问题。 @@ -87,7 +86,7 @@ date: 2020-12-29 11:31:58 在系统架构中,配置中心是整个微服务基础架构体系中的一个组件,如下图,它的功能看上去并不起眼,无非就是配置的管理和存取,但它是整个微服务架构中不可或缺的一环。 - + @@ -172,7 +171,7 @@ Apollo简介 ### Apollo简介 - + **Apollo - A reliable configuration management system** @@ -222,7 +221,7 @@ Apollo快速入门 ### 执行流程 - + 操作流程如下: @@ -263,7 +262,7 @@ Apollo的表结构对`timestamp`使用了多个default声明,所以需要5.6.5 2. 打开1.3发布链接,下载必须的安装包:[https://github.com/ctripcorp/apollo/releases/tag/v1.3.0](https://github.com/ctripcorp/apollo/releases/tag/v1.3.0)。三个都要下 - + 解压安装包后将apollo-configservice-1.3.0.jar, apollo-adminservice-1.3.0.jar, apollo-portal-1.3.0.jar放置于apollo目录下 @@ -350,11 +349,11 @@ Apollo服务端共需要两个数据库:`ApolloPortalDB`和`ApolloConfigDB`, 1. 也可以使用提供的runApollo.bat快速启动三个服务(修改数据库连接地址,数据库以及密码) - + 这里面是一个很简单的脚本 - + @@ -386,29 +385,29 @@ start "ApolloPortal" java -Xms256m -Xmx256m -Dapollo_profile=github,auth -Ddev_m 1. 打开[apollo](http://localhost:8070/) :新建项目apollo-quickstart - + 2. 新建配置项sms.enable - + - + 确认提交配置项 - + -![image-20201228102339176](https://cdn.jsdelivr.net/gh/youthlql/lql_img_003/Apollo/Simple_Introduction/0012.png) +![image-20201228102339176](https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/Apollo/Simple_Introduction/0012.png) 3. 发布配置项 - + #### 应用读取配置 @@ -486,7 +485,7 @@ public class GetConfigTest { -Dapp.id=apollo-quickstart -Denv=DEV -Ddev_meta=http://localhost:8080 - + 运行GetConfigTest,打开控制台,观察输出结果 @@ -524,19 +523,19 @@ sma.enable: true 2. 运行GetConfigTest观察输出结果 - + 3. 在Apollo管理界面修改配置项 - + 4. 发布配置 - + 5. 在控制台查看详细情况:可以看到程序获取的sms.enable的值已由false变成了修改后的true - + @@ -547,7 +546,7 @@ Apollo应用 下图是Apollo架构模块的概览 - + #### 各模块职责 @@ -602,7 +601,7 @@ Apollo应用 它们的关系如下图所示: - + @@ -616,15 +615,15 @@ apollo 默认部门有两个。要增加自己的部门,可在系统参数中 * 进入系统参数设置 - + -![image-20201228103939300](https://cdn.jsdelivr.net/gh/youthlql/lql_img_003/Apollo/Simple_Introduction/0022.png) +![image-20201228103939300](https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/Apollo/Simple_Introduction/0022.png) * 输入key查询已存在的部门设置:organizations - + * 修改value值来添加新部门,下面添加一个微服务部门: @@ -640,11 +639,11 @@ apollo默认提供一个超级管理员: apollo,可以自行添加用户 * 新建用户张三 - + - + #### 创建项目 @@ -662,7 +661,7 @@ apollo默认提供一个超级管理员: apollo,可以自行添加用户 * 应用名称:应用名,仅用于界面展示 * 应用负责人:选择的人默认会成为该项目的管理员,具备项目权限管理、集群创建、Namespace创建等权限 - + 4. 点击提交,创建成功后,会自动跳转到项目首页 @@ -672,7 +671,7 @@ apollo默认提供一个超级管理员: apollo,可以自行添加用户 * 使用管理员apollo将指定项目授权给用户张三 - + * 将修改和发布权限都授权给张三 @@ -732,7 +731,7 @@ Namespace作为配置的分类,可当成一个配置文件。 进入项目首页,点击左下脚的“添加Namespace”,共包括两项:关联公共Namespace和创建Namespace,这里选择“创建Namespace” - + 2. 添加配置项 @@ -759,11 +758,11 @@ Namespace作为配置的分类,可当成一个配置文件。 进入common-template项目管理页面:[http://localhost:8070/config.html?#/appid=common-template](http://localhost:8070/config.html?#/appid=common-template) - + - + 1. 添加配置项并发布 @@ -787,11 +786,11 @@ Namespace作为配置的分类,可当成一个配置文件。 2. 点击左侧的添加Namespace 3. 添加Namespace - + 4. 根据需求可以覆盖引入公共Namespace中的配置,下面以覆盖server.servlet.context-path为例 - + 5. 修改server.servlet.context-path为:/account-service 6. 发布修改的配置项 @@ -807,15 +806,15 @@ Namespace作为配置的分类,可当成一个配置文件。 1. 点击页面左侧的“添加集群”按钮 2. 输入集群名称SHAJQ,选择环境并提交:添加上海金桥数据中心为例 - + -![image-20201228112150602](https://cdn.jsdelivr.net/gh/youthlql/lql_img_003/Apollo/Simple_Introduction/0034.png) +![image-20201228112150602](https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/Apollo/Simple_Introduction/0034.png) 3. 切换到对应的集群,修改配置并发布即可 - + #### 同步集群配置 @@ -828,19 +827,19 @@ Namespace作为配置的分类,可当成一个配置文件。 * 展开要同步的Namespace,点击同步配置 - + - ![image-20201228112603903](https://cdn.jsdelivr.net/gh/youthlql/lql_img_003/Apollo/Simple_Introduction/0037.png) + ![image-20201228112603903](https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/Apollo/Simple_Introduction/0037.png) * 选择同步到的新集群,再选择要同步的配置 - + * 同步完成后,切换到SHAJQ集群,发布配置 - + #### 读取配置 @@ -864,7 +863,7 @@ Namespace作为配置的分类,可当成一个配置文件。 在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。 - + 上图简要描述了配置发布的主要过程: @@ -891,7 +890,7 @@ Admin Service在配置发布后,需要通知所有的Config Service有配置 SELECT * FROM ApolloConfigDB.ReleaseMessage ``` - + @@ -1013,7 +1012,7 @@ Admin Service在配置发布后,需要通知所有的Config Service有配置 4. NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端 - + #### Config Service通知客户端 @@ -1269,7 +1268,7 @@ Apollo客户端会把从服务端获取到的配置在本地文件系统缓存 -Denv=DEV -Dapollo.cacheDir=/opt/data/apollo-config -Dapollo.cluster=DEFAULTbash ``` - + @@ -1367,11 +1366,11 @@ public class AccountApplication { 2. spring-http命名空间在之前已通过关联公共命名空间添加好了,现在来添加spring-boot-druid命名空间 - + 3. 添加本地文件中的配置到对应的命名空间,然后发布配置 - + 4. 在account-service/src/main/resources/application.properties中配置apollo.bootstrap.namespaces需要引入的命名空间(上面写过) @@ -1459,7 +1458,7 @@ public class AccountController { * 访问[http://127.0.0.1:63000/account-service/db-url](http://127.0.0.1:63000/account-service/db-url),显示结果 - + #### 创建其它项目 @@ -1475,7 +1474,7 @@ public class AccountController { 具体如下图所示: - + 下面以添加生产环境部署为例 @@ -1524,7 +1523,7 @@ UPDATE ServerConfig SET `Value` = "http://localhost:8081/eureka/" WHERE `key` = 服务配置项统一存储在ApolloPortalDB.ServerConfig表中,可以通过`管理员工具 - 系统参数`页面进行配置:apollo.portal.envs - 可支持的环境列表 - + 默认值是dev,如果portal需要管理多个环境的话,以逗号分隔即可(大小写不敏感),如: @@ -1557,7 +1556,7 @@ Apollo Portal需要在不同的环境访问不同的meta service(apollo-configse 1. 启动之后,点击account-service服务配置后会提示环境缺失,此时需要补全上边新增生产环境的配置 - + 3. 点击左下方的补缺环境 @@ -1565,7 +1564,7 @@ Apollo Portal需要在不同的环境访问不同的meta service(apollo-configse 4. 补缺过生产环境后,切换到PRO环境后会提示有Namespace缺失,点击补缺 - + 5. 从dev环境同步配置到pro @@ -1611,7 +1610,7 @@ apollo-quickstart项目有两个客户端: 1. 172.16.0.160 2. 172.16.0.170 - + **灰度目标** @@ -1627,7 +1626,7 @@ apollo-quickstart项目有两个客户端: 2. 点击确定后,灰度版本就创建成功了,页面会自动切换到`灰度版本`Tab - + #### 灰度配置 @@ -1635,17 +1634,17 @@ apollo-quickstart项目有两个客户端: 2. 在弹出框中填入要灰度的值:3000,点击提交 - + #### 配置灰度规则 1. 切换到`灰度规则`Tab,点击`新增规则`按钮 - + 2. 在弹出框中`灰度的IP`下拉框会默认展示当前使用配置的机器列表,选择我们要灰度的IP,点击完成 - + 如果下拉框中没找到需要的IP,说明机器还没从Apollo取过配置,可以点击手动输入IP来输入,输入完后点击添加按钮 @@ -1675,11 +1674,11 @@ vm options: `-Dapp.id=apollo-quickstart -Denv=DEV -Ddev_meta=http://localhost:80 } ``` - + 2. 切换到`配置`Tab,再次检查灰度的配置部分,如果没有问题,点击`灰度发布` - + 3. 在弹出框中可以看到主版本的值是2000,灰度版本即将发布的值是3000。填入其它信息后,点击发布 @@ -1687,9 +1686,9 @@ vm options: `-Dapp.id=apollo-quickstart -Denv=DEV -Ddev_meta=http://localhost:80 4. 发布后,切换到`灰度实例列表`Tab,就能看到172.16.0.160已经使用了灰度发布的值 - + - + diff --git a/docs/Computer_NetWork/计算机网络-总结.md b/docs/Computer_NetWork/计算机网络-总结.md index aba837b..0c46666 100644 --- a/docs/Computer_NetWork/计算机网络-总结.md +++ b/docs/Computer_NetWork/计算机网络-总结.md @@ -1,3 +1,17 @@ +--- +title: 计算机网络-总结篇 +tags: + - 计算机网络 + - 面试 +categories: + - 计算机网络 +keywords: 计算机网络,计网,面试 +description: 计算机网络-总结篇,可以用来期末复习,校招面试等。 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/computer_network/logo.jpg' +abbrlink: 3905e6f8 +date: 2020-04-16 17:21:58 +--- + # 备注 @@ -177,7 +191,7 @@ https://www.cnblogs.com/felixzh/p/10345929.html ## 区别+应用场景 - + **总结:** @@ -292,7 +306,7 @@ TCP通过三次握手建立可靠连接 在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。 - + @@ -819,7 +833,7 @@ proactor: 这有十个字节数据,收好了跟我说一声。 ​ 当用户进程进行recvfrom这个系统调用,内核就开始了IO的第一个阶段:等待数据准备。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候**内核**就要等待足够的数据到来。而在用户进程这边,整 个进程会被阻塞。当**内核**一直等到数据准备好了,它就会将数据从**内核**中拷贝到用户内存,然后**内核**返回果,用户进程才解除 block的状态,重新运行起来。**所以,blocking IO的特点就是在IO执行的两个阶段都被block了。** - + @@ -830,7 +844,7 @@ proactor: 这有十个字节数据,收好了跟我说一声。 3. 虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。 4. **所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。** - + @@ -842,7 +856,7 @@ proactor: 这有十个字节数据,收好了跟我说一声。 2. 它的基本原理就是select /epoll这个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,正式发起read请求。 3. 从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket(也就是数据准备好了的socket),即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。 - + @@ -868,7 +882,7 @@ proactor: 这有十个字节数据,收好了跟我说一声。 2. 这个一般用于UDP中,对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情 3. 信号驱动IO放佛很像异步IO,它的第一阶段不是阻塞的。但是很遗憾,它的数据拷贝阶段(第二阶段),任然是阻塞的。 - + @@ -879,7 +893,7 @@ proactor: 这有十个字节数据,收好了跟我说一声。 3. 而另一方面,从**内核**的角度,当它受到一个异步读之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都 完成之后,**内核**会给用户进程发送一个信号,告诉它read操作完成了,用户线程直接使用即可。 在这整个过程中,进程完全没有被阻塞。 4. 异步IO模型使用了Proactor设计模式实现了这一机制。**(具体怎么搞得,看上面的文章链接)** - + diff --git a/docs/ElasticSearch/usage/ElasticSearch-入门.md b/docs/ElasticSearch/usage/ElasticSearch-入门.md index eca1fb7..dd008f6 100644 --- a/docs/ElasticSearch/usage/ElasticSearch-入门.md +++ b/docs/ElasticSearch/usage/ElasticSearch-入门.md @@ -9,8 +9,7 @@ categories: - 用法 keywords: ElasticSearch,全文检索 description: ElasticSearch-入门篇,适合做入门,或者知识回顾。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/ElasticSearch/logo.jpg' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/ElasticSearch/logo.jpg' abbrlink: 7f60dde9 date: 2020-02-03 13:11:45 --- @@ -47,7 +46,7 @@ date: 2020-02-03 13:11:45 下图是ElasticSearch的索引结构,下边黑色部分是物理结构,上边黄色部分是逻辑结构,逻辑结构也是为了更好的 去描述ElasticSearch的工作原理及去使用物理结构中的索引文件。 - + 逻辑结构部分是一个倒排索引表: @@ -59,11 +58,11 @@ date: 2020-02-03 13:11:45 如下: - + 现在,如果我们想搜到`quick brown`我们只需要查找包含每个词条的文档: - + 两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 , 那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳 @@ -237,7 +236,7 @@ http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000 使用postman测试: - + diff --git a/docs/ElasticSearch/usage/ElasticSearch-进阶.md b/docs/ElasticSearch/usage/ElasticSearch-进阶.md index ab20eb2..d3e1fde 100644 --- a/docs/ElasticSearch/usage/ElasticSearch-进阶.md +++ b/docs/ElasticSearch/usage/ElasticSearch-进阶.md @@ -9,8 +9,7 @@ categories: - 用法 keywords: ElasticSearch,全文检索 description: ElasticSearch-进阶篇,ElasticSearch的一些实战用法,集成SpringBoot。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/ElasticSearch/logo.jpg' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/ElasticSearch/logo.jpg' abbrlink: 50e81c79 date: 2020-02-08 18:06:23 --- @@ -431,7 +430,7 @@ Post:http://localhost:9200/xc_test/doc/3 ```json { "name": "spring cloud实战", - "description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。", + "description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战SpringBoot 4.注册中心eureka。", "studymodel": "201001", "price": 5.6 } @@ -1729,4 +1728,5 @@ Post: http://127.0.0.1:9200/xc_course/doc/_search } } -``` \ No newline at end of file +``` + diff --git a/docs/Java/Basis/Java8_New_Features/Java8新特性.md b/docs/Java/Basis/Java8_New_Features/Java8新特性.md index 595881c..967f898 100644 --- a/docs/Java/Basis/Java8_New_Features/Java8新特性.md +++ b/docs/Java/Basis/Java8_New_Features/Java8新特性.md @@ -5,12 +5,11 @@ tags: - JDK8 - 新特性 categories: - - Java + - Java基础 - 新特性 keywords: Java8,新特性,JDK8 description: 详解JDK8出现的新特性。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_Basis/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_Basis/logo.png' abbrlink: de3879ae date: 2020-10-19 22:15:58 --- @@ -21,7 +20,7 @@ date: 2020-10-19 22:15:58 > 本篇文章只讲解比较重要的 - + @@ -112,12 +111,15 @@ import java.util.function.Consumer; * 3. Lambda表达式的使用:(分为6种情况介绍) *

* 总结: - * ->左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只有一个参数,其一对()也可以省略 - * ->右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一对{}和return关键字 + * ->左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只有一个参数,其一对()也 + * 可以省略 + * ->右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一 + 对{}和return关键字 *

* 4.Lambda表达式的本质:作为函数式接口的实例 *

- * 5. 如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口。我们可以在一个接口上使用 @FunctionalInterface 注解, + * 5. 如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口。我们可以在一个接口上 + 使用 @FunctionalInterface 注解, * 这样做可以检查它是否是一个函数式接口。 *

* 6. 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。 @@ -306,13 +308,13 @@ public class LambdaTest1 { **核心函数式接口** - + **其它函数式接口** - + @@ -900,7 +902,7 @@ Stream到底是什么呢? - + @@ -999,7 +1001,10 @@ public class StreamAPITest1 { list.stream().limit(3).forEach(System.out::println); System.out.println(); -// skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补 + /* + skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个, + 则返回一个空流。与 limit(n) 互补 + */ list.stream().skip(3).forEach(System.out::println); System.out.println(); @@ -1019,7 +1024,10 @@ public class StreamAPITest1 { //映射 @Test public void test2(){ -// map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。 + /* + map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应 + 用到每个元素上,并将其映射成一个新的元素。 + */ List list = Arrays.asList("aa", "bb", "cc", "dd"); list.stream().map(str -> str.toUpperCase()).forEach(System.out::println); @@ -1035,8 +1043,10 @@ public class StreamAPITest1 { s.forEach(System.out::println); }); System.out.println(); -// flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。 - //flatMap一层遍历即可拿到想要的结果 + /* + flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连 + 接成一个流。flatMap一层遍历即可拿到想要的结果 + */ Stream characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream); characterStream.forEach(System.out::println); @@ -1171,7 +1181,7 @@ public class StreamAPITest2 { //3-收集 @Test public void test4(){ -// collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法 +// collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法 // 练习1:查找工资大于6000的员工,结果返回为一个List或Set List employees = EmployeeData.getEmployees(); @@ -1209,7 +1219,7 @@ public class StreamAPITest2 { ## 常用API - + diff --git a/docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md b/docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md index 5de3736..051814e 100644 --- a/docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md +++ b/docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md @@ -9,9 +9,8 @@ categories: - 重难点 keywords: Java基础,泛型 description: 万字长文详解Java泛型。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_Basis/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' -abbrlink: 1c342bc4 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_Basis/logo.png' +abbrlink: adb2faf0 date: 2020-10-19 22:21:58 --- @@ -592,7 +591,8 @@ class GenerateTest { /** * 1、在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。 - * 2、由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。 + * 2、由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别 + 泛型方法中识别的泛型。 */ public void show_3(E t) { System.out.println(t.toString()); @@ -777,7 +777,8 @@ class Order { /** * 2、泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说, * 泛型方法所属的类是不是泛型类都没有关系。 - * 3、泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在初始化类时确定,所以无所谓 + * 3、泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在初始化类时确定, + * 所以无所谓 */ public static List copyFromArrayToList(E[] arr){ @@ -1037,7 +1038,7 @@ class Dog extends Animal { `test1()`在编译时就会飘红 - + @@ -1289,7 +1290,7 @@ public class Test_difference { } ``` - + ### 区别3:?通配符可以使用超类限定而T不行 @@ -1382,7 +1383,7 @@ class D { ​ Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,编译器在编译的时候去掉,这个过程就称为类型擦除。 -​ 如在代码中定义的List和List等类型,在编译后都会编程List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。 +​ 如在代码中定义的`List`和`List`等类型,在编译后都会编程List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。 可以通过两个例子,来证明java泛型的类型擦除。 diff --git a/docs/Java/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md b/docs/Java/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md index 8c2e00b..60f241b 100644 --- a/docs/Java/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md +++ b/docs/Java/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md @@ -8,8 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第10章-垃圾回收概述和相关算法。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: d54daa0f date: 2020-11-16 18:14:02 --- @@ -21,7 +20,7 @@ date: 2020-11-16 18:14:02 - + 1. Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动的收集。 @@ -103,7 +102,7 @@ date: 2020-11-16 18:14:02 **十几年前磁盘碎片整理的日子** - + @@ -175,7 +174,7 @@ date: 2020-11-16 18:14:02 ### 应该关心哪些区域的回收? - + 1. 垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收, 1. 其中,**Java堆是垃圾收集器的工作重点** @@ -217,7 +216,7 @@ date: 2020-11-16 18:14:02 ### 循环引用 - + 当p的指针断开的时候,内部的引用形成一个循环,计数器都还算1,无法被回收,这就是循环引用,从而造成内存泄漏 @@ -256,7 +255,7 @@ public class RefCountGC { - + * 如果不小心直接把`obj1.reference`和`obj2.reference`置为null。则在Java堆中的两块内存依然保持着互相引用,无法被回收 @@ -354,7 +353,7 @@ Process finished with exit code 0 3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。 4. 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。 - + @@ -372,7 +371,7 @@ Process finished with exit code 0 - 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器。 7. 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 - + @@ -454,7 +453,7 @@ Object 类中 finalize() 源码 **通过 JVisual VM 查看 Finalizer 线程** - + @@ -580,7 +579,7 @@ MAT与JProfiler的GC Roots溯源 **方式一:命令行使用 jmap** - + @@ -632,23 +631,23 @@ public class GCRootsTest { 1、先执行第一步,然后停下来,去生成此步骤dump文件 - + 2、 点击【堆 Dump】 - + 3、右键 --\> 另存为即可 - + 4、输入命令,继续执行程序 - + 5、我们接着捕获第二张堆内存快照 - + @@ -658,19 +657,19 @@ public class GCRootsTest { > 点击Open Heap Dump也行 - + 2、选择Java Basics --> GC Roots - + 3、第一次捕捉堆内存快照时,GC Roots 中包含我们定义的两个局部变量,类型分别为 ArrayList 和 Date,Total:21 - + 4、打开第二个dump文件,第二次捕获内存快照时,由于两个局部变量引用的对象被释放,所以这两个局部变量不再作为 GC Roots ,从 Total Entries = 19 也可以看出(少了两个 GC Roots) - + @@ -717,13 +716,13 @@ public class GCRootsTest { 1、 - + 2、 - + - + 可以发现颜色变绿了,可以动态的看变化 @@ -731,11 +730,11 @@ public class GCRootsTest { 3、右击对象,选择 Show Selection In Heap Walker,单独的查看某个对象 - + - + @@ -743,13 +742,13 @@ public class GCRootsTest { 点击Show Paths To GC Roots,在弹出界面中选择默认设置即可 - + - + - + ### JProfiler 分析 OOM @@ -799,11 +798,11 @@ count = 6 1、看这个超大对象 - + 2、揪出 main() 线程中出问题的代码 - + @@ -838,7 +837,7 @@ count = 6 * 注意:标记的是被引用的对象,也就是可达对象,并非标记的是即将被清除的垃圾对象 2. 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收 - + @@ -877,7 +876,7 @@ count = 6 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收 - + 新生代里面就用到了复制算法,Eden区和S0区存活对象整体复制到S1区 @@ -907,7 +906,7 @@ count = 6 2. 老年代大量的对象存活,那么复制的对象将会有很多,效率会很低 3. 在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。 - + @@ -936,7 +935,7 @@ count = 6 2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。 - + @@ -1067,7 +1066,7 @@ A:无,没有最好的算法,只有最合适的算法 1. 一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。 3. 分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。 - + diff --git a/docs/Java/JVM/JVM系列-第11章-垃圾回收相关概念.md b/docs/Java/JVM/JVM系列-第11章-垃圾回收相关概念.md index b0f4975..64c5013 100644 --- a/docs/Java/JVM/JVM系列-第11章-垃圾回收相关概念.md +++ b/docs/Java/JVM/JVM系列-第11章-垃圾回收相关概念.md @@ -8,8 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第11章-垃圾回收相关概念。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: 4d401a8b date: 2020-11-17 12:33:24 --- @@ -118,7 +117,7 @@ JVM参数: 2、我也查过了大对象阈值的默认值 - + 我不太懂这个默认值为啥是0,我猜测可能是代表什么比例,目前也没有搜到相关的东西。这个不太重要,暂时就没有太深究,希望读者有知道的可以告知我一声。 @@ -186,11 +185,11 @@ Heap 1、来看看字节码:实例方法局部变量表第一个变量肯定是 this - + 2、你有没有看到,局部变量表的大小是 2。但是局部变量表里只有一个索引为0的啊?那索引为1的是哪个局部变量呢?实际上索引为1的位置是buffer在占用着,执行 System.gc() 时,栈中还有 buffer 变量指向堆中的字节数组,所以没有进行GC - + 3、那么这种代码块的情况,什么时候会被GC呢?我们来看第四个方法 @@ -218,11 +217,11 @@ A:局部变量表长度为 2 ,这说明了出了代码块时,buffer 就出 > 这点看不懂的可以看我前面的文章:虚拟机栈 --> Slot的重复利用 - + - + @@ -303,7 +302,7 @@ Heap 右边的图:后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开(图示中的Forgotten Reference Memory Leak),从而导致没有办法被回收。 - + @@ -447,7 +446,7 @@ Process finished with exit code -1 2. 并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换。由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行 - + @@ -462,7 +461,7 @@ Process finished with exit code -1 3. 适合科学计算,后台处理等弱交互场景 - + > **并发与并行的对比** @@ -483,7 +482,7 @@ Process finished with exit code -1 * 相较于并行的概念,单线程执行。 * 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收(单线程) - + @@ -493,7 +492,7 @@ Process finished with exit code -1 - 比如用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; 2. 典型垃圾回收器:CMS、G1 - + @@ -552,7 +551,7 @@ Process finished with exit code -1 1、一般的垃圾回收算法至少会划分出两个年代,年轻代和老年代。但是单纯的分代理论在垃圾回收的时候存在一个巨大的缺陷:为了找到年轻代中的存活对象,却不得不遍历整个老年代,反过来也是一样的。 - + 2、如果我们从年轻代开始遍历,那么可以断定N, S, P, Q都是存活对象。但是,V却不会被认为是存活对象,其占据的内存会被回收了。这就是一个惊天的大漏洞!因为U本身是老年代对象,而且有外部引用指向它,也就是说U是存活对象,而U指向了V,也就是说V也应该是存活对象才是!而这都是因为我们只遍历年轻代对象! @@ -600,7 +599,7 @@ Process finished with exit code -1 4. 这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。如下图,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。 - + @@ -662,7 +661,7 @@ Hello,尚硅谷 `StringBuffer str = new StringBuffer("hello,尚硅谷");` - + diff --git a/docs/Java/JVM/JVM系列-第12章-垃圾回收器.md b/docs/Java/JVM/JVM系列-第12章-垃圾回收器.md index 2e68209..6a8b74c 100644 --- a/docs/Java/JVM/JVM系列-第12章-垃圾回收器.md +++ b/docs/Java/JVM/JVM系列-第12章-垃圾回收器.md @@ -8,8 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第12章-垃圾回收器。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: 7706d61d date: 2020-11-19 18:33:24 --- @@ -45,7 +44,7 @@ GC 分类与性能指标 **按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器。** - + 1. 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。 1. 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中 @@ -61,7 +60,7 @@ GC 分类与性能指标 1. 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。 2. 独占式垃圾回收器(Stop the World)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。 - + @@ -106,7 +105,7 @@ GC 分类与性能指标 2. 这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的 3. 吞吐量优先,意味着在单位时间内,STW的时间最短:0.2+0.2=0.4 - + @@ -116,7 +115,7 @@ GC 分类与性能指标 - 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的 2. 暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5,但是总的GC时间可能会长 - + @@ -170,17 +169,17 @@ GC 分类与性能指标 2. 并行回收器:ParNew、Parallel Scavenge、Parallel old 3. 并发回收器:CMS、G1 - + **官方文档** - + **7款经典回收器与垃圾分代之间的关系** - + 1. 新生代收集器:Serial、ParNew、Parallel Scavenge; @@ -193,7 +192,7 @@ GC 分类与性能指标 ### 垃圾收集器的组合关系 - + @@ -252,11 +251,11 @@ jinfo -flag UseParallelOldGC 进程id JDK 8 中默认使用 ParallelGC 和 ParallelOldGC 的组合 - + #### JDK9 - + @@ -282,7 +281,7 @@ Serial 回收器:串行回收 这个收集器是一个单线程的收集器,“单线程”的意义:它只会使用一个CPU(串行)或一条收集线程去完成垃圾收集工作。更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World) - + @@ -314,7 +313,7 @@ ParNew 回收器:并行回收 2. ParNew 收集器除了采用**并行回收**的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。 3. ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。 - + 1. 对于新生代,回收次数频繁,使用并行方式高效。 2. 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源) @@ -362,7 +361,7 @@ Parallel 回收器:吞吐量优先 5. Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制。 - + 1. 在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在server模式下的内存回收性能很不错。 2. **在Java8中,默认是此垃圾收集器。** @@ -419,7 +418,7 @@ CMS 回收器:低延迟 ### CMS 工作原理(过程) - + CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记) @@ -439,7 +438,7 @@ CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶 3. 另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,**而是当堆内存使用率达到某一阈值时,便开始进行回收**,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次**“Concurrent Mode Failure”** 失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。 4. CMS收集器的垃圾收集算法采用的是**标记清除算法**,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,**不可避免地将会产生一些内存碎片**。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。 - + @@ -559,11 +558,11 @@ G1 回收器:区域化分代式 G1的分代,已经不是下面这样的了 - + G1的分区是这样的一个区域 - + **空间整合** @@ -656,7 +655,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不 > > 如图所示,可以将区域分配到Eden,幸存者和旧时代区域。 此外,还有第四种类型的物体被称为巨大区域。 这些区域旨在容纳标准区域大小的50%或更大的对象。 它们存储为一组连续区域。 最后,最后一种区域类型是堆的未使用区域。 - + @@ -668,7 +667,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不 **Regio的细节** - + 1. 每个Region都是通过指针碰撞来分配空间 2. G1为每一个Region设 计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。 @@ -687,7 +686,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: * 混合回收(Mixed GC) * (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。) - + 顺时针,Young GC --> Young GC+Concurrent Marking --> Mixed GC顺序,进行垃圾回收 @@ -732,7 +731,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: - + 1. 在回收 Region 时,为了不进行全堆的扫描,引入了 Remembered Set 2. Remembered Set 记录了当前 Region 中的对象被哪个对象引用了 @@ -749,7 +748,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: 2. 年轻代回收只回收Eden区和Survivor区 3. YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。 - + 图的大致意思就是: @@ -805,7 +804,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: 当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。 - + @@ -855,11 +854,11 @@ G1 GC的垃圾回收过程主要包括如下三个环节: 截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。 - + - + @@ -923,11 +922,11 @@ GC 日志分析 2、这个只会显示总的GC堆的变化,如下: - + 3、参数解析 - + @@ -939,11 +938,11 @@ GC 日志分析 2、输入信息如下 - + 3、参数解析 - + @@ -955,7 +954,7 @@ GC 日志分析 2、输出信息如下 - + 3、说明:日志带上了日期和时间 @@ -990,13 +989,13 @@ GC 日志分析 #### Young GC - + #### Full GC - + @@ -1030,11 +1029,11 @@ public class GCLogTest1 { 1、首先我们会将3个2M的数组存放到Eden区,然后后面4M的数组来了后,将无法存储,因为Eden区只剩下2M的剩余空间了,那么将会进行一次Young GC操作,将原来Eden区的内容,存放到Survivor区,但是Survivor区也存放不下,那么就会直接晋级存入Old 区 - + 2、然后我们将4M对象存入到Eden区中 - + 老年代图画的有问题,free应该是4M @@ -1057,7 +1056,7 @@ Process finished with exit code 0 ``` - + 与 JDK7 不同的是,JDK8 直接判定 4M 的数组为大对象,直接怼到老年区去了 @@ -1083,15 +1082,15 @@ GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等 在线分析网址:gceasy.io - + - + - + @@ -1127,7 +1126,7 @@ GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等 1. 停顿时间比其他几款收集器确实有了质的飞跃,但也未实现最大停顿时间控制在十毫秒以内的目标。 2. 而吞吐量方面出现了明显的下降,总运行时间是所有测试收集器里最长的。 - + @@ -1157,7 +1156,7 @@ GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等 **吞吐量** - + max-JOPS:以低延迟为首要前提下的数据 @@ -1167,13 +1166,13 @@ critical-JOPS:不考虑低延迟下的数据 **低延迟** - + 在ZGC的强项停顿时间测试上,它毫不留情的将Parallel、G1拉开了两个数量级的差距。无论平均停顿、95%停顿、998停顿、99. 98停顿,还是最大停顿时间,ZGC都能毫不费劲控制在10毫秒以内。 虽然ZGC还在试验状态,没有完成所有特性,但此时性能已经相当亮眼,用“令人震惊、革命性”来形容,不为过。未来将在服务端、大内存、低延迟应用的首选垃圾收集器。 - + @@ -1193,4 +1192,4 @@ critical-JOPS:不考虑低延迟下的数据 AliGC是阿里巴巴JVM团队基于G1算法,面向大堆(LargeHeap)应用场景。指定场景下的对比: - \ No newline at end of file + \ No newline at end of file diff --git a/docs/Java/JVM/JVM系列-第1章-JVM与Java体系结构.md b/docs/Java/JVM/JVM系列-第1章-JVM与Java体系结构.md index 893258b..b84c840 100644 --- a/docs/Java/JVM/JVM系列-第1章-JVM与Java体系结构.md +++ b/docs/Java/JVM/JVM系列-第1章-JVM与Java体系结构.md @@ -5,10 +5,10 @@ tags: - 虚拟机 categories: - JVM + - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第1章-JVM与Java体系结构。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: 8c954c6 date: 2020-11-02 11:51:56 --- @@ -38,7 +38,7 @@ date: 2020-11-02 11:51:56 3. 新项目上线,对各种JVM参数设置一脸茫然,直接默认吧然后就JJ了。 4. 每次面试之前都要重新背一遍JVM的一些原理概念性的东西,然而面试官却经常问你在实际项目中如何调优VM参数,如何解决GC、OOM等问题,一脸懵逼。 - + 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 @@ -50,7 +50,7 @@ date: 2020-11-02 11:51:56 1. 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,这其实是一种本末倒置的“病态”。 2. 如果我们把核心类库的API比做数学公式的话,那么Java虚拟机的知识就好比公式的推导过程。 - + - 计算机系统体系对我们来说越来越远,在不了解底层实现方式的前提下,通过高级语言很容易编写程序代码。但事实上计算机并不认识高级语言。 @@ -87,7 +87,7 @@ Java VS C++ 1. 垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率,但是,垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高扩展性应用和诊断运行时问题的基础,也是Java工程师进阶的必备能力。 2. C++语言需要程序员自己来分配内存和回收内存,对于高手来说可能更加舒服,但是对于普通开发者,如果技术实力不够,很容易造成内存泄漏。而Java全部交给JVM进行内存分配和回收,这也是一种趋势,减少程序员的工作量。 - + ## 什么人需要学JVM? @@ -103,26 +103,26 @@ Java VS C++ **英文文档规范**:https://docs.oracle.com/javase/specs/index.html - + **中文书籍:** - + > 周志明老师的这本书**非常推荐看**,不过只推荐看第三版,第三版较第二版更新了很多,个人觉得没必要再看第二版。 - + - + TIOBE排行榜 ----------- **TIOBE 排行榜**:https://www.tiobe.com/tiobe-index/ - + - 世界上没有最好的编程语言,只有最适用于具体应用场景的编程语言。 - 目前网上一直流传Java被python,go撼动Java第一的地位。学习者不需要太担心,Java强大的生态圈,也不是说是朝夕之间可以被撼动的。 @@ -148,14 +148,14 @@ Java-跨平台的语言 - + JVM-跨语言的平台 ------ - + @@ -187,7 +187,7 @@ JVM-跨语言的平台 2. 自己动手写一个Java虚拟机,难吗? 3. 天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣 - + Java发展重大事件 ------------ @@ -215,7 +215,7 @@ Java发展重大事件 ## Open JDK和Oracle JDK - + - 在JDK11之前,Oracle JDK中还会存在一些Open JDK中没有的,闭源的功能。但在JDK11中,我们可以认为Open JDK和Oracle JDK代码实质上已经达到完全一致的程度了。 - 主要的区别就是两者更新周期不一样 @@ -258,11 +258,11 @@ JVM的位置 JVM是运行在操作系统之上的,它与硬件没有直接的交互 - + - + @@ -275,7 +275,7 @@ JVM的整体结构 - + @@ -284,7 +284,7 @@ Java代码执行流程 凡是能生成被Java虚拟机所能解释、运行的字节码文件,那么理论上我们就可以自己设计一套语言了 - + diff --git a/docs/Java/JVM/JVM系列-第2章-类加载子系统.md b/docs/Java/JVM/JVM系列-第2章-类加载子系统.md index a15b433..a409621 100644 --- a/docs/Java/JVM/JVM系列-第2章-类加载子系统.md +++ b/docs/Java/JVM/JVM系列-第2章-类加载子系统.md @@ -5,10 +5,10 @@ tags: - 虚拟机 categories: - JVM + - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第2章-类加载子系统。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: 2e0079af date: 2020-11-02 21:31:58 --- @@ -25,19 +25,19 @@ date: 2020-11-02 21:31:58 ### 简图 - + ### 详细图 英文版 - + 中文版 - + 注意:方法区只有HotSpot虚拟机有,J9,JRockit都没有 @@ -59,7 +59,7 @@ date: 2020-11-02 21:31:58 3. **加载的类信息存放于一块称为方法区的内存空间**。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射) - + @@ -71,7 +71,7 @@ date: 2020-11-02 21:31:58 - + 类加载过程 ------- @@ -96,11 +96,11 @@ public class HelloLoader { * 加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main * 加载失败则抛出异常 - + 完整的流程图如下所示: - + @@ -142,7 +142,7 @@ public class HelloLoader { 使用 BinaryViewer软件查看字节码文件,其开头均为 CAFE BABE ,如果出现不合法的字节码文件,那么将会验证不通过。 - + #### 准备(Prepare) @@ -185,7 +185,7 @@ public class HelloApp { * 反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用 - + ### 初始化阶段 @@ -225,7 +225,7 @@ public class HelloApp { 查看下面这个代码的字节码,可以发现有一个`()`方法。 - + ```java public class ClassInitTest { @@ -277,15 +277,15 @@ public class ClassInitTest { **举例2:无 static 变量** - + 加上之后就有了 - + #### 4说明 - + 在构造器中: @@ -296,7 +296,7 @@ public class ClassInitTest { 若该类具有父类,JVM会保证子类的`()`执行前,父类的`()`已经执行完毕 - + 如上代码,加载流程如下: @@ -374,17 +374,17 @@ class DeadThread{ - + **ExtClassLoader** - + **AppClassLoader** - + @@ -579,15 +579,15 @@ public class CustomClassLoader extends ClassLoader { ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器) - + sun.misc.Launcher 它是一个java虚拟机的入口应用 - + #### 获取ClassLoader途径 - + @@ -640,7 +640,7 @@ Java虚拟机对class文件采用的是**按需加载**的方式,也就是说 3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。 4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常 - + ### 双亲委派机制代码演示 @@ -707,7 +707,7 @@ public class String { } ``` - + 由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误。 @@ -765,7 +765,7 @@ Process finished with exit code 1 - + diff --git a/docs/Java/JVM/JVM系列-第3章-运行时数据区.md b/docs/Java/JVM/JVM系列-第3章-运行时数据区.md index f7ef14b..6f2cab7 100644 --- a/docs/Java/JVM/JVM系列-第3章-运行时数据区.md +++ b/docs/Java/JVM/JVM系列-第3章-运行时数据区.md @@ -5,11 +5,12 @@ tags: - 虚拟机 categories: - JVM + - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第3章-运行时数据区。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' -date: 2020-11-9 15:38:42 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' +abbrlink: a7ad3cab +date: 2020-11-09 15:38:42 --- @@ -26,15 +27,15 @@ date: 2020-11-9 15:38:42 本节主要讲的是运行时数据区,也就是下图这部分,它是在类加载完成后的阶段 - + 当我们通过前面的:类的加载 --> 验证 --> 准备 --> 解析 --\> 初始化,这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区 - + 类比一下也就是大厨做饭,我们把大厨后面的东西(切好的菜,刀,调料),比作是运行时数据区。而厨师可以类比于执行引擎,将通过准备的东西进行制作成精美的菜品。 - + @@ -49,7 +50,7 @@ date: 2020-11-9 15:38:42 > 下图来自阿里巴巴手册JDK8 - + @@ -61,7 +62,7 @@ date: 2020-11-9 15:38:42 - 线程独有:独立包括程序计数器、栈、本地方法栈 - 线程间共享:堆、堆外内存(永久代或元空间、代码缓存) - + @@ -69,7 +70,7 @@ date: 2020-11-9 15:38:42 **每个JVM只有一个Runtime实例**。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。 - + @@ -113,7 +114,7 @@ PC寄存器介绍 > 官方文档网址:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html - + 1. JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,**寄存器存储指令相关的现场信息**。CPU只有把数据装载到寄存器才能够运行。 2. 这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。**JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟**。 @@ -131,7 +132,7 @@ PC寄存器介绍 PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。 - + @@ -267,7 +268,7 @@ SourceFile: "PCRegisterTest.java" * 左边的数字代表**指令地址(指令偏移)**,即 PC 寄存器中可能存储的值,然后执行引擎读取 PC 寄存器中的值,并执行该指令 - + @@ -281,7 +282,7 @@ SourceFile: "PCRegisterTest.java" 2. JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令 - + @@ -305,7 +306,7 @@ CPU 时间片 3. 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,**每个程序轮流执行**。 - + @@ -313,7 +314,7 @@ CPU 时间片 ## 本地方法 - + @@ -392,7 +393,7 @@ Java使用起来非常方便,然而有些层次的任务用Java实现起来不 4. 本地方法一般是使用C语言或C++语言实现的。 5. 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。 - + diff --git a/docs/Java/JVM/JVM系列-第4章-虚拟机栈.md b/docs/Java/JVM/JVM系列-第4章-虚拟机栈.md index b34a791..8dfab92 100644 --- a/docs/Java/JVM/JVM系列-第4章-虚拟机栈.md +++ b/docs/Java/JVM/JVM系列-第4章-虚拟机栈.md @@ -5,10 +5,11 @@ tags: - 虚拟机 categories: - JVM + - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第4章-虚拟机栈。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' +abbrlink: 5b1b6560 date: 2020-11-10 10:38:42 --- @@ -30,7 +31,7 @@ date: 2020-11-10 10:38:42 1. 首先栈是运行时的单位,而堆是存储的单位。 2. 即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放哪里 - + @@ -65,7 +66,7 @@ date: 2020-11-10 10:38:42 - + - 虚拟机栈的生命周期 - 生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了 @@ -89,7 +90,7 @@ date: 2020-11-10 10:38:42 - 对于栈来说不存在垃圾回收问题 - 栈不需要GC,但是可能存在OOM - + ### 虚拟机栈的异常 @@ -163,7 +164,7 @@ Exception in thread "main" java.lang.StackOverflowError **设置栈参数之后** - + 部分输出结果 @@ -201,7 +202,7 @@ Exception in thread "main" java.lang.StackOverflowError 4. 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。 - + 1. **不同线程中所包含的栈帧是不允许存在相互引用的**,即不可能在一个栈帧之中引用另外一个线程的栈帧。 2. 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。 @@ -229,11 +230,11 @@ Exception in thread "main" java.lang.StackOverflowError - 一些附加信息 - + 并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的 - + 局部变量表 ------- @@ -311,7 +312,7 @@ public class LocalVariablesTest { } ``` - + 看完字节码后,可得结论:所以局部变量表所需的容量大小是在编译期确定下来的。 @@ -323,29 +324,29 @@ public class LocalVariablesTest { 1、0-15 也就是有16行字节码 - + 2、方法异常信息表 - + 3、Misc - + 4、行号表 Java代码的行号和字节码指令行号的对应关系 - + 5、注意:生效行数和剩余有效行数都是针对于字节码文件的行数 - + 1、图中圈的东西表示该局部变量的作用域 @@ -369,7 +370,7 @@ Java代码的行号和字节码指令行号的对应关系 6. 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量) 7. 如果当前帧是由构造方法或者实例方法创建的,那么**该对象引用this将会存放在index为0的slot处**,其余的参数按照参数表顺序继续排列。(this也相当于一个变量) - + ### Slot代码示例 @@ -387,7 +388,7 @@ Java代码的行号和字节码指令行号的对应关系 局部变量表:this 存放在 index = 0 的位置 - + @@ -407,7 +408,7 @@ Java代码的行号和字节码指令行号的对应关系 weight 为 double 类型,index 直接从 3 蹦到了 5 - + @@ -450,7 +451,7 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 局部变量 c 重用了局部变量 b 的 slot 位置 - + @@ -498,13 +499,13 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 - 比如:执行复制、交换、求和等操作 - + - + @@ -535,7 +536,7 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 - + 局部变量表就相当于食材 @@ -570,23 +571,23 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 10 return ``` - + ### 一步一步看流程 1、首先执行第一条语句,PC寄存器指向的是0,也就是指令地址为0,然后使用bipush让操作数15入操作数栈。 - + 2、执行完后,PC寄存器往下移,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表1的位置(istore_1),我们可以看到局部变量表的已经增加了一个元素。并且操作数栈为空了 * 解释为什么局部变量表索引从 1 开始,因为该方法为实例方法,局部变量表索引为 0 的位置存放的是 this - + 3、然后PC下移,指向的是下一行。让操作数8也入栈,同时执行store操作,存入局部变量表中 - + @@ -594,11 +595,11 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 iload_1:取出局部变量表中索引为1的数据入操作数栈 - + 5、然后将操作数栈中的两个元素执行相加操作,并存储在局部变量表3的位置 - + @@ -606,7 +607,7 @@ iload_1:取出局部变量表中索引为1的数据入操作数栈 **关于类型转换的说明** - + @@ -615,7 +616,7 @@ iload_1:取出局部变量表中索引为1的数据入操作数栈 - + - m改成800之后,byte存储不了,就成了short型,sipush 800 @@ -644,11 +645,11 @@ iload_1:取出局部变量表中索引为1的数据入操作数栈 getSum() 方法字节码指令:最后带着个 ireturn - + testGetSum() 方法字节码指令:一上来就加载 getSum() 方法的返回值() - + @@ -840,7 +841,7 @@ SourceFile: "DynamicLinkingTest.java" - + @@ -1135,7 +1136,7 @@ interface MethodInterface { Son 类中 show() 方法的字节码指令如下 - + @@ -1175,7 +1176,7 @@ public class Lambda { } ``` - + @@ -1232,7 +1233,7 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 如图所示:如果类中重写了方法,那么调用的时候,就会直接在该类的虚方法表中查找 - + 1、比如说son在调用toString的时候,Son没有重写过,Son的父类Father也没有重写过,那就直接调用Object类的toString。那么就直接在虚方法表里指明toString直接指向Object类。 @@ -1242,24 +1243,24 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 **例子2** - + - + - + - + 方法返回地址 -------- - + > 在一些帖子里,方法返回地址、动态链接、一些附加信息 也叫做帧数据区 @@ -1308,7 +1309,7 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 2. 方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码 - + @@ -1320,7 +1321,7 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 * target :出现异常跳转至地址为 11 的指令执行 * type :捕获异常的类型 - + diff --git a/docs/Java/JVM/JVM系列-第5章-堆.md b/docs/Java/JVM/JVM系列-第5章-堆.md index b210ab4..1ba46e8 100644 --- a/docs/Java/JVM/JVM系列-第5章-堆.md +++ b/docs/Java/JVM/JVM系列-第5章-堆.md @@ -8,8 +8,8 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第5章-堆。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' +abbrlink: 50ac3a1c date: 2020-11-11 20:38:42 --- @@ -27,7 +27,7 @@ date: 2020-11-11 20:38:42 1. 堆针对一个JVM进程来说是唯一的。也就是**一个进程只有一个JVM实例**,一个JVM实例中就有一个运行时数据区,一个运行时数据区只有一个堆和一个方法区。 2. 但是**进程包含多个线程,他们是共享同一堆空间的**。 - + @@ -68,7 +68,7 @@ public class SimpleHeap { } ``` - + @@ -88,13 +88,13 @@ public class SimpleHeap { 约定:新生区 <–> 新生代 <–> 年轻代 、 养老区 <–> 老年区 <–> 老年代、 永久区 <–\> 永久代 - + 2. 堆空间内部结构,JDK1.8之前从永久代 替换成 元空间 - + @@ -122,17 +122,17 @@ public class HeapDemo { 1、双击jdk目录下的这个文件 - + 2、工具 -> 插件 -> 安装Visual GC插件 - + 3、运行上面的代码 - + @@ -215,7 +215,7 @@ public class HeapSpaceInitial { 设置下参数再看 - + ```java public class HeapSpaceInitial { @@ -250,7 +250,7 @@ public class HeapSpaceInitial { **方式一: jps / jstat -gc 进程id** - + > jps:查看java进程 > @@ -285,7 +285,7 @@ OU: 老年代使用的量 **方式二:-XX:+PrintGCDetails** - + @@ -333,11 +333,11 @@ Process finished with exit code 1 2、堆内存变化图 - + 3、原因:大对象导致堆内存溢出 - + @@ -357,9 +357,9 @@ Process finished with exit code 1 3、其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区) - + - + - 配置新生代与老年代在堆结构的占比 @@ -383,7 +383,7 @@ Process finished with exit code 1 - + @@ -439,7 +439,7 @@ public class EdenSurvivorTest { 1、我们创建的对象,一般都是存放在Eden区的,**当我们Eden区满了后,就会触发GC操作**,一般被称为 YGC / Minor GC操作 - + 2、当我们进行一次垃圾收集后,红色的对象将会被回收,而绿色的独享还被占用着,存放在S0(Survivor From)区。同时我们给每个对象设置了一个年龄计数器,经过一次回收后还存在的对象,将其年龄加 1。 @@ -453,11 +453,11 @@ public class EdenSurvivorTest { > > 3、也就是说s0区和s1区在互相转换。 - + 4、我们继续不断的进行对象生成和垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,也就是将年轻代中的对象晋升到老年代中 - + 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。 @@ -475,7 +475,7 @@ public class EdenSurvivorTest { * 那万一老年代都放不下,则先触发FullGC ,再看看能不能放下,放得下最好,但如果还是放不下,那只能报 OOM 3. 如果 Eden 区满了,将对象往幸存区拷贝时,发现幸存区放不下啦,那只能便宜了某些新对象,让他们直接晋升至老年区 - + ### 常用调优工具 @@ -528,7 +528,7 @@ GC分类 3. Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行 - + @@ -649,7 +649,7 @@ Heap * 新生代:有Eden、两块大小相同的survivor(又称为from/to或s0/s1)构成,to总为空。 * 老年代:存放新生代中经历多次GC仍然存活的对象。 - + @@ -661,7 +661,7 @@ Heap - + @@ -711,7 +711,7 @@ TLAB(Thread Local Allocation Buffer) 2. 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为**快速分配策略**。 3. 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。 - + 1、每个线程都有一个TLAB空间 @@ -741,7 +741,7 @@ TLAB(Thread Local Allocation Buffer) **TLAB 分配过程** - + diff --git a/docs/Java/JVM/JVM系列-第6章-方法区.md b/docs/Java/JVM/JVM系列-第6章-方法区.md index e548959..6338502 100644 --- a/docs/Java/JVM/JVM系列-第6章-方法区.md +++ b/docs/Java/JVM/JVM系列-第6章-方法区.md @@ -8,8 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第6章-方法区。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: 136cd965 date: 2020-11-13 19:38:42 --- @@ -25,7 +24,7 @@ date: 2020-11-13 19:38:42 ThreadLocal:如何保证多个线程在并发环境下的安全性?典型场景就是数据库连接管理,以及会话管理。 - + **栈、堆、方法区的交互关系** @@ -36,7 +35,7 @@ ThreadLocal:如何保证多个线程在并发环境下的安全性?典型场 3. 真正的 person 对象存放在 Java 堆中 4. 在 person 对象中,有个指针指向方法区中的 person 类型数据,表明这个 person 对象是用方法区中的 Person 类 new 出来的 - + 方法区的理解 -------- @@ -48,7 +47,7 @@ ThreadLocal:如何保证多个线程在并发环境下的安全性?典型场 1. 《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。 3. 所以,**方法区可以看作是一块独立于Java堆的内存空间**。 - + @@ -88,7 +87,7 @@ public class MethodAreaDemo { 简单的程序,加载了1600多个类 - + @@ -104,7 +103,7 @@ public class MethodAreaDemo { 5. 永久代、元空间二者并不只是名字变了,内部结构也调整了 6. 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常 - + @@ -121,7 +120,7 @@ public class MethodAreaDemo { 2. -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M 3. 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。 - + ### JDK8及以后(元空间) @@ -229,11 +228,11 @@ Exception in thread "main" java.lang.OutOfMemoryError: Metaspace #### 概念 - + 《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的**类型信息、常量、静态变量、即时编译器编译后的代码缓存**等。 - + @@ -715,7 +714,7 @@ public static int count; - + 1. 方法区,内部包含了运行时常量池 2. 字节码文件,内部包含了常量池。(之前的字节码文件中已经看到了很多Constant pool的东西,这个就是常量池) @@ -729,7 +728,7 @@ public static int count; 1. 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外。还包含一项信息就是**常量池表**(**Constant Pool Table**),包括各种字面量和对类型、域和方法的符号引用。 2. 字面量: 10 , “我是某某”这种数字和字符串都是字面量 - + **为什么需要常量池?** @@ -750,7 +749,7 @@ public static int count; 2. 比如说我们这个文件中有6个地方用到了"hello"这个字符串,如果不用常量池,就需要在6个地方全写一遍,造成臃肿。我们可以将"hello"等所需用到的结构信息记录在常量池中,并通过**引用的方式**,来加载、调用所需的结构 4. 这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。 - + **常量池中有啥?** @@ -925,69 +924,69 @@ SourceFile: "MethodAreaDemo.java" 1、初始状态 - + 2、首先将操作数500压入操作数栈中 - + 3、然后操作数 500 从操作数栈中取出,存储到局部变量表中索引为 1 的位置 - + 4、 - + 5、 - + 6、 - + 7、 - + 8、 - + 9、 - + 10、 - + 11、图片写错了是#25和#26(获得System类) - + 12、 - + 13、 - + 15、执行加法运算后,将计算结果放在操作数栈顶 - + 16、就是真正的打印 - + 17、 - + @@ -1022,7 +1021,7 @@ SourceFile: "MethodAreaDemo.java" 方法区由永久代实现,使用 JVM 虚拟机内存(虚拟的内存) - + @@ -1030,7 +1029,7 @@ SourceFile: "MethodAreaDemo.java" 方法区由永久代实现,使用 JVM 虚拟机内存 - + @@ -1038,7 +1037,7 @@ SourceFile: "MethodAreaDemo.java" 方法区由元空间实现,使用物理机本地内存 - + @@ -1096,15 +1095,15 @@ public class StaticFieldTest { JDK6环境下 -image-20201113224231761 +image-20201113224231761 JDK7环境下 - + JDK8环境 - + @@ -1151,7 +1150,7 @@ public class StaticObjTest { 4、测试发现:三个对象的数据在内存中的地址都落在Eden区范围内,所以结论:**只要是对象实例必然会在Java堆中分配**。 - + > 1、0x00007f32c7800000(Eden区的起始地址) ---- 0x00007f32c7b50000(Eden区的终止地址) > @@ -1163,7 +1162,7 @@ public class StaticObjTest { 5、接着,找到了一个引用该staticObj对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过Inspector查看该对象实例,可以清楚看到这确实是一个java.lang.Class类型的对象实例,里面有一个名为staticobj的实例字段: - + 从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,**存储于Java堆之中**,从我们的实验中也明确验证了这一点 @@ -1218,7 +1217,7 @@ public class StaticObjTest { - + @@ -1272,7 +1271,7 @@ public class BufferTest { 直接占用了 1G 的本地内存 - + @@ -1282,13 +1281,13 @@ public class BufferTest { 原来采用BIO的架构,在读写本地文件时,我们需要从用户态切换成内核态 - + **直接缓冲区(NIO)** NIO 直接操作物理磁盘,省去了中间过程 - + ### 直接内存与 OOM @@ -1352,7 +1351,7 @@ Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory - + diff --git a/docs/Java/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md b/docs/Java/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md index 82d08f9..dfa2b04 100644 --- a/docs/Java/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md +++ b/docs/Java/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md @@ -8,8 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第7章-对象的实例化内存布局与访问定位。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: debff71a date: 2020-11-14 19:38:42 --- @@ -37,7 +36,7 @@ date: 2020-11-14 19:38:42 - + ### 对象创建的方式 @@ -212,7 +211,7 @@ class Account{ 对象的内存布局 --------- - + @@ -243,7 +242,7 @@ class Account{ 图解内存布局 - + @@ -252,7 +251,7 @@ class Account{ **JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?** - + 定位,通过栈上reference访问 @@ -263,7 +262,7 @@ class Account{ 1. 缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低 2. 优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改 - + @@ -272,4 +271,4 @@ class Account{ 1. 优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据 2. 缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值 - \ No newline at end of file + \ No newline at end of file diff --git a/docs/Java/JVM/JVM系列-第8章-执行引擎.md b/docs/Java/JVM/JVM系列-第8章-执行引擎.md index 053c148..beae0b9 100644 --- a/docs/Java/JVM/JVM系列-第8章-执行引擎.md +++ b/docs/Java/JVM/JVM系列-第8章-执行引擎.md @@ -8,8 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第8章-执行引擎。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: 408712f4 date: 2020-11-15 19:48:42 --- @@ -24,7 +23,7 @@ date: 2020-11-15 19:48:42 - + ### 执行引擎概述 @@ -35,7 +34,7 @@ date: 2020-11-15 19:48:42 3. JVM的主要任务是负责**装载字节码到其内部**,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。 4. 那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是**将字节码指令解释/编译为对应平台上的本地机器指令才可以**。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。 - + 1、前端编译:从Java程序员-字节码文件的这个过程叫前端编译 @@ -52,7 +51,7 @@ date: 2020-11-15 19:48:42 3. 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。 4. 从外观上来看,所有的Java虚拟机的执行引擎输入、处理、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行、即时编译的等效过程,输出的是执行过程。 - + @@ -72,18 +71,18 @@ Java代码编译和执行过程 - + 3. javac编译器(前端编译器)流程图如下所示: - + 4. Java字节码的执行是由JVM执行引擎来完成,流程图如下所示 - + @@ -106,7 +105,7 @@ Java代码编译和执行过程 **用图总结一下** - + 机器码 指令 汇编语言 ------------- @@ -165,7 +164,7 @@ Java代码编译和执行过程 - + @@ -194,7 +193,7 @@ Java代码编译和执行过程 2. 汇编过程:实际上指把汇编语言代码翻译成目标机器指令的过程。 - + @@ -212,7 +211,7 @@ Java代码编译和执行过程 3. 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。 - + @@ -289,7 +288,7 @@ Java代码编译和执行过程 2. 在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的1/8。曾经有这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机,此故障说明了JIT的存在。—**阿里团队** - + @@ -318,7 +317,7 @@ public class JITTest { 通过 JVisualVM 查看 JIT 编译器执行的编译次数 - + @@ -370,7 +369,7 @@ public class JITTest { * 如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。 * 如果未超过阈值,则使用解释器对字节码文件解释执行 - + @@ -388,7 +387,7 @@ public class JITTest { 它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。 - + @@ -402,7 +401,7 @@ public class JITTest { - + diff --git a/docs/Java/JVM/JVM系列-第9章-StringTable(字符串常量池).md b/docs/Java/JVM/JVM系列-第9章-StringTable(字符串常量池).md index 59d44aa..95c3112 100644 --- a/docs/Java/JVM/JVM系列-第9章-StringTable(字符串常量池).md +++ b/docs/Java/JVM/JVM系列-第9章-StringTable(字符串常量池).md @@ -8,8 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第9章-StringTable(字符串常量池)。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/JVM/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/JVM/logo.png' abbrlink: ee2ba71e date: 2020-11-16 12:38:02 --- @@ -167,11 +166,11 @@ str 的内容并没有变:“test ok” 位于字符串常量池中的另一 4. 在JDK7中,StringTable的长度默认值是60013,StringTablesize设置没有要求 5. 在JDK8中,StringTable的长度默认值是60013,StringTable可以设置的最小值为1009 - + - + @@ -277,11 +276,11 @@ String 的内存分配 - + - + @@ -383,23 +382,23 @@ public class StringTest4 { 1、程序启动时已经加载了 2293 个字符串常量 - + 2、加载了一个换行符(println),所以多了一个 - + 3、加载了字符串常量 “1”~“9” - + 4、加载字符串常量 “10” - + 5、之后的字符串"1" 到 "10"不会再次加载 - + @@ -426,7 +425,7 @@ class Memory { 分析运行时内存(foo() 方法是实例方法,其实图中少了一个 this 局部变量) - + @@ -494,7 +493,7 @@ class Memory { IDEA 反编译 class 文件后,来看这个问题 - + @@ -930,7 +929,7 @@ public class StringNewTest { 5. `23 ldc #8 ` :在字符串常量池中放入 “b”(如果之前字符串常量池中没有 “b” 的话) 6. `31 invokevirtual #9 ` :调用 StringBuilder 的 toString() 方法,会生成一个 String 对象 - + @@ -989,13 +988,13 @@ JDK6 :正常眼光判断即可 * new String() 即在堆中 * str.intern() 则把字符串放入常量池中 - + JDK7及后续版本,**注意大坑** - + @@ -1054,11 +1053,11 @@ public class StringExer1 { **JDK6** -![image-20201116113423492](https://cdn.jsdelivr.net/gh/youthlql/lql_img_002/JVM/chapter_009/0015.png) +![image-20201116113423492](https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.1.0/JVM/chapter_009/0015.png) **JDK7/8** - + @@ -1081,7 +1080,7 @@ public class StringExer1 { } ``` - + **练习3** @@ -1170,11 +1169,11 @@ public class StringIntern2 { arr[i] = new String(String.valueOf(data[i % data.length])); ``` - + - + 2、使用 intern() 方法:由于数组中字符串的引用都指向字符串常量池中的字符串,所以程序需要维护的 String 对象更少,内存占用也更低 @@ -1183,11 +1182,11 @@ arr[i] = new String(String.valueOf(data[i % data.length])); arr[i] = new String(String.valueOf(data[i % data.length])).intern(); ``` - + - + @@ -1219,11 +1218,11 @@ public class StringGCTest { * Number of entries 和 Number of literals 明显没有 100000 * 以上两点均说明 StringTable 区发生了垃圾回收 - + - + diff --git a/docs/Java/collection/HashMap-JDK7源码讲解.md b/docs/Java/collection/HashMap-JDK7源码讲解.md index e0478e5..3225448 100644 --- a/docs/Java/collection/HashMap-JDK7源码讲解.md +++ b/docs/Java/collection/HashMap-JDK7源码讲解.md @@ -8,9 +8,9 @@ categories: - HashMap keywords: Java集合,HashMap。 description: HashMap-JDK7源码讲解。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_Basis/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' -date: 2020-11-1 10:21:58 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_Basis/logo.png' +abbrlink: f1f58db2 +date: 2020-11-01 10:21:58 --- @@ -202,7 +202,7 @@ hadoop2 大致是这样的一个结构 - + - 每个链表就算哈希表的桶(bucket) - 链表的节点值就算一个键值对 @@ -294,7 +294,8 @@ public class HashMap /* - 1、扩容阈值(threshold):当哈希表的大小【就是上面的size】 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的 容量) + 1、扩容阈值(threshold):当哈希表的大小【就是上面的size】 ≥ 扩容阈值时,就会扩容哈希表 + (即扩充HashMap的容量) 2、扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数 3、扩容阈值 = 容量 x 加载因子 */ @@ -397,7 +398,7 @@ static class Entry implements Map.Entry { `HashMap`中的数组元素 & 链表节点 采用 `Entry`类实现 - + 1、一个正方形代表一个Entry对象,同时也代表一个键值对。 @@ -456,8 +457,10 @@ static class Entry implements Map.Entry { /* 设置扩容阈值 = 初始容量 - 1、注意:此处不是真正的阈值,仅是为了接收参数初始容量大小(capacity)、加载因子(Load factor),并没 有真正初始化哈希表,即初始化存储数组table - 2、真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时,下面会详细说明。 + 1、注意:此处不是真正的阈值,仅是为了接收参数初始容量大小(capacity)、加载因子(Load factor), + 并没有真正初始化哈希表,即初始化存储数组table + 2、真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时,下面会 + 详细说明。 */ threshold = initialCapacity; @@ -495,14 +498,17 @@ static class Entry implements Map.Entry { public V put(K key, V value) { /* ① - 1、若哈希表未初始化(即 table为空),则调用inflateTable方法,使用构造函数时设置的阈值(即初始容量)初 始化数组table + 1、若哈希表未初始化(即 table为空),则调用inflateTable方法,使用构造函数时设置的阈值 + (即初始容量)初始化数组table */ if (table == EMPTY_TABLE) { inflateTable(threshold); } /* ② 1、判断key是否为空值null - 2、若key == null,则调用putForNullKey方法,putForNullKey方法最终将该键-值存放到数组table中的第1 个位置,即table[0]。本质:key = Null时,hash值 = 0,故存放到table[0]中)该位置永远只有1个value, 新传进来的value会覆盖旧的value + 2、若key == null,则调用putForNullKey方法,putForNullKey方法最终将该键-值存放到数组 + table中的第1个位置,即table[0]。本质:key = Null时,hash值 = 0,故存放到table[0]中) + 该位置永远只有1个value,新传进来的value会覆盖旧的value 3、k != null往下走 */ if (key == null) @@ -515,12 +521,14 @@ public V put(K key, V value) { int i = indexFor(hash, table.length); /* ③ - 1、通过遍历以该数组元素为头结点的链表,逐个判断是否发生hash冲突,同时判断该key对应的值是否已存在 + 1、通过遍历以该数组元素为头结点的链表,逐个判断是否发生hash冲突,同时判断该key对应的值是 + 否已存在 */ for (Entry e = table[i]; e != null; e = e.next) { Object k; /* ④ - 1、如果发生了hash冲突,且key也相等。则用新value替换旧value(此时说明发生了更新的情况),注意这里 强调的是发生了hash冲突并且key也相等。 + 1、如果发生了hash冲突,且key也相等。则用新value替换旧value(此时说明发生了更新的情况), + 注意这里强调的是发生了hash冲突并且key也相等。 */ if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; @@ -550,14 +558,16 @@ public V put(K key, V value) { private void inflateTable(int toSize) { /* - 将传入的容量大小转化为:>传入容量大小的最小的2的次幂,即如果传入的是容量大小是18,那么转化后,初始化容量 大小为32(即2的5次幂) + 将传入的容量大小转化为:>传入容量大小的最小的2的次幂,即如果传入的是容量大小是18,那么转化后, + 初始化容量大小为32(即2的5次幂) */ int capacity = roundUpToPowerOf2(toSize); //重新计算阈值 threshold = 容量 * 加载因子 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); /* - 使用计算后的初始容量(已经是2的次幂) 初始化数组table(作为数组长度)即 哈希表的容量大小 = 数组大小(长 度) + 使用计算后的初始容量(已经是2的次幂) 初始化数组table(作为数组长度)即 哈希表的容量大小 = + 数组大小(长度) */ table = new Entry[capacity]; initHashSeedAsNeeded(capacity); @@ -646,7 +656,8 @@ private V putForNullKey(V value) { ## indexFor() ```java -//这里h & (length-1)的意思就是hash值与数组长度取模。只是因为数组长度是特殊的2的幂,所以这个等价关系刚好成立 + //这里h & (length-1)的意思就是hash值与数组长度取模。只是因为数组长度是特殊的2的幂, + //所以这个等价关系刚好成立 static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); @@ -758,15 +769,15 @@ void transfer(Entry[] newTable, boolean rehash) { 大概画了一下图: - + - + - + - + @@ -860,7 +871,7 @@ void transfer(Entry[] newTable, boolean rehash) { **hashmap初始状态** - + @@ -886,7 +897,7 @@ void transfer(Entry[] newTable, boolean rehash) { **两个线程调用完毕之后,hashmap目前是这样的。** - + @@ -907,9 +918,9 @@ void transfer(Entry[] newTable, boolean rehash) { 3、来看下此时内存里的状态 - + - + ## 步骤4 @@ -946,7 +957,7 @@ void transfer(Entry[] newTable, boolean rehash) { 2、线程2直接**扩容完毕**,那么完成后的状态是这样【假设e2和e3还是hash到同一个位置】 - + 3、线程1还是原来的状态 @@ -956,11 +967,11 @@ void transfer(Entry[] newTable, boolean rehash) { 目前两个线程里的新数组是这样的 - + 为了方便后面观看,我画成这样。 - + @@ -1004,7 +1015,7 @@ void transfer(Entry[] newTable, boolean rehash) { 也就变成了下面这个样子。 - + @@ -1050,7 +1061,7 @@ void transfer(Entry[] newTable, boolean rehash) { 执行完,变成这样。 - + @@ -1066,7 +1077,7 @@ void transfer(Entry[] newTable, boolean rehash) { 3、执行pos_3: newTable[i] = e得到 newTable1[3] == e2 - + 这样就形成了循环链表,再get()数据就会陷入死循环。 @@ -1118,7 +1129,8 @@ void transfer(Entry[] newTable, boolean rehash) { //根据key值,通过hash()计算出对应的hash值 int hash = (key == null) ? 0 : hash(key); - //根据hash值计算出对应的数组下标,遍历以该数组下标的数组元素为头结点的链表所有节点,寻找该key对应的值 + //根据hash值计算出对应的数组下标,遍历以该数组下标的数组元素为头结点的链表所有节点, + //寻找该key对应的值 for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; diff --git a/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md b/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md index bc4fe65..ea5a97c 100644 --- a/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md +++ b/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md @@ -8,9 +8,9 @@ categories: - HashMap keywords: Java集合,HashMap。 description: HashMap-JDK8源码讲解及常见面试题。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_Basis/logo.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' -date: 2020-11-1 10:22:05 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_Basis/logo.png' +abbrlink: cbc5672a +date: 2020-11-01 10:22:05 --- @@ -27,7 +27,7 @@ date: 2020-11-1 10:22:05 在JDK8中,优化了HashMap的数据结构,引入了红黑树。即HashMap的数据结构:数组+链表+红黑树。HashMap变成了这样。 - + ### 为什么要引入红黑树 @@ -146,16 +146,22 @@ date: 2020-11-1 10:22:05 //与红黑树相关的参数 - //单链表(桶)的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树 + /* + 1、单链表(桶)的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时, + 则将链表转换成红黑树 + */ static final int TREEIFY_THRESHOLD = 8; /* - 1、桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计 算),在重新计算存储位置后,当原有的红黑树内节点数量 < 6时,则将 红黑树转换成链表 + 1、桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据 + 存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内节点数量 < 6时,则将 红黑树转换 + 成链表 */ static final int UNTREEIFY_THRESHOLD = 6; /* - 1、最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)。否则,若 (单链表)桶内元素太多时,则直接扩容,而不是树形化。 + 1、最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)。 + 否则,若 (单链表)桶内元素太多时,则直接扩容,而不是树形化。 2、为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD */ static final int MIN_TREEIFY_CAPACITY = 64; @@ -205,8 +211,12 @@ public class HashMap // 设置加载因子 this.loadFactor = loadFactor; - // 设置扩容阈值 - // 此处不是真正的阈值,仅仅只是将传入的容量大小转化为:>传入容量大小的最小的2的幂,该阈值后面会重新计算 + + /* + 1、设置扩容阈值 + 2、此处不是真正的阈值,仅仅只是将传入的容量大小转化为:>传入容量大小的最小的2的幂, + 该阈值后面会重新计算 + */ this.threshold = tableSizeFor(initialCapacity); } @@ -342,7 +352,8 @@ Process finished with exit code 0 /* - 1、若哈希表的数组tab为空,则通过resize()进行初始化,所以,初始化哈希表的时机就是第1次调用put函数时, 即调用resize() 初始化创建。 + 1、若哈希表的数组tab为空,则通过resize()进行初始化,所以,初始化哈希表的时机就是第1次 + 调用put函数时,即调用resize() 初始化创建。 */ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; @@ -358,12 +369,13 @@ Process finished with exit code 0 else { Node e; K k; - //判断 table[i]的元素的key是否与需插入的key一样,若相同则直接用新value覆盖旧value【即更新操作】 + //判断 table[i]的元素的key是否与需插入的key一样,若相同则直接用新value覆盖旧value + //【即更新操作】 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; - //继续判断:需插入的数据结构是否为红黑树 or 链表。若是红黑树,则直接在树中插入 or 更新键值对 + //继续判断:需插入的数据结构是否为红黑树or链表。若是红黑树,则直接在树中插入or更新键值对 else if (p instanceof TreeNode) /* 1、putTreeVal作用:向红黑树插入 or 更新数据(键值对) @@ -377,9 +389,10 @@ Process finished with exit code 0 else { /* 过程: - 1、遍历table[i],判断Key是否已存在:采用equals()对比当前遍历节点的key 与 需插入数据的 key:若已存在,则直接用新value覆盖旧value - 2、遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据(尾插法) - 3、新增节点后,需判断链表长度是否>8(8 = 桶的树化阈值):若是,则把链表转换为红黑树 + 1、遍历table[i],判断Key是否已存在:采用equals()对比当前遍历节点的key 与 + 需插入数据的key:若已存在,则直接用新value覆盖旧value + 2、遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据(尾插法) + 3、新增节点后,需判断链表长度是否>8(8 = 桶的树化阈值):若是,则把链表转换为红黑树 */ for (int binCount = 0; ; ++binCount) { //对于2情况的操作 尾插法插入尾部 @@ -432,7 +445,8 @@ Process finished with exit code 0 int h; /* 1、当key = null时,hash值 = 0,所以HashMap的key可为null - 2、当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后对哈希码进行扰动处理。高位参与 低位的运算:h ^ (h >>> 16) + 2、当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后对哈希码进行扰动处理。 + 高位参与低位的运算:h ^ (h >>> 16) */ return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); @@ -442,7 +456,7 @@ Process finished with exit code 0 JDK8 hash的运算原理:高位参与低位运算,使得hash更加均匀。 - + @@ -555,7 +569,7 @@ JDK8 hash的运算原理:高位参与低位运算,使得hash更加均匀。 JDK8扩容时,数据在数组下标的计算方式 - + * `JDK8`根据此结论作出的新元素存储位置计算规则非常简单,提高了扩容效率。 diff --git a/docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md b/docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md index 48900f6..26630e2 100644 --- a/docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md +++ b/docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md @@ -6,10 +6,10 @@ tags: - 源码 categories: - Java并发 + - 原理 keywords: Java并发,原理,源码 description: 万字系列长文讲解Java并发-第一阶段-多线程基础知识。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png' abbrlink: efc79183 date: 2020-10-05 22:40:58 --- @@ -31,13 +31,13 @@ date: 2020-10-05 22:40:58 概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。 说明:线程作为CPU调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。 - + 补充: - + 进程可以细化为多个线程。 每个线程,拥有自己独立的:栈、程序计数器 @@ -96,7 +96,10 @@ public class ThreadTest { //问题一:我们不能通过直接调用run()的方式启动线程。 // t1.run(); - //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException + /* + 问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。 + 会报IllegalThreadStateException + */ // t1.start(); //我们需要重新创建一个线程的对象 MyThread t2 = new MyThread(); @@ -158,7 +161,11 @@ public class ThreadTest1 { //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1 = new Thread(mThread); t1.setName("线程1"); - //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run() + + /* + 5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()--> + 调用了Runnable类型的target的run() + */ t1.start(); //再启动一个线程,遍历100以内的偶数 @@ -260,7 +267,8 @@ private void init(ThreadGroup g, Runnable target, String name, tid = nextThreadID(); } -/*如果你是实现了runnable接口,那么在上面的代码中target便不会为null,那么最终就会通过重写的规则去调用真正实现了Runnable接口(你之前传进来的那个Runnable接口实现类)的类里的run方法*/ +/*如果你是实现了runnable接口,那么在上面的代码中target便不会为null,那么最终就会通过重写的 +规则去调用真正实现了Runnable接口(你之前传进来的那个Runnable接口实现类)的类里的run方法*/ @Override public void run() { @@ -273,7 +281,7 @@ private void init(ThreadGroup g, Runnable target, String name, 1、多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理。 2、在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread类的构造方法传递了一个Runnable接口对象的时候,那么该接口对象将被Thread类中的target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法。而这个run()方法去调用实现了Runnable接口的那个类所重写过run()方法,进而执行相应的逻辑。多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。如下图所示: - + @@ -361,7 +369,8 @@ private void init(ThreadGroup g, Runnable target, String name, tid = nextThreadID(); } -/*由于这里是通过继承Thread类来实现的线程,那么target这个东西就是Null。但是因为你继承了Runnable接口并且重写了run(),所以最终还是调用子类的run()*/ +/*由于这里是通过继承Thread类来实现的线程,那么target这个东西就是Null。但是因为你继承 +了Runnable接口并且重写了run(),所以最终还是调用子类的run()*/ @Override public void run() { if (target != null) { @@ -635,7 +644,7 @@ public void run() { 1、如果直接调用run()方法,相当于就是简单的调用一个普通方法。 - + 2、run()的调用是在start0()这个Native C++方法里调用的 @@ -645,11 +654,11 @@ public void run() { Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态,这几个状态在Java源码中用枚举来表示。 - + 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示。 - + > 图中 wait到 runnable状态的转换中,`join`实际上是`Thread`类的方法,但这里写成了`Object`。 @@ -690,7 +699,7 @@ public static void main(String[] args) { 2、当JVM启动后,实际有多个线程,但是至少有一个非守护线程(比如main线程)。 - + - Finalizer:GC守护线程 @@ -780,7 +789,8 @@ public static void main(String[] args) { } /* -设置该线程为守护线程必须在启动它之前。如果t.start()之后,再t.setDaemon(true);会抛出IllegalThreadStateException +设置该线程为守护线程必须在启动它之前。如果t.start()之后,再t.setDaemon(true); +会抛出IllegalThreadStateException */ ``` @@ -872,7 +882,108 @@ public static void main(String[] args) throws InterruptedException { # 中断 -记住中断只是一个状态,Java的方法可以选择对这个中断进行响应,也可以选择不响应。响应的意思就是写相对应的代码执行相对应的操作,不响应的意思就是什么代码都不写。 +1、Java 中的中断和操作系统的中断还不一样,这里就按照**状态**来理解吧,不要和操作系统的中断联系在一起 + +2、记住中断只是一个状态,Java的方法可以选择对这个中断进行响应,也可以选择不响应。响应的意思就是写相对应的代码执行相对应的操作,不响应的意思就是什么代码都不写。 + +## 几个方法 + +```java +// Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态 +public boolean isInterrupted() {} + +/* +1、Thread 中的静态方法,检测调用这个方法的线程是否已经中断 +2、注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为 false +如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了 +*/ +public static boolean interrupted() {} + +// Thread 类中的实例方法,用于设置一个线程的中断状态为 true +public void interrupt() {} +``` + + + + + +## 小tip + + + +```java +public static boolean interrupted() + +public boolean isInterrupted()//这个会清除中断状态 + +``` + +为什么要这么设置呢?原因在于: + +* interrupted()是一个静态方法,可以在Runnable接口实例中使用 +* isInterrupted()是一个Thread的实例方法,在重写Thread的run方法时使用 + + + +```java +public class ThreadInterrupt { + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + System.out.println(Thread.interrupted()); + }); //这个new Thread用的是runnable接口那个构造函数 + + Thread t2 = new Thread(){ + @Override + public void run() { + System.out.println(isInterrupted()); + } + };//这个new Thread用的就是Thread的空参构造 + + } +} + +``` + +也就是说接口中不能调用Thread的实例方法,只能通过静态方法来判断是否发生中断 + + + +## 重难点 + +当然,中断除了是线程状态外,还有其他含义,否则也不需要专门搞一个这个概念出来了。 + +> 初学者肯定以为 thread.interrupt() 方法是用来暂停线程的,主要是和它对应中文翻译的“中断”有关。中断在并发中是常用的手段,请大家一定好好掌握。可以将中断理解为线程的状态,它的特殊之处在于设置了中断状态为 true 后,这几个方法会感知到: +> +> 1. wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int) +> +> 这些方法都有一个共同之处,方法签名上都有`throws InterruptedException`,这个就是用来响应中断状态修改的。 +> +> 2. 如果线程阻塞在 InterruptibleChannel 类的 IO 操作中,那么这个 channel 会被关闭。 +> +> 3. 如果线程阻塞在一个 Selector 中,那么 select 方法会立即返回。 +> +> 对于以上 3 种情况是最特殊的,因为他们能自动感知到中断(这里说自动,当然也是基于底层实现),**并且在做出相应的操作后都会重置中断状态为 false**。然后执行相应的操作(通常就是跳到 catch 异常处)。 +> +> 如果不是以上3种情况,那么,线程的 interrupt() 方法被调用,会将线程的中断状态设置为 true。 +> +> 那是不是只有以上 3 种方法能自动感知到中断呢?不是的,如果线程阻塞在 LockSupport.park(Object obj) 方法,也叫挂起,这个时候的中断也会导致线程唤醒,但是唤醒后不会重置中断状态,所以唤醒后去检测中断状态将是 true。 + +> 资料: [Oracle官方文档](https://docs.oracle.com/javase/specs/index.html) ---> [ The Java® Language Specification Java SE 8 Edition](https://docs.oracle.com/javase/specs/jls/se8/html/index.html) ---> 第17章 Threads and Locks + + + +## InterruptedException + +它是一个特殊的异常,不是说 JVM 对其有特殊的处理,而是它的使用场景比较特殊。通常,我们可以看到,像 Object 中的 wait() 方法,ReentrantLock 中的 lockInterruptibly() 方法,Thread 中的 sleep() 方法等等,这些方法都带有 `throws InterruptedException`,我们通常称这些方法为阻塞方法(blocking method)。 + +阻塞方法一个很明显的特征是,它们需要花费比较长的时间(不是绝对的,只是说明时间不可控),还有它们的方法结束返回往往依赖于外部条件,如 wait 方法依赖于其他线程的 notify,lock 方法依赖于其他线程的 unlock等等。 + +当我们看到方法上带有 `throws InterruptedException` 时,我们就要知道,这个方法应该是阻塞方法,我们如果希望它能早点返回的话,我们往往可以通过中断来实现。 + +除了几个特殊类(如 Object,Thread等)外,感知中断并提前返回是通过轮询中断状态来实现的。我们自己需要写可中断的方法的时候,就是通过在合适的时机(通常在循环的开始处)去判断线程的中断状态,然后做相应的操作(通常是方法直接返回或者抛出异常)。当然,我们也要看到,如果我们一次循环花的时间比较长的话,那么就需要比较长的时间才能**感知**到线程中断了。 + + + ## wait()中断测试 @@ -912,17 +1023,9 @@ public static void main(String[] args) { > 注释掉e.printStackTrace();的输出 > > false //pos_4 -> true //pos_5 t.isInterrupted()之后会立即清除中断状态 +> true //pos_5 > wait响应中断 //pos_1 -> false //pos_3 因为pos_5清除了中断状态,所以这里检测到就是flase,没有被中断过 - -* 当该线程在wait()、join()、sleep(long, int)状态时,如果被打断,则会收到一个异常提醒。因为这些方法都抛出了 - - ```throws InterruptedException``` 这个异常,通过try catch可以做相应的处理。 - -* 但是只要线程被打断,无论哪个方法都可以通过`isInterrupted()`方法检测到打断的状态。 - - +> false //pos_3 @@ -956,47 +1059,6 @@ try { -## 两个判断中断状态的方法 - - - -```java -public static boolean interrupted() - -public boolean isInterrupted()//这个会清除中断状态 - -``` - -为什么要这么设置呢?原因在于: - -* interrupted()是一个静态方法,可以在Runnable接口实例中使用 -* isInterrupted()是一个Thread的实例方法,在重写Thread的run方法时使用 - - - -```java -public class ThreadInterrupt { - public static void main(String[] args) throws InterruptedException { - Thread t1 = new Thread(() -> { - System.out.println(Thread.interrupted()); - }); //这个new Thread用的是runnable接口那个构造函数 - - Thread t2 = new Thread(){ - @Override - public void run() { - System.out.println(isInterrupted()); - } - };//这个new Thread用的就是Thread的空参构造 - - } -} - -``` - -也就是说接口中不能调用Thread的实例方法,只能通过静态方法来判断是否发生中断 - - - # 关闭线程 ## 优雅的关闭(通过一个Boolean) @@ -1494,13 +1556,10 @@ volatile自己虽然不能保证原子性,但是和CAS结合起来就可以保 ## CAS 是什么? -- CAS:比较并交换,它是一条 CPU 并发原语 - 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子性的。 +- CAS:比较并交换compareAndSet,它是一条 CPU 并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子性的。 + +- 例: AtomicInteger 的 compareAndSet('期望值','设置值') 方法,期望值与目标值一致时,修改目标变量为设置值,期望值与目标值不一致时,返回 false 和最新主存的变量值 -- 例: AtomicInteger 的 compareAndSet('期望值','设置值') 方法 - 期望值与目标值一致时,修改目标变量为设置值 - 期望值与目标值不一致时,返回 false 和最新主存的变量值 - - CAS 的底层原理 例: AtomicInteger.getAndIncrement() 调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令 @@ -1515,7 +1574,7 @@ volatile自己虽然不能保证原子性,但是和CAS结合起来就可以保 - + @@ -1723,7 +1782,7 @@ public Unsafe getUnsafe() throws IllegalAccessException { Unsafe的功能如下图: - + ## CAS相关 diff --git a/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md index bf2233a..0bae6f6 100644 --- a/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md +++ b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md @@ -6,12 +6,12 @@ tags: - 源码 categories: - Java并发 + - 原理 keywords: Java并发,原理,源码 description: 万字系列长文讲解-Java并发体系-第三阶段-JUC并发包。JUC在高并发编程中使用频率非常高,这里会详细介绍其用法。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png' abbrlink: 5be45d9e -date: 2020-10-19 22:13:58 +date: 2020-10-09 22:13:58 --- @@ -1869,7 +1869,7 @@ public class ForkJoinRecursiveAction { ForkJoinTask就是ForkJoinPool里面的每一个任务。他主要有两个子类:`RecursiveAction`和`RecursiveTask`。然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果。 - + ## 小总结 diff --git a/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md index c905faa..ed7bfef 100644 --- a/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md +++ b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md @@ -6,12 +6,12 @@ tags: - 源码 categories: - Java并发 + - 原理 keywords: Java并发,原理,源码 description: 万字系列长文讲解-Java并发体系-第三阶段-JUC并发包。JUC在高并发编程中使用频率非常高,这里会详细介绍其用法。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png' abbrlink: 70c90e5d -date: 2020-10-19 22:13:58 +date: 2020-10-10 22:13:58 --- @@ -563,7 +563,7 @@ public int getUnarrivedParties() 根据上面的代码,我们可以画出下面这个很简单的图: - + 这棵树上有 7 个 phaser 实例,每个 phaser 实例在构造的时候,都指定了 parties 为 5,但是,对于每个拥有子节点的节点来说,每个子节点都是它的一个 party,我们可以通过 phaser.getRegisteredParties() 得到每个节点的 parties 数量: @@ -896,32 +896,26 @@ Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 E **七大参数** -- corePoolSize - 线程池中的常驻核心线程数 +- corePoolSize 线程池中的常驻核心线程数 创建线程池后,当有请求任务进来,就安排池中的线程去执行请求任务 - 当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列中 - + 当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列中 + - maximumPoolSize 线程池能够容纳同时执行的最大线程数,此值必须大于等于1 -- keepAliveTime - 多余的空闲线程的存活时间 +- keepAliveTime 多余的空闲线程的存活时间 当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时, - 多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止 - + 多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止 + - unit keepAliveTime 的单位 - workQueue 任务队列,被提交但尚未被执行的任务 -- threadFactory - 表示生成线程池中工作线程的线程工厂<线程名字、线程序数...> - 用于创建线程一般用默认的即可 - -- handler - 拒接策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时 - 如何拒绝新的任务 +- threadFactory,表示生成线程池中工作线程的线程工厂<线程名字、线程序数...>,用于创建线程一般用默认的即可 + +- handler,拒接策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何拒绝新的任务 @@ -958,7 +952,7 @@ public class ThreadPoolDemo { ## 线程池的底层工作流程 - + 1、创建线程池后,等待请求任务 @@ -1652,7 +1646,7 @@ public class ExecutorCompletionService implements CompletionService { **执行流程:** - + diff --git a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md index 10c3d2e..69a4a5f 100644 --- a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md +++ b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md @@ -6,12 +6,12 @@ tags: - 源码 categories: - Java并发 + - 原理 keywords: Java并发,原理,源码 description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。' -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png' abbrlink: 230c5bb3 -date: 2020-10-19 22:09:58 +date: 2020-10-06 22:09:58 --- @@ -291,7 +291,7 @@ d = e - f ; 由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。 - + @@ -343,7 +343,7 @@ int c = a + b; 冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。 - + 输入设备:鼠标,键盘等等 @@ -367,7 +367,7 @@ int c = a + b; CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内 存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。靠近CPU 的缓存称为L1,然后依次是 L2,L3和主内存,CPU缓存模型如图下图所示。 - + CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越小。速度越快的价格越贵。 @@ -377,7 +377,7 @@ CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速 3、L3 Cache是三级缓存中大的一级,例如12MB,同时也是缓存中慢的一级,在同一个CPU插槽 之间的核共享一个L3 Cache。 - + 上面的图中有一个Latency指标。比如Memory这个指标为59.4ns,表示CPU在操作内存的时候有59.4ns的延迟,一级缓存最快只有1.2ns。 @@ -409,7 +409,7 @@ Cache的出现是为了解决CPU直接访问内存效率低下问题的。 每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操 作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接 访问对方工作内存中的变量。 - + Java的线程不能直接在主内存中操作共享变量。而是首先将主内存中的共享变量赋值到自己的工作内存中,再进行操作,操作完成之后,刷回主内存。 @@ -424,7 +424,7 @@ Java内存模型是一套在多线程读写共享数据时,对共享数据的 JMM内存模型与CPU硬件内存架构的关系: - + 工作内存:可能对应CPU寄存器,也可能对应CPU缓存,也可能对应内存。 @@ -434,9 +434,9 @@ JMM内存模型与CPU硬件内存架构的关系: ## 再谈可见性 - + - + 1、图中所示是 个双核 CPU 系统架构 ,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的1级缓存,在有些架构里面还有1个所有 CPU 共享的2级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 或者 L2 存或者 CPU 寄存器。 @@ -452,7 +452,7 @@ JMM内存模型与CPU硬件内存架构的关系: 为了保证数据交互时数据的正确性,Java内存模型中定义了8种操作来完成这个交互过程,这8种操作本身都是原子性的。虚拟机实现时必须保证下面 提及的每一种操作都是原子的、不可再分的。 - + > (1)lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。 > @@ -478,7 +478,7 @@ JMM内存模型与CPU硬件内存架构的关系: 如果没有synchronized,那就是下面这样的 - + @@ -589,7 +589,7 @@ volatile不保证原子性,只保证可见性和禁止指令重排 ## CPU术语介绍 - + @@ -717,7 +717,7 @@ public class VolatileExample { **1、下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图** - + > 图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任 意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确 实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时, 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率 @@ -725,7 +725,7 @@ public class VolatileExample { **2、下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图** - + > 图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。 LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。 上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。 @@ -752,7 +752,7 @@ class VolatileBarrierExample { 针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化 - + ​ 注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器通常会在这里插入一个StoreLoad屏障。 @@ -766,7 +766,7 @@ class VolatileBarrierExample { X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作 做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。 - + diff --git a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md index b43fce3..25e0948 100644 --- a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md +++ b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md @@ -6,19 +6,19 @@ tags: - 源码 categories: - Java并发 + - 原理 keywords: Java并发,原理,源码 description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。' -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png' abbrlink: '8210870' -date: 2020-10-19 22:10:58 +date: 2020-10-07 22:10:58 --- # 可见性设计的硬件 - + 从硬件的级别来考虑一下可见性的问题 @@ -305,13 +305,13 @@ MESI协议规定了一组消息,就说各个处理器在操作内存数据的 - + ## MESI-优化 - + ​ MESI协议如果每次写数据的时候都要发送invalidate消息等待所有处理器返回ack,然后获取独占锁后才能写数据,那可能就会导致性能很差了,因为这个对共享变量的写操作,实际上在硬件级别变成串行的了。所以为了解决这个问题,硬件层面引入了写缓冲器和无效队列 @@ -449,7 +449,7 @@ int b = c; //load ## 相关术语 - + @@ -461,7 +461,7 @@ int b = c; //load **第一个机制是通过总线锁保证原子性。**如果多个处理器同时对共享变量进行读改写操作 (i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操 作就不是原子的,操作完之后共享变量的值会和期望的不一致。举个例子,如果i=1,我们进行 两次i++操作,我们期望的结果是3,但是有可能结果是2,如图所示。 - + ​ 原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入 系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享 变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。 diff --git a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md index 83639fe..5212d5c 100644 --- a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md +++ b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md @@ -6,12 +6,12 @@ tags: - 源码 categories: - Java并发 + - 原理 keywords: Java并发,原理,源码 description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。' -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png' abbrlink: 113a3931 -date: 2020-10-19 22:10:58 +date: 2020-10-08 22:10:58 --- @@ -357,17 +357,17 @@ monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保 术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html 在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示: - + ## 对象头 当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是 存在锁对象的对象头中的。 HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp - + 从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot 源码中的 oop.hpp 文件中。 - + - 在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata @@ -383,21 +383,21 @@ monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保 Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类 型是 markOop 。源码位于 markOop.hpp 中。 - + - + 在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下: - + 在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下: - + 再加一个图对比一下,有一丁点的补充 - + @@ -447,7 +447,7 @@ Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode) 线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个`Lock Record`,其包括一个用于存储对象头中的 `mark word`(官方称之为`Displaced Mark Word`)以及一个指向对象的指针。下图右边的部分就是一个`Lock Record`。 - + @@ -571,7 +571,7 @@ synchronized(obj){ } ``` - + ## 轻量级锁什么时候升级为重量级锁? @@ -608,7 +608,7 @@ synchronized(obj){ - 重量级锁的状态下,对象的`mark word`为指向一个堆中monitor对象的指针。一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。 - + 在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚 拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主 要数据结构如下: @@ -661,7 +661,7 @@ ObjectMonitor() { 1、执行monitorenter时,会调用InterpreterRuntime.cpp (位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函 数。具体代码可参见HotSpot源码。 - + @@ -1147,7 +1147,7 @@ if (TryLock(Self) > 0) break ; 可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数, 执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。 用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构: - + 从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。 内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。 用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存 储资源、I/O资源等。 系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。 diff --git a/docs/Java/concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md b/docs/Java/concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md index b34090f..66bf896 100644 --- a/docs/Java/concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md +++ b/docs/Java/concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md @@ -5,10 +5,11 @@ tags: - AQS源码 categories: - Java并发 + - 原理 keywords: Java并发,AQS源码 description: '万字系列长文讲解-Java并发体系-第四阶段-AQS源码解读-[1]。' -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' -top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png' +abbrlink: 92c4503d date: 2020-10-26 17:59:42 --- @@ -26,8 +27,8 @@ date: 2020-10-26 17:59:42 *

* 可重入锁: * 1、可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。 - * 2、是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象), - * 不会因为之前已经获取过还没释放而阻塞 + * 2、是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个 + * 对象),不会因为之前已经获取过还没释放而阻塞 */ public class ReEnterLockDemo { @@ -307,8 +308,8 @@ LockSupport底层还是UNSAFE(前面讲过)。 - 形象的理解 线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。 当调用park方法时 - *如果有凭证,则会直接消耗掉这个凭证然后正常退出; - *如果无凭证,就必须阻塞等待凭证可用; + 如果有凭证,则会直接消耗掉这个凭证然后正常退出; + 如果无凭证,就必须阻塞等待凭证可用; 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。 我们用LockSupport来测试下之前的异常场景 @@ -409,11 +410,11 @@ Process finished with exit code 0 **技术翻译:**是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量`state`表示持有锁的状态。 - + - + AbstractOwnableSynchronizer AbstractQueuedLongSynchronizer @@ -425,7 +426,7 @@ AbstractQueuedSynchronizer AQS是一个抽象的父类,可以将其理解为一个框架。基于AQS这个框架,我们可以实现多种同步器,比如下方图中的几个Java内置的同步器。同时我们也可以基于AQS框架实现我们自己的同步器以满足不同的业务场景需求。 - + @@ -433,7 +434,7 @@ AQS是一个抽象的父类,可以将其理解为一个框架。基于AQS这 加锁会导致阻塞:有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理 - + 1、抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种**排队等候机制**,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。 @@ -514,7 +515,7 @@ Node 的数据结构其实也挺简单的,就是 thread + waitStatus + pre + n ## AQS队列基本结构 - + 注意排队队列,不包括head(也就是后文要说的哨兵节点)。 @@ -582,7 +583,7 @@ public class AQSDemo { 以这样的一个实际例子说明。 - + @@ -643,7 +644,8 @@ public class AQSDemo { public final void acquire(int arg) { // 此时 arg == 1 /* - 1、首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要进队 列排队了。 + 1、首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要 + 进队列排队了。 2、有可能成功的情况就是,在走到这一步的时候,前面占锁的线程刚好释放锁 */ if (!tryAcquire(arg) && @@ -712,7 +714,8 @@ public class AQSDemo { /* - 1、假设tryAcquire(arg) 返回false,那么代码将执行:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这个方法,首先需要执行:addWaiter(Node.EXCLUSIVE) + 1、假设tryAcquire(arg) 返回false,那么代码将执行:acquireQueued(addWaiter(Node.EXCLUSIVE), + arg),这个方法,首先需要执行:addWaiter(Node.EXCLUSIVE) 2、此方法的作用是把线程包装成node,同时进入到队列中。参数mode此时是Node.EXCLUSIVE,代表独占模式 3、以下几行代码想把当前node加到链表的最后面去,也就是进到队列的最后 */ @@ -728,7 +731,8 @@ public class AQSDemo { // 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为排队队列新的尾巴 if (compareAndSetTail(pred, node)) { /* - 1、进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了 + 1、进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,上面已经有 + node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了 */ pred.next = node; // 线程入队了,可以返回了 @@ -761,7 +765,7 @@ public class AQSDemo { 2、C在if逻辑里准备入队,进行相应设置后,变成下面这样。 - + @@ -778,7 +782,8 @@ public class AQSDemo { Node t = tail; /* - 1、进入这个分支,说明是队列为空的这种情况,那么就准备初始化一个空的节点(new Node())作为排队队列 的head。 + 1、进入这个分支,说明是队列为空的这种情况,那么就准备初始化一个空的节点(new Node()) + 作为排队队列的head。 */ if (t == null) { // Must initialize /* @@ -786,15 +791,22 @@ public class AQSDemo { 2、还是一步CAS,因为可能是很多线程同时进来呢 */ if (compareAndSetHead(new Node())) - /* - 1、注意这里传的参数是new Node(),说明是一个空的节点(并不是我们B线程封装的节点,这 个空节点只作为占位符,称作傀儡节点或者哨兵节点)。这个时候head节点的waitStatus==0, 看 new Node()构造方法就知道了。注意:new Node()虽然是空节点,但他不是null - 2、这个时候有了head,但是tail还是null,设置一下,把tail指向head,放心,马上就有线程要 来了,到时候tail就要被抢了 - 3、注意:这里只是设置了tail=head,这里可没return哦。所以,设置完了以后,继续for循环, 下次就到下面的else分支了 - */ + /* + 1、注意这里传的参数是new Node(),说明是一个空的节点(并不是我们B线程封装的节点, + 这个空节点只作为占位符,称作傀儡节点或者哨兵节点)。这个时候head节点的waitStatus==0, + 看new Node()构造方法就知道了。注意:new Node()虽然是空节点,但他不是null + 2、这个时候有了head,但是tail还是null,设置一下,把tail指向head,放心,马上就有 + 线程要来了,到时候tail就要被抢了 + 3、注意:这里只是设置了tail=head,这里可没return哦。所以,设置完了以后,继续for + 循环,下次就到下面的else分支了 + */ tail = head; } else { /* - 1、下面几行,和上一个方法 addWaiter 是一样的,只是这个套在无限循环里,就是将当前线程排到 队尾,有线程竞争的话排不上重复排,直到排上了再return 【这里看不懂的话就看下面的例子】 */ + 1、下面几行,和上一个方法 addWaiter 是一样的,只是这个套在无限循环里,就是将当前 + 线程排到队尾,有线程竞争的话排不上重复排,直到排上了再return + 【这里看不懂的话就看下面的例子】 + */ node.prev = t; if (compareAndSetTail(t, node)) { @@ -819,7 +831,7 @@ public class AQSDemo { 此时队列变成了下面的样子: - + 3、然后if结束之后,继续空的for循环,B线程开始了第二轮循环。 @@ -831,11 +843,11 @@ public class AQSDemo { 2、`node.prev = t`,进入if之后,让B节点的prev指针指向t,然后`compareAndSetTail(t, node)`设置尾节点 - + 3、CAS设置尾节点成功之后,执行if里的逻辑 - + @@ -851,7 +863,8 @@ public class AQSDemo { && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); 2、acquireQueued这个方法,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入排队队列队尾 - 3、注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,意味着上面这段代码将 进入selfInterrupt() + 3、注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,意味着上面这段 + 代码将进入selfInterrupt() 4、这个方法非常重要,真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了 */ final boolean acquireQueued(final Node node, int arg) { @@ -861,10 +874,13 @@ public class AQSDemo { for (;;) { final Node p = node.predecessor(); /* - 1、p == head 说明当前节点虽然进到了排队队列,但是是队列的第一个,因为它的前驱是head(或者说 是哨兵节点,因为head指向了哨兵节点) + 1、p == head 说明当前节点虽然进到了排队队列,但是是队列的第一个,因为它的前驱是head + (或者说是哨兵节点,因为head指向了哨兵节点) 2、注意,队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为排队队列 3、所以当前节点可以去试抢一下锁 - 4、这里我们说一下,为什么可以去试试:它是排队队列队头,所以作为队头,可以去试一试能不能拿到 锁,因为可能之前的线程已经释放锁了。如果尝试成功,那它就不需要被挂起,直接拿锁,效率会高 + 4、这里我们说一下,为什么可以去试试:它是排队队列队头,所以作为队头,可以去试一试能不能 + 拿到锁,因为可能之前的线程已经释放锁了。如果尝试成功,那它就不需要被挂起,直接拿锁, + 效率会高 5、tryAcquire已经分析过了, 忘记了请往前看一下,就是简单用CAS试操作一下state */ if (p == head && tryAcquire(arg)) { @@ -915,7 +931,9 @@ public class AQSDemo { /* 1、前驱节点 waitStatus大于0 ,之前说过,大于0说明前驱节点取消了排队。 - 2、这里需要知道这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。所以下面这块代码说 的是将当前节点的prev指向waitStatus<=0的节点,简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如 果前驱节点取消了排队,找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。 + 2、这里需要知道这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。所以 + 下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,简单说,就是为了找个好爹,因为你还 + 得依赖它来唤醒呢,如果前驱节点取消了排队,找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。 */ if (ws > 0) { /* @@ -935,8 +953,13 @@ public class AQSDemo { /* 1、如果进入到这个分支意味着什么,前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0 - 2、正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0,用CAS将前驱节点的 waitStatus设置为Node.SIGNAL(也就是-1),表示我后面有节点需要被唤醒。 - 3、这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思,Doug Lea 注释的是:代表后继节点需要 被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。这里涉及的是两个问 题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码。 + 2、正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0,用CAS将前驱节点 + 的waitStatus设置为Node.SIGNAL(也就是-1),表示我后面有节点需要被唤醒。 + 3、这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思,Doug Lea 注释的是:代表后继 + 节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态, + 我们知道,每个node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。 + 这里涉及的是两个问 题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 + “waitStatus代表后继节点的状态”这种思路去看一遍源码。 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } @@ -993,11 +1016,15 @@ public class AQSDemo { } /* 1、接下来说说如果shouldParkAfterFailedAcquire(p, node)返回false的情况 - 2、仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回 true的,原因很简单,前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说,我都还没给前驱设置-1呢,怎么 可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。 + 2、仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会 + 返回true的,原因很简单,前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说,我都还没给前驱 + 设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。 3、解释下为什么shouldParkAfterFailedAcquire(p, node)返回false的时候不直接挂起线程: 主要是为了应对在经过这个方法后,node已经是head的直接后继节点了。 - 4、假设返回fasle的时候,node已经是head的直接后继节点了,但是你直接挂起了线程,就要走别人唤醒你的那几步代 码。那这里完全可以重新走一遍for循环,直接尝试下获取锁,可能会更快。注意是可能,不代表一定,因为你也无法确定 unparkSuccessor释放锁,通知后继节点这个方法执行的快慢。但是你多尝试一次获取锁,总归是快的。 + 4、假设返回fasle的时候,node已经是head的直接后继节点了,但是你直接挂起了线程,就要走别人唤醒你的那 + 几步代码。那这里完全可以重新走一遍for循环,直接尝试下获取锁,可能会更快。注意是可能,不代表一定,因为 + 你也无法确定unparkSuccessor释放锁,通知后继节点这个方法执行的快慢。但是你多尝试一次获取锁,总归是快的。 for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { @@ -1074,7 +1101,8 @@ private void unparkSuccessor(Node node) { if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* - 1、下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)从队尾往前找,找到 waitStatus<=0的所有节点中排在最前面的 + 1、下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)从队尾往前找, + 找到waitStatus<=0的所有节点中排在最前面的 */ Node s = node.next; if (s == null || s.waitStatus > 0) { @@ -1169,7 +1197,7 @@ protected final void setExclusiveOwnerThread(Thread thread) { - +