From 17fa15cfa139418054c31410c1cb843d9c097d4f Mon Sep 17 00:00:00 2001 From: sihai <1446037005@qq.com> Date: Thu, 15 Jul 2021 16:30:09 +0800 Subject: [PATCH 01/17] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 59 +++++++++++++++---- ...21\351\235\242\350\257\225\351\242\230.md" | 33 +++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 "docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" diff --git a/README.md b/README.md index cb0e7c3..7e1c3a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ **[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,春招目前拿到了大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:https://sihai.blog.csdn.net,春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 @@ -8,12 +8,23 @@ **一起冲!!!** -#### Java面试思维导图预警 +#### Java学习资源汇总(个人总结) -这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,先来瞧瞧。 +1、Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 + +![](http://image.ouyangsihai.cn/FodtVWogwlUu5DhV1dZuAEhBznf7) + +2、面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi + +3、大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 + +4、面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo + +#### Java面试思维导图(手打) + +这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,预览在下方,先来瞧瞧。 ![](http://image.ouyangsihai.cn/FtJ3PbBRdNSa1NaUr96JmUq24_yS) -![](http://image.ouyangsihai.cn/FsTv2aMsGcv8M9Rsyx6POBOGKY1g) **划重点**:想要`导图源文件`,更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 @@ -90,25 +101,47 @@ ## 项目准备 -- [如何进行项目的自我介绍呢?](docs/interview/自我介绍和项目介绍.md) - -- [项目需要准备必备知识及方法](docs/project/秒杀项目总结.md) +- [我的个人项目介绍模板](docs/interview/自我介绍和项目介绍.md) +- [本人真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!](https://mp.weixin.qq.com/s/JBtCRaw9Nabk9aIImjkFIQ) +- [项目必备知识及解决方案](docs/project/秒杀项目总结.md) ## 面试知识点 -- [各公司面试知识点汇总](docs/interview-experience/面试常见知识.md) -- [面试常见问题分类汇总](docs/interview-experience/面试常见问题分类汇总.md) +- [各大公司面试知识点汇总](docs/interview-experience/各大公司面经.md) +- [Java后端面试常见问题分类汇总(高频考点)](docs/interview-experience/面试常见问题分类汇总.md) ## 公司面经 - [2020年各公司面试经验汇总](docs/interview-experience/各大公司面经.md) +- [最新!!招银网络科技Java面经,整理附答案](https://mp.weixin.qq.com/s/HAUOH-EYS_3Ho2XxYkTGXA) +- [拿了 30K 的 offer!](https://mp.weixin.qq.com/s/R4gZ8IuskxgxA1SZwfCOoA) +- [重磅面经!!四面美团最终拿到了 offer](https://mp.weixin.qq.com/s/P1mDcH5hEXqNp2Jpz5Qjmg) +- [十面阿里,七面头条](https://mp.weixin.qq.com/s/FErQnLvYnuZxiaDkYWPO5A) ## Java ### 基础 -- [Java基础系列文章](https://mp.weixin.qq.com/mp/homepage?__biz=MzI2OTQ4OTQ1NQ==&hid=1&sn=c455e51f87eaa9c12d6b45e0e4d33960&scene=1&devicetype=iOS13.3.1&version=17000c2b&lang=zh_CN&nettype=WIFI&ascene=7&session_us=gh_2bfc34fbf760&fontScale=100&wx_header=1) +这几篇文章虽然是基础,但是确实深入理解基础,如果你能很好的理解这些基础,那么对于Java基础面试题也是没有什么问题的,背面试题不如理解原理,很重要。 + +- [Java基础思维导图](http://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483823&idx=1&sn=4588a874055e8ca54f2bbe1ede12cff4&scene=19#wechat_redirect) +- [Java基础(一) 深入解析基本类型](http://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483948&idx=1&sn=cb0ae3d82a1629e3a0538b6f31e2473b&scene=19#wechat_redirect) +- [Java基础(二) 自增自减与贪心规则](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483951&idx=1&sn=af5b54ed2e26d975f96643d9dfd66fab&scene=19#wechat_redirect) +- [Java基础(三) 加强型for循环与Iterator](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483952&idx=1&sn=43130fdf815970e0e12347d057c6b24f&scene=19#wechat_redirect) +- [Java基础(四) java运算顺序的深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483955&idx=1&sn=abfb3e8ac31cb84bb78216d9c953abc0&scene=19#wechat_redirect) +- [Java基础(五) String性质深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483956&idx=1&sn=1c19164967621fa5449a7830d006c8f9&scene=19#wechat_redirect) +- [Java基础(六) switch语句的深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483999&idx=1&sn=092ad983f87798360ef33b1485f3201b&scene=19#wechat_redirect) +- [Java基础(七) 深入解析java四种访问权限](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484000&idx=1&sn=0b188c70ac54c65a0419ab0d5da14af4&scene=19#wechat_redirect) +- [Java基础(八) 深入解析常量池与装拆箱机制](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484002&idx=1&sn=a1d9ec01c91537aca444408c989f5a50&scene=19#wechat_redirect) +- [Java基础(九) 可变参数列表介绍](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484003&idx=1&sn=84366ed430c332d4b8e2b2d6b54280f4&scene=19#wechat_redirect) +- [Java基础(十) 深入理解数组类型](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484004&idx=1&sn=9c58b6948f05bbeffea3552fec9ee9a6&scene=19#wechat_redirect) +- [Java基础(十一) 枚举类型](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484005&idx=1&sn=5aaec133dca189fcabc86defcd54c5b8&scene=19#wechat_redirect) +- [类与接口(二)java的四种内部类详解](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484075&idx=1&sn=e0fd37cc5c1eb5fb359ed3dc9c15af66&scene=19#wechat_redirect) +- [类与接口(三)java中的接口与嵌套接口](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484076&idx=1&sn=1903edbc469b2660e51e1154a8b63a27&scene=19#wechat_redirect) +- [类与接口(四)方法重载解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484078&idx=1&sn=db5f231dc64974057d4ee29af1649e8b&scene=19#wechat_redirect) +- [类与接口(五)java多态、方法重写、隐藏](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484083&idx=1&sn=d5b3d1daca2eb4e8d9583d75e6f6ad6c&scene=19#wechat_redirect) + ### 容器(包括juc) @@ -149,6 +182,7 @@ - [深入理解Java虚拟机-你了解GC算法原理吗](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ni-liao-jie-gc-suan-fa-yuan-li-ma.html) - [几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-jian-de-la-ji-hui-shou-qi.html) - [面试官100%会严刑拷打的 CMS 垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-cms-la-ji-hui-shou-qi.html) +- [JVM 面试题 87 题详解](https://sihai.blog.csdn.net/article/details/118737581) ### Java8 @@ -162,6 +196,7 @@ ## 计算机网络 - [http面试问题全解析](docs/network/http面试问题全解析.md) +- [计算机网络常见面试题](https://sihai.blog.csdn.net/article/details/118737663) - 关于tcp、udp网络模型的问题,这篇文章告诉你 - http、https还不了解,别慌! - 面试官问我计算机网络的问题,我一个问题给他讲半个小时 @@ -173,6 +208,8 @@ ## Linux - [java工程师linux命令,这篇文章就够了](https://blog.ouyangsihai.cn/java-gong-cheng-shi-linux-ming-ling-zhe-pian-wen-zhang-jiu-gou-liao.html) +- [linux常见面试题(基础版)](https://sihai.blog.csdn.net/article/details/118737736) +- [linux高频面试题](docs/operating-system/linux高频面试题.md) - 常问的几个Linux面试题,通通解决它 ## 数据结构与算法 @@ -182,10 +219,12 @@ - 跳表这种数据结构,你真的清楚吗,面试官可能会问这些问题! - 红黑树你了解多少,不会肯定会被面试官怼坏 - [B树,B+树,你了解多少,面试官问那些问题?](https://blog.ouyangsihai.cn/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) +- [这篇文章带你彻底理解红黑树](https://sihai.blog.csdn.net/article/details/118738496) - 二叉树、二叉搜索树、二叉平衡树、红黑树、B树、B+树 ### 算法 + - [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) - [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) - [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) diff --git "a/docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" "b/docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..bd0a83d --- /dev/null +++ "b/docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,33 @@ +linux查找命令 + +https://blog.51cto.com/whylinux/2043871 +项目部署常见linux命令 + +https://blog.csdn.net/u010938610/article/details/79625988 +进程文件里有哪些信息 +sed 和 awk 的区别 + +awk用法:https://www.cnblogs.com/isykw/p/6258781.html + +其实sed和awk都是每次读入一行来处理的,区别是:sed 适合简单的文本替换和搜索;而awk除了自动给你分列之外,里面丰富的函数大大增强了awk的功能。数据统计,正则表达式搜索,逻辑处理,前后置脚本等。因此基本上sed能做的,awk可以全部完成并且做的更好。 + +作者:哩掉掉 链接:https://www.zhihu.com/question/297858714/answer/572046422 +linux查看进程并杀死的命令 + +https://blog.csdn.net/qingmu0803/article/details/38271077 +有一个文件被锁住,如何查看锁住它的线程? +如何查看一个文件第100行到150行的内容 + +https://blog.csdn.net/zmx19951103/article/details/78575265 +如何查看进程消耗的资源 + +https://www.cnblogs.com/freeweb/p/5407105.html +如何查看每个进程下的线程? + +https://blog.csdn.net/inuyashaw/article/details/55095545 +linux 如何查找文件 + +linux命令:https://juejin.im/post/5d3857eaf265da1bd04f2437 + +select epoll等问题 +https://juejin.im/post/5b624f4d518825068302aee9#heading-13 \ No newline at end of file From f4991c41141dd471f72cb521f929d47108f265cc Mon Sep 17 00:00:00 2001 From: sihai <1446037005@qq.com> Date: Thu, 15 Jul 2021 16:40:24 +0800 Subject: [PATCH 02/17] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E4=B8=8E=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7e1c3a6..8f796a7 100644 --- a/README.md +++ b/README.md @@ -383,13 +383,13 @@ ## Java学习资源 -- [2020年Java学习资源,你想要的都在这里了](https://mp.weixin.qq.com/s/wLjjy7D57s3UOv4sr8Lxkg) +- [2021 Java 1000G 最新学习资源大汇总](https://mp.weixin.qq.com/s/I0jimqziHqRNaIy0kXRCnw) **截图** -![](http://image.ouyangsihai.cn/Fl0FhkpxLNw0_4-pe8_f8MwAyHzc) -![](http://image.ouyangsihai.cn/Fp3EtjR1FbKPJG2uPdGpMiFjHBNR) -![](http://image.ouyangsihai.cn/FqEKc4i6lsfLCRomFAIksG_rgveY) +![](http://image.ouyangsihai.cn/FkS4oPQKzp9n4x1Lqy-po99DSdwM) +![](http://image.ouyangsihai.cn/FmuFM8wLHGYUt9EKdWSi_TN1utxH) +![](http://image.ouyangsihai.cn/Fs29GQ1AQ8hRJpHC2ctzKr5KfrE6) ## Java书籍推荐 From 3b29fb1d4060bbdd0e61602c533b69daf8fa0e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= Date: Sat, 24 Jul 2021 10:13:41 +0800 Subject: [PATCH 03/17] =?UTF-8?q?=E4=BF=AE=E6=94=B9readme=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...\347\273\234\351\235\242\350\257\225-1.md" | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" diff --git a/README.md b/README.md index 8f796a7..a2eef96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ **[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:https://sihai.blog.csdn.net,春招目前拿到了大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:[https://sihai.blog.csdn.net](https://sihai.blog.csdn.net),春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" new file mode 100644 index 0000000..8ff7d76 --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" @@ -0,0 +1,21 @@ +面试官:OSI与TCP/IP的各层的结构,都有哪些协议呢? +我:哦,我好像知道,我说一下我的理解,这个主要有两种参考模型,一种是基于OSI的参考模型,可以分为7层,分别是:物理层、数据链路层、网络层、运输层、会话层、表示层和应用层;另一种是基于TCP、IP的参考模型,可以分为4层,分别是:网络接口层、网络层、运输层和应用层。 + +关于每一层的协议,我知道的还不少呢,我来说说我知道的吧。 + +应用层:RIP、FTP、DNS、Telnet、SMTP、HTTP、WWW + +我知道的还不少吧,嘚瑟一下,让我一口气说完吧。 + +表示层:JPEG、MPEG、ASCII,MIDI +会话层:RPC、SQL +传输层:TCP、UDP、SPX +网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGMP +数据链路层:PPP、HDLC、VLAN、MAC +物理层:RJ45、IEEE802.3 + +面试官:你能解释一下RJ45吗? + +我:嗯嗯嗯,你自己查一下吧。。。 + + From 24e6272a8ecc460b8db17e8a4ac1910655877e5e Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sat, 24 Jul 2021 12:10:57 +0800 Subject: [PATCH 04/17] =?UTF-8?q?=E6=9B=B4=E6=96=B0Java=E5=AD=A6=E4=B9=A0?= =?UTF-8?q?=E8=B7=AF=E7=BA=BF=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a2eef96..6d273d1 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,40 @@ **[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:[https://sihai.blog.csdn.net](https://sihai.blog.csdn.net),春招目前拿到了大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量251W+**,**CSDN 博客专家**,CSDN博客地址:[https://sihai.blog.csdn.net](https://sihai.blog.csdn.net),春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 -**JavaInterview 最新地址**:https://github.com/OUYANGSIHAI/JavaInterview - **一起冲!!!** -#### Java学习资源汇总(个人总结) +👉 如果你不知道该学习什么的话,请看 [Java 学习线路图是怎样的?](https://zhuanlan.zhihu.com/p/392712685) (原创不易,欢迎点赞),这是 2021 最新最完善的 Java 学习路线! + +👉 Java学习资源汇总(个人总结) -1、Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 +- Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 -![](http://image.ouyangsihai.cn/FodtVWogwlUu5DhV1dZuAEhBznf7) +- 面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi -2、面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi +- 大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 -3、大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 +- 面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo -4、面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo +👉 Java各种电子书:如果你需要各种电子书,可以移步这个仓库 [Java电子书合集](https://github.com/hello-go-maker/cs-books) -#### Java面试思维导图(手打) +👉 Java面试思维导图(手打) -这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,预览在下方,先来瞧瞧。 +👉 这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,预览在下方,先来瞧瞧。 ![](http://image.ouyangsihai.cn/FtJ3PbBRdNSa1NaUr96JmUq24_yS) -**划重点**:想要`导图源文件`,更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 +**划重点**:更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 + +![](http://image.ouyangsihai.cn/FuRA5sT9JUaVbx-YD-Acor04AWhF) -![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) + -
+
@@ -385,12 +387,6 @@ - [2021 Java 1000G 最新学习资源大汇总](https://mp.weixin.qq.com/s/I0jimqziHqRNaIy0kXRCnw) -**截图** - -![](http://image.ouyangsihai.cn/FkS4oPQKzp9n4x1Lqy-po99DSdwM) -![](http://image.ouyangsihai.cn/FmuFM8wLHGYUt9EKdWSi_TN1utxH) -![](http://image.ouyangsihai.cn/Fs29GQ1AQ8hRJpHC2ctzKr5KfrE6) - ## Java书籍推荐 @@ -447,6 +443,8 @@ 如果大家想要实时关注我更新的文章以及分享的干货的话,关注我的公众号 **程序员的技术圈子**。 -![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) +![](http://image.ouyangsihai.cn/FuRA5sT9JUaVbx-YD-Acor04AWhF) + + From acea66cd30555c0563c45a97d138afaff220b10d Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Tue, 28 Dec 2021 08:44:52 +0800 Subject: [PATCH 05/17] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 11 + ...42\350\257\225\351\242\230\344\270\200.md" | 700 ++++++++++++++++++ ...56\345\272\223\344\274\230\345\214\226.md" | 12 + ...37\346\234\272\351\235\242\350\257\225.md" | 588 +++++++++++++++ ...42\350\257\225\351\227\256\351\242\230.md" | 169 +++++ ...\347\273\234\351\235\242\350\257\225-1.md" | 21 - ...235\242\350\257\225-TCP\345\222\214UDP.md" | 172 +++++ ...7\273\234\351\235\242\350\257\225-http.md" | 264 +++++++ 8 files changed, 1916 insertions(+), 21 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 "docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" create mode 100644 "docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" create mode 100644 "docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" delete mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9a17302 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "qiniu.access_key": "5zQ0AuES-7MGr7y89zIyMIK7JsNUlpTLggi5JtGu", + "qiniu.bucket": "sihai", + "qiniu.domain": "image.ouyangsihai.cn", + "qiniu.enable": true, + "qiniu.secret_key": "Mq3GKXAmESb4QGcv8mY8-lNWt42G0AFHpTKgl5Yh", + "pasteImageToQiniu.access_key": "5zQ0AuES-7MGr7y89zIyMIK7JsNUlpTLggi5JtGu", + "pasteImageToQiniu.bucket": "sihai", + "pasteImageToQiniu.domain": "image.ouyangsihai.cn", + "pasteImageToQiniu.secret_key": "Mq3GKXAmESb4QGcv8mY8-lNWt42G0AFHpTKgl5Yh" +} \ No newline at end of file diff --git "a/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" new file mode 100644 index 0000000..9297b4d --- /dev/null +++ "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" @@ -0,0 +1,700 @@ +### 基础问题 + +> truncate和delete有什么区别? + +truncate table:删除内容、不删除定义、释放空间 +delete table:删除内容、不删除定义、不释放空间 +drop table:删除内容、删除定义、释放空间 + +具体来说,有以下区别: +- truncate table 只能删除表中全部数据,delete from 可以删除表中的全部数据也可以部分删除。 +- delete from记录是一条条删除的,删除的每一行记录都会进日志,而truncate一次性删除整个页,日志只会记录页的释放。 +- truncate删除后不能回滚,delete则可以。 +- truncate的删除速度比delete快。 +- delete删除后,删除的数据占用的存储空间还存在,可以恢复数据;truncate则空间不存在,同时不能恢复数据。 + +总的来说,其差别在于,truncate删除是不可恢复的,同时空间也不存在,不支持回滚,而delete删除正好相反。 + +> select Count(*)、select Count(数字)和select Count(column)的区别? + +count(*)和count(1)返回的结果是记录的总行数,包括对NULL的统计;而count(column)是不会记录NULL的统计。 + +> EXISTS关键词的使用方法 + +EXISTS表示是否存在,使用EXISTS时,如果内存查询语句查询到符合条件的值,就返回一个true,否则,将返回false。 + +例如: +```sql +SELECT * FROM user + where EXISTS + (SELECT name FROM employee WHERE id=100) +``` + +如果employee表中存在id为100的员工,内层查询就会返回一个true,外层查询接收到true后,开始查询user表中的数据,因为where没有设置其他查询条件,所以,将查询出user的全部数据。 + +> 内连接(inner join)和外连接的区别? + +- 内连接:只会查询出两表连接符合条件的记录。 + +```sql +SELECT * FROM user1 u1 INNER JOIN user2 u2 ON u1.id = u2.id; +``` + +如上sql所示,只会查询到user1和user2关联符合条件的记录。 + +外连接分为左外连接、右外连接和全外连接。 + +- 左外连接 + +它的原理是:以左表为基准,去右表匹配数据,找不到匹配的用NULL补齐。其显示左表的全部记录和右表符合连接条件的记录。 + +- 右外连接 + +它的原理是:以右表为基准,去左表匹配数据,找不到匹配的用NULL补齐。其显示右表的全部记录和左表符合连接条件的记录。这正好与左外连接相反。 + +- 全外连接 + +除了显示符合连接条件的记录之外,两个表的其他数据也会显示出来。 + +> inner join 和 left join性能谁优谁劣? + +![](http://image.ouyangsihai.cn/Ft-S3YZNbH0Il2x0K6wbxSUcZ2yE) + +如上图,是mysql的执行顺序,我们可以看出,外查询是在内查询的基础上,进而进行查询操作的。因此,我们从理论上可以得出,内连接的执行效率是更好一些的。 + +但是,外连接也是会进行优化操作的,在编译优化阶段,如果左连接的结果和内连接一样,左连接查询会转换成内连接查询,但这也表明编译优化器也认为内连接的效率是更高的。 + +虽然从查询的结果来看一般不会有太大的区别,但是,如果左右表之间的数据差别很大,内连接的效率是明显更高的,因为左连接以左表为基准,并且会进行回表操作。 + +最后,给出一个结论:在外连接和内连接都可以实现需求时,建议使用内连接进行操作。 + +> 存储过程是什么,优势是什么,为什么不建议使用? + +存储过程:为了完成特定功能的SQL语句集,存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。在数据量特别庞大的情况下利用存储过程能达到倍速的效率提升。 + +**优势** +- 存储过程是预编译的,因此执行速度较快; +- 存储过程在服务端执行,减少客户端的压力; +- 减少网络流量,客户端只需要传递存储过程名称和参数即可进行调用,减少了传输的数据量; +- 一次编写,任意次执行,复用的思想简单便捷; +- 安全性较高,因为在服务端执行,管理员可以对存储过程进行权限限制,能够避免非法的访问,保证数据的安全性。 + +**缺点** + +- 调试麻烦,可移植性查。 + +这一个缺点也是存储过程在实际开发中用的不多的原因,现在开发的理念是简便。 + +> 数据库的三级范式 + +1NF:原子性,字段不可再分; +2NF:在满足第一范式的基础上,一个表只能说明一个事物,非主键属性必须玩去哪依赖于主键属性; +3NF:在满足第二范式的基础上,每列都与主键有直接关系,不存在传递依赖,任何非主键属性不依赖于其他非主键属性。 + +> sql语句应该考虑哪些安全性 + +- 防止sql注入,对sql语句尽量进行过滤同时使用预编译的sql语句绑定变量 +- 查询错误信息不要返回给用户,将错误记录到日志 +- 最小用户权限设置,最好不要使用root用户连接数据库 +- 定期做数据备份,避免数据丢失 + +> 什么叫sql注入,如何防止? + +SQL注入是一种将SQL代码添加到输入参数中,传递到服务器解析并执行的一种攻击手法。 + +**SQL注入攻击**是输入参数未经过滤,然后直接拼接到SQL语句当中解析,执行达到预想之外的一种行为,称之为SQL注入攻击。 + +常见的sql注入,有下列几种: +- 数字注入 + +例如,查询的sql语句为:`SELECT * FROM user WHERE id = 1`,正常是没有问题的,如果我们进行sql注入,写成`SELECT * FROM user WHERE id = 1 or 1=1`,那么,这个语句永远都是成立的,这就有了问题,也就是sql注入。 + +- 字符串注入 + +字符串注入是因为注释的原因,导致sql错误的被执行,例如字符`#`、`--`。 + +例如,`SELECT * FROM user WHERE username = 'sihai'#'ADN password = '123456'`,这个sql语句'#'后面都被注释掉了,相当于`SELECT * FROM user WHERE username = 'sihai' `。 + +这种情况我们在mybatis中也是会存在的,所以在服务端写sql时,需要特别注意此类情况。 + +该如何防范此类问题呢? +- 严格检查输入变量的类型和格式,也就是对相关传入的参数进行验证,尽可能降低风险。 +- 过滤和转义特殊字符。 +- 利用mysql的预编译机制,在Java中mybatis也是有预编译的方法的,所以可以采用这种方式避免。 + +> MySQL中InnoDB和MyISAM的区别? + +- 事务处理方面:MyISAM强调的是性能,查询的速度比InnoDB更快,但是,不支持事务,InnoDB是支持事务的。 +- 外键:InnoDB支持外键,MyISAM不支持。 +- 锁:InnoDB支持行锁和表锁,默认会使用行锁,而MyISAM只是支持表锁。由于行锁是能更好的支持并发操作,因此,InnoDB更加适合插入和更新操作较多的情况,而MyISAM适用于频繁查询操作。 +- 全文索引:MyISAM支持全文索引,InnoDB不支持。但是,在5.6版本开始InnoDB也开始支持全文索引。 +- 表主键:MyISAM允许没有主键的表存在,而InnoDB如果不存在主键,会自动生成一个6字节的主键。 +- 查询表的行数差异:InnoDB不保存表的函数信息,因此,select count(*)时会扫描整个表来进行计算;MyISAM内置了计数器,只需要简单的读取保存好的行数即可。 + + +### 事务相关 + +> 数据库事务的四个基本要素? + +事务是指一组SQL语句组成的逻辑处理单元。 + +ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) + + +- 原子性(Atomicity) + +原子性指的是整个数据库的事务是一个不可分割的工作单位,每一个都应该是一个原子操作。 + +当我们执行一个事务的时候,如果一系列的操作中,有一个操作失败了,那么,需要将这一个事务中的所有操作恢复到执行事务之前的状态,这就是事务的原子性。 + +下面举个简单的例子。 +```java +i++; +``` +上面这个最简单不过的代码经常也会被问到,这是一个原子操作吗?那肯定不是,如果我们把这个代码放到一个事务中来说,当i+1出现问题的时候,回滚的就是整个代码i++(i = i + 1)了,所以回滚之后,i的值也是不会改变的。 + +以上就是原子性的概念。 + +- 一致性(consistency) + +一致性是指事务将数据库从一种状态转变为下一种一致性的状态,也就是说在事务执行前后,这两种状态应该是一样的,也就是数据库的完整性约束不会被破坏。 + +另外,需要注意的是一致性是不关注中间状态的,比如银行转账的过程,你转账给别人,至于中间的状态,你少了500 ,他多了500,这些中间状态不关注,如果分多次转账中间状态也是不可见的,只有最后的成功或者失败的状态是可见的。 + +如果到分布式的一致性问题,又可以分为强一致性、弱一致性和最终一致性,关于这些概念,可以自己查查,还是很有意思的。 + +- 隔离性(isolation) + +事务我们是可以开启很多的,MySQL数据库中可以同时启动很多的事务,但是,事务和事务之间他们是相互分离的,也就是互不影响的,这就是事务的隔离性。 + +- 持久性(durability) + +事务的持久性是指事务一旦提交,就是永久的了,就是发生问题,数据库也是可以恢复的。因此,持久性保证事务的高可靠性。 + +> 并发事务会带来什么问题 + +#### 脏读 + +**脏读:** 在不同的事务下,当前事务可以读到另外事务未提交的数据。另外我们需要注意的是默认的MySQL隔离级别是`REPEATABLE READ`是不会发生脏读的,脏读发生的条件是需要事务的隔离级别为`READ UNCOMMITTED`,所以如果出现脏读,可能就是这种隔离级别导致的。 + +下面我们通过一个例子看一下。 +![](http://image.ouyangsihai.cn/Fh7WDJf8NpXn0QY0sA-vpVyGtGhs) + +从上面这个例子可以看出,当我们的事务的隔离级别为`READ UNCOMMITTED`的时候,在会话A还没有提交时,会话B就能够查询到会话A没有提交的数据。 + + +#### 不可重复读 + +**不可重复读:** 是指在一个事务内多次读取同一集合的数据,但是多次读到的数据是不一样的,这就违反了数据库事务的一致性的原则。但是,这跟脏读还是有区别的,脏读的数据是没有提交的,但是不可重复读的数据是已经提交的数据。 + +我们通过下面的例子来看一下这种问题的发生。 + +![](http://image.ouyangsihai.cn/FuqDKDktSrQTMPP8fAm0cmWdNsR8) + +从上面的例子可以看出,在A的一次会话中,由于会话B插入了数据,导致两次查询的结果不一致,所以就出现了不可重复读的问题。 + +我们需要注意的是不可重复读读取的数据是已经提交的数据,事务的隔离级别为`READ COMMITTED`,这种问题我们是可以接受的。 + +如果我们需要避免不可重复读的问题的发生,那么我们可以使用**Next-Key Lock算法**(设置事务的隔离级别为`READ REPEATABLE`)来避免,在MySQL中,不可重复读问题就是Phantom Problem,也就是**幻像问题**。 + +#### 丢失更新 + +**丢失更新:** 指的是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。在当前数据库的任何隔离级别下都不会导致丢失更新问题,要出现这个问题,在多用户计算机系统环境下有可能出现这种问题。 + +如何避免丢失更新的问题呢,我们只需要让事务的操作变成串行化,不要并行执行就可以。 + +我们一般使用`SELECT ... FOR UPDATE`语句,给操作加上一个排他X锁。 + +> 数据库事务的隔离级别 + +数据库提供了四种隔离级别。 + +- 读未提交数据(READ UNCOMMITTED) + +允许事务读取未被其他事务提交的变更,可能有脏读、不可重复读和幻读的问题。 + +例如,某个时刻会话a修改了一个数据,但是未提交,此时,会话b读取了该数据,此时,a回滚了事务,这就会出现a、b数据不一致,这就是**脏读**。 + +- 读已提交数据(READ COMMITTED) + +允许事务读取已经被其他事务提交的变更,可能不可重复读和幻读的问题。 + +例如,某个时刻会话a修改了一个数据,提交了,此时的结果是10,此时b对该数据进行了修改为20,并提交了,此时会话a再次读取该数据,发现结果是20了,因此,在同一事务中,出现了两次读取的结果不一致的现象,这就是**不可重复读**。 + +- 可重复读(REPEATABLE READ,默认隔离级别) + +可重复读,从字面上的意思也能明白,就是在同一事务中读取多次,确保每次读取到的数据都是一样的,可以避免脏读和不可重复读,但是可能会出现幻读。 + +- 可串行化(SERIALIZABLE) + +可串行化是指所有的事务都是一个接一个执行,可以避免所有的问题,但是,效果太低。 + +最后,再用一张图来总结一下。 + +![](http://image.ouyangsihai.cn/Fh1arOgS78BnrjBXKwGw8NSP27uM) + + +### 锁相关 + +> 数据库中锁机制,说说数据库中锁的类型 + +对于MySQL来说,锁是一个很重要的特性,数据库的锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性,这样才能保证在高并发的情况下,访问数据库的时候,数据不会出现问题。 + +在数据库中,lock和latch都可以称为锁,但是意义却不同。 + +**Latch**一般称为`闩锁`(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差,在InnoDB引擎中,Latch又可以分为`mutex`(互斥量)和`rwlock`(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。 + +**Lock**的对象是`事务`,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。 + +InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 + +**S or X (共享锁、排他锁)** + +数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的**行级锁**,也就是**共享锁(Shared Lock)和互斥锁(Exclusive Lock)**。 +- 共享锁(读锁)(S Lock),允许事务读一行数据。 +- 排他锁(写锁)(X Lock),允许事务删除或更新一行数据。 + +**IS or IX (共享、排他)意向锁** + +为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB存储引擎支持一种额外的锁方式,就称为**意向锁**,意向锁在 InnoDB 中是**表级锁**,意向锁分为: + + +- 意向共享锁:表达一个事务想要获取一张表中某几行的共享锁。 +- 意向排他锁:表达一个事务想要获取一张表中某几行的排他锁。 + + +另外,这些锁之间的并不是一定可以共存的,有些锁之间是不兼容的,所谓**兼容性**就是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。 + +下面我们再看一下这两种锁的兼容性。 + +- S or X (共享锁、排他锁)的兼容性 + +![](http://image.ouyangsihai.cn/Fvr5OSfX9nP2Ip30O088kVx-Pdza) + + +- IS or IX (共享、排他)意向锁的兼容性 + +![](http://image.ouyangsihai.cn/Fgf-Pg6JPVJ7CmPyrdcow_5q-VZm) + +> MySQL中锁的粒度 + +在数据库中,锁的粒度的不同可以分为表锁、页锁、行锁,这些锁的粒度之间也是会发生升级的,**锁升级**的意思就是讲当前锁的粒度降低,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁,下面分别介绍一下这三种锁的粒度(参考自博客:https://blog.csdn.net/baolingye/article/details/102506072)。 + +##### 表锁 + +表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。 + +当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。 + +使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。 + +**特点:** 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 + +##### 页锁 + +页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。 +在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源 颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。 +使用页级锁定的主要是BerkeleyDB存储引擎。 + +**特点:** 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 + +##### 行锁 + +行级锁定最大的特点就是锁定对象的粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。 + +虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。 + +**特点:** 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 + +比较表锁我们可以发现,这两种锁的特点基本都是相反的,而从锁的角度来说,**表级锁**更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而**行级锁**则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。 + +##### MySQL 不同引擎支持的锁的粒度 + +![](http://image.ouyangsihai.cn/FvpMs-9FmTS7IKUBEv3WCm0IFJJH) + +> 了解一致性非锁定读和一致性锁定读吗? + +#### 一致性锁定读(Locking Reads) + +在一个事务中查询数据时,普通的SELECT语句不会对查询的数据进行加锁,其他事务仍可以对查询的数据执行更新和删除操作。因此,InnoDB提供了两种类型的锁定读来保证额外的安全性: + + - `SELECT ... LOCK IN SHARE MODE` + - `SELECT ... FOR UPDATE` + + `SELECT ... LOCK IN SHARE MODE`: 对读取的行添加S锁,其他事物可以对这些行添加S锁,若添加X锁,则会被阻塞。 + + `SELECT ... FOR UPDATE`: 会对查询的行及相关联的索引记录加X锁,其他事务请求的S锁或X锁都会被阻塞。 当事务提交或回滚后,通过这两个语句添加的锁都会被释放。 注意:只有在自动提交被禁用时,SELECT FOR UPDATE才可以锁定行,若开启自动提交,则匹配的行不会被锁定。 + + #### 一致性非锁定读 + + **一致性非锁定读(consistent nonlocking read)** 是指InnoDB存储引擎通过多版本控制(MVVC)读取当前数据库中行数据的方式。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB会去读取行的一个快照。所以,非锁定读机制大大提高了数据库的并发性。 + + ![来自网络:侵权删](http://image.ouyangsihai.cn/FhF001C8JBPwaEXPgJ9TEzFT8C6X) + +一致性非锁定读是InnoDB默认的读取方式,即读取不会占用和等待行上的锁。在事务隔离级别`READ COMMITTED`和`REPEATABLE READ`下,InnoDB使用一致性非锁定读。 + +然而,对于快照数据的定义却不同。在`READ COMMITTED`事务隔离级别下,一致性非锁定读总是**读取被锁定行的最新一份快照数据**。而在`REPEATABLE READ`事务隔离级别下,则**读取事务开始时的行数据版本**。 + +下面我们通过一个简单的例子来说明一下这两种方式的区别。 + +首先创建一张表; + +![](http://image.ouyangsihai.cn/FqzGMzTgnaAxaSX2Ko9YVUjnmFWt) + +插入一条数据; + +``` +insert into lock_test values(1); +``` + +查看隔离级别; + +``` +select @@tx_isolation; +``` + +![](http://image.ouyangsihai.cn/Fn3A5-fYhDs8rb0VvNMC1OL6B9sW) + +下面分为两种事务进行操作。 + +在`REPEATABLE READ`事务隔离级别下; + +![](http://image.ouyangsihai.cn/FhE1WGAeSkZGAq90Cx1alh3dZTVa) + +在`REPEATABLE READ`事务隔离级别下,读取事务开始时的行数据,所以当会话B修改了数据之后,通过以前的查询,还是可以查询到数据的。 + +在`READ COMMITTED`事务隔离级别下; + +![](http://image.ouyangsihai.cn/FrdXfAw47rFAzRks4-4Y9IoWKtl4) + +在`READ COMMITTED`事务隔离级别下,读取该行版本最新的一个快照数据,所以,由于B会话修改了数据,并且提交了事务,所以,A读取不到数据了。 + +> InnoDB存储引擎行锁的算法了解吗? + +InnoDB存储引擎有3种行锁的算法,其分别是: + +- Record Lock:单个行记录上的锁。 +- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。 +- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。 + +**Record Lock**:总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。 + +**Next-Key Lock**:结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。举个例子10,20,30,那么该索引可能被Next-Key Locking的区间为: +![](http://image.ouyangsihai.cn/FrOLmJtKBxs70A0QHAc35CccZg2Y) + +除了Next-Key Locking,还有**Previous-Key Locking**技术,这种技术跟Next-Key Lock正好相反,锁定的区间是区间范围和前一个值。同样上述的值,使用Previous-Key Locking技术,那么可锁定的区间为: +![](http://image.ouyangsihai.cn/Fr-S9vxXpA--rHGiPkWMpCdhEKxT) + +不是所有索引都会加上Next-key Lock的,这里有一种**特殊的情况**,在查询的列是唯一索引(包含主键索引)的情况下,`Next-key Lock`会降级为`Record Lock`。 + +接下来,我们来通过一个例子解释一下。 +```java +CREATE TABLE test ( + x INT, + y INT, + PRIMARY KEY(x), // x是主键索引 + KEY(y) // y是普通索引 +); +INSERT INTO test select 3, 2; +INSERT INTO test select 5, 3; +INSERT INTO test select 7, 6; +INSERT INTO test select 10, 8; +``` +我们现在会话A中执行如下语句; +```java +SELECT * FROM test WHERE y = 3 FOR UPDATE +``` + +我们分析一下这时候的加锁情况。 + +- 对于主键x + +![](http://image.ouyangsihai.cn/Fo3ptcBFShRMwLAC1guVV4mUO-93) + + +- 辅助索引y + +![](http://image.ouyangsihai.cn/Fj8hUM6slurRWb5SImLWWDM2YDu0) + + +用户可以通过以下两种方式来显示的关闭Gap Lock: + +- 将事务的隔离级别设为 READ COMMITED。 +- 将参数innodb_locks_unsafe_for_binlog设置为1。 + +**Gap Lock的作用**:是为了阻止多个事务将记录插入到同一个范围内,设计它的目的是用来解决**Phontom Problem(幻读问题)**。在MySQL默认的隔离级别(Repeatable Read)下,InnoDB就是使用它来解决幻读问题。 + +>**幻读**:是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL可能会返回之前不存在的行,也就是第一次执行和第二次执行期间有其他事务往里插入了新的行。 + +> 说说悲观锁和乐观锁 + +##### 悲观锁 + +悲观锁是指在数据处理过程中,从一开始就使数据处于锁定状态,知道更改完成才释放。 + +MySQL中悲观锁使用以下方式:`select...for update` + +例如: +```sql +select name from item where id = 200 for update; +insert into orders(id,item_id) values(null,100) +update item set count = count - 1 where id = 100; +``` +我们使用`select name from item where id = 200 for update;`对id为200的数据进行了锁定,其他要对该条数据进行修改,必须等到该事务提交之后,否则无法修改。这样就保证了并发的安全性。 + +需要注意的是:`select...for update`语句必须在事务中使用。 + +悲观锁虽然能够解决并发安全的问题,但是,这种锁定会导致性能降低,加锁时间过长,并发性不好,影响系统的整体性能。因此,这种方式在实际的开发中用的很少。 + +##### 乐观锁 + +乐观锁是相对悲观锁而言的,认为数据一般情况下不会出现冲突,所以在数据进行更新的时候,才会将数据锁定。 + +**乐观锁的实现方式** + +- 使用数据版本(version)机制实现 + +该方式是在每次更新数据的时候,都要对更新的数据进行version版本+1操作。 + +其具体原理是:读取数据时,将此版本一同读出,之后,更新数据时,对此版本+1,每次提交数据时,如果提交的数据版本大于或等于数据库表中的版本,则可以更新,说明是最新数据,否则,不予更新,说明数据已经过期。 + +- 使用时间戳实现 + +该机制和version是类似的,也是需要再表中增加一个字段,类型使用时间类型即可。 + +原理:在更新数据时,检查数据库中当前的时间戳和更新前取到的时间戳,如果对比一致,就予以更新,否则不更新。 + +##### 使用场景分析 + +悲观锁可以在并发量不大的情况下使用,并发量大的情况下,使用乐观锁,大多数情况下都建议使用乐观锁。 + +### 索引相关 + +> 索引有哪些类型? + +索引有很多中类型:普通索引、唯一索引、主键索引、组合索引、全文索引,下面我们看看如何创建和删除下面这些类型的索引。 + +- 唯一索引:是在表上一个或者多个字段组合建立的索引,这些字段组合的值在表中不可重复。 +- 非唯一索引:是在表上一个或者多个字段组合建立的索引,这些字段组合的值在表中可重复。 +- 主键索引:是唯一索引的特定类型。表中创建主键时,会自动创建主键索引且只有一个。 +- 组合索引:基于多个字段而创建的索引。 + +下面再看看索引的创建和删除的方法。 + +#### 索引的创建方式 + +索引的创建是可以在很多种情况下进行的。 + +- 直接创建索引 + +``` +CREATE [UNIQUE|FULLLTEXT] INDEX index_name ON table_name(column_name(length)) +``` +`[UNIQUE|FULLLTEXT]`:表示可选择的索引类型,唯一索引还是全文索引,不加话就是普通索引。 +`table_name`:表的名称,表示为哪个表添加索引。 +`column_name(length)`:column_name是表的列名,length表示为这一列的前length行记录添加索引。 + +- 修改表结构的方式添加索引 + +``` +ALTER TABLE table_name ADD [UNIQUE|FULLLTEXT] INDEX index_name (column(length)) +``` + +- 创建表的时候同时创建索引 + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`), + [UNIQUE|FULLLTEXT] INDEX index_name (title(length)) +) +``` + +#### 主键索引和组合索引创建的方式 + +前面讲的都是**普通索引、唯一索引和全文索引**创建的方式,但是,**主键索引和组合索引**创建的方式却是有点不一样的,所以单独拿出来讲一下。 + +**组合索引创建方式** + +- 创建表的时候同时创建索引 + + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`), + INDEX index_name(id,title) +) +``` + +- 修改表结构的方式添加索引 + +``` +ALTER TABLE table_name ADD INDEX name_city_age (name,city,age); +``` + +**主键索引创建方式** +主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。 + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`) +) +``` + +#### 删除索引 + +删除索引可利用`ALTER TABLE`或`DROP INDEX`语句来删除索引。类似于`CREATE INDEX`语句,`DROP INDEX`可以在`ALTER TABLE`内部作为一条语句处理,语法如下。 + +(1)`DROP INDEX index_name ON talbe_name` +(2)`ALTER TABLE table_name DROP INDEX index_name` +(3)`ALTER TABLE table_name DROP PRIMARY KEY` + +第3条语句只在删除`PRIMARY KEY`索引时使用,因为一个表只可能有一个`PRIMARY KEY`索引,因此不需要指定索引名。 + +> 数据库索引的实现原理 + +在讲解本题之前,建议大家先了解一下B+树的原理,这对后面的讲解的理解有很大的帮助,大家可以阅读一些这篇文章:[面试官问你B树和B+树,就把这篇文章丢给他](https://www.java1000.com/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) + +基于MySQL数据库的引擎不同,索引的实现原理也是不相同的,这里主要以最主流的InnoDB和MyISAM搜索引擎举例,来说明索引的实现原理。 + +##### MyISAM索引实现原理 + +首先,我们需要明白一点,MyISAM 引擎的整理结构是采用主键索引和辅助索引构成的。MyISAM 引擎使用 B+ 树作为索引结构,叶节点的 data 域存放的是数据记录的地址。如下图所示。 + +![](http://image.ouyangsihai.cn/FhIggfScI0BhtoxY4xOCh6wLTJD0) + +由上图可知,MyISAM 引擎的叶子节点存放的是**数据记录的地址**。 + +接下来,再来看一下辅助索引。 + +![](http://image.ouyangsihai.cn/Fk9XGH0vXWUgUOqniAqBpkm8ETAf) + +在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是**主索引要求 key 是唯一的,而辅助索引的 key 可以重复。** + +辅助索引也是一颗B+树,data域保存数据记录的地址。 + +**基于MyISAM引擎的索引检索算法:** 按照B+树算法搜索索引,假若指定搜索的key存在,则可以直接取出值,然后以data域的值为地址,使用地址获取对应的数据记录。 + +##### InnoDB索引实现原理 + +基于MyISAM引擎实现的索引原理与基于InnoDB实现的索引原理总是分不开的,可以说是一对欢喜冤家,而两者之间最大的区别就在于,**InnoDB的数据文件本身就是索引文件**,怎么理解这句话呢?其实是相对于MyISAM引擎实现的索引而言的。从上分析我们可以知道,**MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址**,而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录,而保存这个索引的key就是数据表的主键,因此InnoDB引擎表数据文件本身就是**主索引**。 + +总结一下,意思就是说InnoDB实现的索引叶子节点保存的是数据记录,而MyISAM引擎实现的索引的叶子节点保存的是数据记录的地址,还需要通过地址去索引对应的数据。 + +![](http://image.ouyangsihai.cn/Fp7rcVb2jKuNnpqcZY58zQYmWman) + +另外,由于基于InnoDB实现的索引的数据文件本身要按主键聚集,因此,基于InnoDB实现的索引是必须有主键存在的。 + +**其主键策略**:如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键;如果没有找到上面符合条件的列,则会生成6个字节的bigint unsigned值作为其主键。 + +**考点:尽量在InnoDB引擎上采用自增字段做表的主键** + +InnoDB引擎数据文件本身是一棵B+树,非自增的主键会造成在插入新记录时数据文件为了维持B+树的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。如果不了解以上B+树的原理,建议阅读上面的B+树的文章。如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 + +因此,采用在我们平时的开发当中,我们通常会采用自增主键,因为,MySQL的常用的版本中,默认的搜索引擎就是InnoDB,所以,采用自增主键,其实是可以保证比较好的效率的。 + + +**辅助索引** + +在上述MyISAM引擎索引的讲解中提到了辅助索引,MyISAM引擎中的主索引和辅助索引都是指向了同一数据记录的,而在InnoDB引擎中的表现却不一样。 + +**InnoDB的辅助索引data域存储相应记录主键索引的值而不是地址**。搜索辅助索引需要先根据辅助索引获取到主键值,再根据主键到主索引中获取到对应的数据记录。 +![](http://image.ouyangsihai.cn/FjeFUL7Iq4iIXtVOVZqBe4hJmE7G) + + +> 谈谈聚簇索引和非聚簇索引 + +其实,在上面对索引实现原理的分析当中,已经对这两个概念有了很好的讲解了,只是没有明显的指出而已。 + +InnoDB引擎采用的是**聚簇索引**,而MyISAM引擎采用的是**非聚簇索引**。这两个概念的区别就在于**叶节点是否存放一整行记录**,我们都知道,InnoDB引擎叶子节点存放的是数据记录,而MyISAM引擎的叶子节点存放的是数据记录的地址,所以说,只要理解了基于InnoDB引擎和基于MyISAM引擎实现的索引原理,就理解了以上这两个概念。这么说是不是就很容易理解的,不就是对应了两种不同的引擎吗,是不是很简单。 + +如果还不是很理解,再放一张图。 + +![](http://image.ouyangsihai.cn/Ft6BKzPJQ-Le30BjRM44ICGTjAtU) + +左边的是聚集索引(聚簇索引),右边的是非聚集索引(非聚簇索引),这两个是不是就是**基于InnoDB引擎和基于MyISAM引擎实现的索引原理**。 + +**聚簇索引的优势** + +- 当你需要取出一定范围内的数据时,用聚簇索引也比用非聚簇索引好。 +- 当通过聚簇索引查找目标数据时理论上比非聚簇索引要快,原因在于非聚簇索引叶子节点存放的是数据记录的地址,索引定位到对应主键时还要多一次目标记录寻址,即多一次I/O。 +- 采用覆盖索引扫描的查询可以直接使用页节点中的主键值。 + +**聚簇索引的不足** + +- 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。 +- 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。 +- 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。二级索引的叶节点存储的是主键值,而不是行指针(非聚簇索引存储的是指针或者说是地址),这是为了减少当出现行移动或数据页分裂时二级索引的维护工作,但会让二级索引占用更多的空间。 +- 采用聚簇索引插入新值比采用非聚簇索引插入新值的速度要慢很多,因为插入要保证主键不能重复,判断主键不能重复,采用的方式在不同的索引下面会有很大的性能差距,聚簇索引遍历所有的叶子节点,非聚簇索引也判断所有的叶子节点,但是聚簇索引的叶子节点除了带有主键还有记录值,记录的大小往往比主键要大的多。这样就会导致聚簇索引在判定新记录携带的主键是否重复时进行昂贵的I/O代价。 + +最后,说明一下,如果你很好的理解了索引的原理,这上面的就会很好理解,如果理解不到位,就会发现这都是什么东西?因此,看这个的时候,先看一下上面那一题的解答。 + +> 覆盖索引 + +覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。 + +举个例子 + +```sql +select * from user where name = "sihai"; +``` +以上语句查询是会从数据表中进行查询的,因为,没有对user表中的name字段增加索引的操作。 + +```sql +alter table user add index name_index(name); +``` +我们对user表中的name字段添加了索引。我们再使用sql语句`select * from user where name = "sihai";`进行查询是,就会使用覆盖索引。 + +因此,我们就可以非常清楚的明白,覆盖索引就是如果发现可以走索引的方式得到数据,就不采用回表查询的操作了,从而提高了查询的效率。 + +另外,从上面的索引原理的介绍也可以得到另外一个结论:使用覆盖索引InnoDB引擎比MyISAM引擎效果更佳,原因在于InnoDB采用聚集索引,如果二级索引中包含查询所需的数据,就不再需要在聚集索引中查找了。 + +最后一点,想要使用覆盖索引,就必须要使得查询能够用到索引,因此,也需要注意索引失效的场景。 + +> 建立索引的原则 + +- 最左前缀匹配原则,MySQL会遇到范围查询停止匹配,所以会导致组合索引失效。 +- 索引列不进行函数运算。 +- 注意一个表建立索引的数量,不是索引建的越多越好,维护索引也会有很大的开销。 +- 尽量选择区分度高的字段作为索引。 +- where语句中,经常使用的字段应该考虑建立索引。 +- 分组和排序语句中,经常使用的字段应该考虑建立索引。 +- 两个表关联字段考虑建立索引。 +- like模糊查询中,只有右模糊查询才会使用索引。 +- 在varchar 字段上建立索引时,必须指定索引长度。 +- 禁止建立超过3个字段的联合索引。 +- 尽量采用覆盖索引,避免回表查询。 +- 索引优化的目标:至少要达到range级别,要求是ref级别,如果可以是consts最好。 + +> 索引失效的情况 + +- 使用组合索引,没有满足最左匹配原则,导致失效。 +- or语句所有字段必须都有索引,否则失效。 +- like以%开头,索引失效。 +- 需要类型转换。 +- where中索引列有运算。 +- where中索引列使用了函数。 +- 如果mysql觉得全表扫描更快时(数据少)。 + +### 其他问题 + +> 一张有自增id的表,当数据记录到了20之后,删除了第18,19,20条记录,再把MySQL重启,再插入一条记录,这条记录的id是21还是18呢? + +当表的引擎采用MyISAM时,是21,当表的引擎采用InnoDB时是18。 + +MyISAM引擎会把表自增主键的最大id记录到数据文件中,做了持久化,重启后也不会消失,而InnoDB是将自增主键的最大id记录在内存中,重启后,会丢失。 + +> 关系型数据库和非关系型数据库的区别 + +非关系型数据库的优势在于性能和可扩展性,非关系型数据库一般都是基于键值对的,当然也支持文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,同时,也是基于内存进行相关的操作,同时,底层也有较好的数据结构的支持,保证了其操作的效率,性能较高;另外,同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。成本低:非关系型数据库部署简单,基本都是开源软件。 + + +关系型数据库的优势在于支持更加复杂的sql操作、事务支持和使用表结构更加易于维护。 + + + diff --git "a/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" "b/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" new file mode 100644 index 0000000..32cea00 --- /dev/null +++ "b/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" @@ -0,0 +1,12 @@ +## 数据库优化 + +### 为什么要做优化 + + +### 从哪几个方面考虑优化 + + +### MySQL语句优化 + + +### 其他 \ No newline at end of file diff --git "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" new file mode 100644 index 0000000..ce68d03 --- /dev/null +++ "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" @@ -0,0 +1,588 @@ +> 简单的说一下Java的垃圾回收机制,解决了什么问题? + +这个题目其实我是不太想写的,原因在于太笼统了,容易让面试者陷入误区,难以正确的回答,但是,如何加上一个解决了什么问题,那么,这个面试题还是有意义的,可以看看面试者的理解。 + +在c语言和c++中,对于内存的管理是非常的让人头疼的,也是很多人放弃的原因,因此,在后面的一些高级语言中,设计者就想解决这一个问题,让开发者专注于自己的业务开发即可,因此,在Java中也引入了垃圾回收机制,它使得开发者在编写程序的时候不再需要考虑内存管理,由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。 + +> 了解JVM的内存模型吗? + +JVM载执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 + +Java 虚拟机所管理的内存一共分为Method Area(方法区)、VM Stack(虚拟机栈)、Native Method Stack(本地方法栈)、Heap(堆)、Program Counter Register(程序计数器)五个区域。 + +这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。具体如下图所示: + +![](http://image.ouyangsihai.cn/FhxTjHOieWt_ugomW5L33YNkJlJQ) + +上图介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:**元数据区取代了永久代**。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:**元数据空间并不在虚拟机中,而是使用本地内存**。 + + +### 1 程序计数器(Program Counter Register) + +**程序计数器(Program Counter Register)**是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器**。在虚拟机概念模型中,**字节码解释器**工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 + +程序计数器是一块 **“线程私有”** 的内存,每条线程都有一个独立的程序计数器,能够将切换后的线程恢复到正确的执行位置。 + +- 执行的是一个**Java方法** + +计数器记录的是正在执行的**虚拟机字节码指令的地址**。 + +- 执行的是**Native方法** + +**计数器为空(Undefined)**,因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。 + +- 程序计数器也是唯一一个在Java虚拟机规范中没有规定任何**OutOfMemoryError**情况的内存区域。 + +其实,我感觉这块区域,作为我们开发人员来说是不能过多的干预的,我们只需要了解有这个区域的存在就可以,并且也没有虚拟机相应的参数可以进行设置及控制。 + +### 2 Java虚拟机栈(Java Virtual Machine Stacks) + +![](http://image.ouyangsihai.cn/FksaMoFlAkSPkTB84bV4cK7xa8L3) + + +**Java虚拟机栈(Java Virtual Machine Stacks)**描述的是**Java方法执行的内存模型**:每个方法在执行的同时都会创建一个**栈帧(Stack Frame)**,从上图中可以看出,栈帧中存储着**局部变量表**、**操作数栈**、**动态链接**、**方法出口**等信息。每一个方法从调用直至执行完成的过程,会对应一个栈帧在虚拟机栈中入栈到出栈的过程。 + +与程序计数器一样,Java虚拟机栈也是**线程私有**的。 + +而**局部变量表**中存放了编译期可知的各种: + +* **基本数据类型**(boolen、byte、char、short、int、 float、 long、double) +* **对象引用**(reference类型,它不等于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) +* **returnAddress类型**(指向了一条字节码指令的地址) + +其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。**局部变量表所需的内存空间在编译期间完成分配**,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 + +Java虚拟机规范中对这个区域规定了两种异常状况: + +* **StackOverflowError**:线程请求的栈深度大于虚拟机所允许的深度,将会抛出此异常。 +* **OutOfMemoryError**:当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存,就会抛出该异常。 + +一直觉得上面的概念性的知识还是比较抽象的,下面我们通过JVM参数的方式来控制栈的内存容量,模拟StackOverflowError异常现象。 + + +### 3 本地方法栈(Native Method Stack) + +**本地方法栈(Native Method Stack)** 与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 + +在虚拟机规范中对本地方法栈中使用的语言、方式和数据结构并无强制规定,因此具体的虚拟机可实现它。甚至**有的虚拟机(Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一**。与虚拟机一样,本地方法栈会抛出**StackOverflowError**和**OutOfMemoryError**异常。 + +- 使用-Xss参数减少栈内存容量(更多的JVM参数可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html)) + +这个例子中,我们将栈内存的容量设置为`256K`(默认1M),并且再定义一个变量查看栈递归的深度。 + +```java +/** + * @ClassName Test_02 + * @Description 设置Jvm参数:-Xss256k + * @Author 欧阳思海 + * @Date 2019/9/30 11:05 + * @Version 1.0 + **/ +public class Test_02 { + + private int len = 1; + + public void stackTest() { + len++; + System.out.println("stack len:" + len); + stackTest(); + } + + public static void main(String[] args) { + Test_02 test = new Test_02(); + try { + test.stackTest(); + } catch (Throwable e) { + e.printStackTrace(); + } + } +} + +``` +运行时设置JVM参数 + +![](http://image.ouyangsihai.cn/Fp8Z9xGi-AN7k7laSOHupU7htMg9) + +输出结果: + +![](http://image.ouyangsihai.cn/FuHgFTcCaWlFjEtorqUbRF3RI_Cx) + + + +### 4 Java堆(Heap) + + + +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 + + +Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: + +![图片摘自https://blog.csdn.net/bruce128/article/details/79357870](http://image.ouyangsihai.cn/FvwbMlmR_k5r4xwnpH5LXDN-4qok) + + +* **新生代(Young)**: 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 + +如果把新生代再分的细致一点,新生代又可细分为**Eden空间**、**From Survivor空间**、**To Survivor空间**,默认比例为8:1:1。 + +* **老年代(Tenured/Old)**:在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。 +* **永久代(Perm)**:永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。 + + +其中**新生代和老年代组成了Java堆的全部内存区域**,而**永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现** + +另外,再强调一下堆空间内存分配的大体情况,这对于后面一些Jvm优化的技巧还是有帮助的。 + +- 老年代 : 三分之二的堆空间 +- 年轻代 : 三分之一的堆空间 + eden区: 8/10 的年轻代空间 + survivor0 : 1/10 的年轻代空间 + survivor1 : 1/10 的年轻代空间 + +最后,我们再通过一个简单的例子更加形象化的展示一下**堆溢出**的情况。 + +- JVM参数设置:-Xms10m -Xmx10m + +这里将堆的最小值和最大值都设置为10m,如果不了解这些参数的含义,可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) + +```java +/** + * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError + * @author zzm + */ +public class HeapTest { + + static class HeapObject { + } + + public static void main(String[] args) { + List list = new ArrayList(); + + //不断的向堆中添加对象 + while (true) { + list.add(new HeapObject()); + } + } +} + +``` + +输出结果: +![](http://image.ouyangsihai.cn/Fhky14SMLxHjx9R9ZCcY0jAJ8ljg) + +图中出现了`java.lang.OutOfMemoryError`,并且提示了`Java heap space`,这就说明是Java堆内存溢出的情况。 + +**堆的Dump文件分析** + +我的使用的是VisualVM工具进行分析,关于如何使用这个工具查看这篇文章([深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析 ](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html))。在运行程序之后,会同时打开VisualVM工具,查看堆内存的变化情况。 + +![](http://image.ouyangsihai.cn/Fhdj0VggJwgP-qAOdWBBrWmO5XrM) + +在上图中,可以看到,堆的最大值是30m,但是使用的堆的容量也快接近30m了,所以很容易发生堆内存溢出的情况。 + +接着查看dump文件。 + +![](http://image.ouyangsihai.cn/FpYV2YbCGR3ByPy3vFNJbVNpoLdW) + +如上图,堆中的大部分的对象都是HeapObject,所以,就是因为这个对象的一直产生,所以导致堆内存不够分配,所以出现内存溢出。 + +我们再看GC情况。 + +![](http://image.ouyangsihai.cn/FpB0KxmFAtZOx7g95dlfyAp8mLEV) + +如上图,Eden新生代总共48次minor gc,耗时1.168s,基本满足要求,但是survivor却没有,这不正常,同时Old Gen老年代总共27次full gc,耗时4.266s,耗时长,gc多,这正是因为大量的大对象进入到老年代导致的,所以,导致full gc频繁。 + +### 5 方法区(Method Area) + +**方法区(Method Area)** 与Java堆一样,是各个线程共享的内存区域。它用于存储一杯`虚拟机加载`的**类信息、常量、静态变量、及时编译器编译后的代码**等数据。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 **Non-Heap**。 + + + +**运行时常量池(Runtime Constant Pool)** + +**运行时常量池(Runtime Constant Pool)**是方法区的一部分。**Class文件**中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是**常量池(Constant Pool Table)**,用于存放编译期生成的各种字面量和符号引用,**这部分内容将在类加载后进入方法区的运行时常量池存放**。 + +Java虚拟机对Class文件每一部分(自然包括常量池)的格式有严格规定,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行。但**对于运行时常量池,Java虚拟机规范没有做任何有关细节的要求**,不同的提供商实现的虚拟机可以按照自己的需求来实现此内存区域。不过一般而言,除了保存**Class文件中的描述符号引用**外,还会把**翻译出的直接引用**也存储在运行时常量池中。 + +运行时常量池相对于Class文件常量池的另外一个重要特征是具备**动态性**,Java语言并不要求常量一定只有编译器才能产生,也就是**并非置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中**。 + +#### 运行时常量池举例 + +上面的**动态性**在开发中用的比较多的便是String类的`intern()` 方法。所以,我们以`intern()` 方法举例,讲解一下**运行时常量池**。 + +`String.intern()`是一个`native`方法,作用是:如果字符串常量池中已经包含有一个等于此String对象的字符串,则直接返回池中的字符串;否则,加入到池中,并返回。 + +```java +/** + * @ClassName MethodTest + * @Description vm参数设置:-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC + * @Author 欧阳思海 + * @Date 2019/11/25 20:06 + * @Version 1.0 + **/ + +public class MethodTest { + + public static void main(String[] args) { + List list = new ArrayList(); + long i = 0; + while (i < 1000000000) { + System.out.println(i); + list.add(String.valueOf(i++).intern()); + } + } +} +``` + +vm参数介绍: +>-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC +开始堆内存和最大堆内存都是512m,永久代大小10m,新生代和老年代1:4,E:S1:S2=8:1:1,最大经过15次survivor进入老年代,使用的,垃圾收集器是新生代ParNew,老年代CMS。 + +通过这样的设置之后,查看运行结果: +![](http://image.ouyangsihai.cn/FlpAIczI0f-h26J5QGYXdUkW2hbw) + +首先堆内存耗完,然后看看GC情况,设置这些参数之后,GC情况应该会不错,拭目以待。 + +![](http://image.ouyangsihai.cn/FvkHjwUTJcMK0j7KvrgLVWJOHODY) + +上图是GC情况,我们可以看到**新生代** 21 次minor gc,用了1.179秒,平均不到50ms一次,性能不错,**老年代** 117 次full gc,用了45.308s,平均一次不到1s,性能也不错,说明jvm运行是不错的。 + +>**注意:** 在JDK1.6及以前的版本中运行以上代码,因为我们通过`-XX:PermSize=10M -XX:MaxPermSize=10M`设置了方法区的大小,所以也就是设置了常量池的容量,所以运行之后,会报错:`java.lang.OutOfMemoryError:PermGen space`,这说明常量池溢出;在JDK1.7及以后的版本中,将会一直运行下去,不会报错,在前面也说到,JDK1.7及以后,去掉了永久代。 + +### 6 直接内存 + +**直接内存(Direct Memory)**并不是虚拟机**运行时数据区**的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致**OutOfMemoryError**异常出现。 + +这个我们实际中主要接触到的就是NIO,在NIO中,我们为了能够加快IO操作,采用了一种直接内存的方式,使得相比于传统的IO快了很多。 + +在NIO引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配**堆外内存**,然后通过一个存储在Java堆中的`DirectByteBuffer`对象作为这块内存的引用进行操作。这样能避免在Java堆和Native堆中来回复制数据,在一些场景里显著提高性能。 + +在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致动态扩展时出现**OutOfMemoryError**异常。 + +> JVM的四种引用? + +Java把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 + +- 强引用 + +以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 + +```java + String str = "sihai"; + List list = new Arraylist(); + list.add(str); +``` +以上就是一个强引用的例子,及时内存不足,该对象也不会被回收。 + +- 软引用 + +如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 + +软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。 + +当内存足够大时可以把数组存入软引用,取数据时就可从内存里取数据,提高运行效率。 + +- 弱引用 + +如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:**只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存**。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 + +弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +- 虚引用 + +如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:**虚引用必须和引用队列(ReferenceQueue)联合使用。** + +当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 + +**注意:** 在实际程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。 + +> GC用的可达性分析算法中,哪些对象可以作为GC Roots对象? + +- 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。 +- 方法区中类静态属性引用的对象。 +- 方法区中常量引用的对象。 +- 本地方法栈中JNI(即一般说的Native方法)引用的对象。 + +> 如何判断对象是不是垃圾? + +#### 引用计数算法 + +先讲讲第一个算法:**引用计数算法**。 + +其实,这个算法的思想非常的简单,一句话就是:**给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。** + +这些简单的算法现在是否还被大量的使用呢,其实,现在用的已经不多,没有被使用的最主要的原因是他有一个很大的**缺点**:**很难解决对象之间循环引用的问题**。 + +**循环引用**:当A有B的引用,B又有A的引用的时候,这个时候,即使A和B对象都为null,这个时候,引用计数算法也不会将他们进行垃圾回收。 + +```java +/** + * @ClassName Test_02 + * @Description + * @Author 欧阳思海 + * @Date 2019/12/5 16:59 + * @Version 1.0 + **/ +public class Test_02 { + + public static void main(String[] args) { + Instance instanceA = new Instance(); + Instance instanceB = new Instance(); + + instanceA.instance = instanceB; + instanceB.instance = instanceA; + + instanceA = null; + instanceB = null; + + System.gc(); + + Scanner scanner = new Scanner(System.in); + scanner.next(); + } +} + +class Instance{ + public Object instance = null; +} + +``` + +如果使用的是**引用计数算法**,这是不能被回收的,当然,现在的JVM是可以被回收的。 + +#### 可达性分析算法 + +这个算法的思想也是很简单的,这里有一个概念叫做**可达性分析**,如果知道图的数据结构,这里可以把每一个对象当做图中的一个节点,我们把一个节点叫做**GC Roots**,如果一个节点到**GC Roots**没有任何的相连的路径,那么就说明这个节点不可达,也就是这个节点可以被回收。 + +![](http://image.ouyangsihai.cn/FqyjRBThJ5HhXAIEH4UE7znJhOuk) + +上面图中,虽然obj7、8、9相互引用,但是到GC Roots不可达,所以,这种对象也是会被当做垃圾收集的。 + +在Java中,可以作为`GC Roots`的对象包括以下几种: + +- 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。 +- 方法区中类静态属性引用的对象。 +- 方法区中常量引用的对象。 +- 本地方法栈中JNI(即一般说的Native方法)引用的对象。 + +> 介绍一下JVM的堆 + +这个面试题其实和上面的有点重合,但是,这里单独拿出来再介绍一下,因为这个确实是比较常见的,这样大家也都有印象。 + +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 + +Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: + +![图片摘自https://blog.csdn.net/bruce128/article/details/79357870](http://image.ouyangsihai.cn/FvwbMlmR_k5r4xwnpH5LXDN-4qok) + + +* **新生代(Young)**: 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 + +如果把新生代再分的细致一点,新生代又可细分为**Eden空间**、**From Survivor空间**、**To Survivor空间**,默认比例为8:1:1。 + +* **老年代(Tenured/Old)**:在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。 +* **永久代(Perm)**:永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。 + + +其中**新生代和老年代组成了Java堆的全部内存区域**,而**永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现** + +另外,再强调一下堆空间内存分配的大体情况,这对于后面一些Jvm优化的技巧还是有帮助的。 + +- 老年代 : 三分之二的堆空间 +- 年轻代 : 三分之一的堆空间 + eden区: 8/10 的年轻代空间 + survivor0 : 1/10 的年轻代空间 + survivor1 : 1/10 的年轻代空间 + +最后,我们再通过一个简单的例子更加形象化的展示一下**堆溢出**的情况。 + +- JVM参数设置:-Xms10m -Xmx10m + +这里将堆的最小值和最大值都设置为10m,如果不了解这些参数的含义,可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) + +```java +/** + * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError + * @author zzm + */ +public class HeapTest { + + static class HeapObject { + } + + public static void main(String[] args) { + List list = new ArrayList(); + + //不断的向堆中添加对象 + while (true) { + list.add(new HeapObject()); + } + } +} + +``` + +输出结果: +![](http://image.ouyangsihai.cn/Fhky14SMLxHjx9R9ZCcY0jAJ8ljg) + +图中出现了`java.lang.OutOfMemoryError`,并且提示了`Java heap space`,这就说明是Java堆内存溢出的情况。 + +> 介绍一下Minor GC和Full GC + +这个概念首先我们要了解JVM的内存分区,在上面的面试题中已经做了介绍,这里就不再介绍了。 + +新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 + +老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 + +**内存分配规则** + +- 对象优先分配到Eden区,如果Eden区空间不够,则执行一次minor GC; +- 大对象直接进入到老年代; +- 长期存活的对象可以进入到老年代; +- 动态判断对象年龄。如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入到老年代。 + +在这里更加详细的规则介绍可以参考这篇文章:[深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-jvm-nei-cun-fen-pei-yu-hui-shou-ce-lue-yuan-li-cong-ci-gao-bie-jvm-nei-cun-fen-pei-wen-mang.html) + +> 说一下Java对象创建的方法 + +这个问题其实很简单,但是很多人却只知道new的方式。 + +- new的方法,最常见; +- 调用对象的clone方法; +- 使用反射,Class.forName(); +- 运用反序列化机制,java.io.ObjectInputStream对象的readObject()方法。 + +> 介绍一下几种垃圾收集算法? + +#### 标记-清除(Mark-Sweep)算法 + +**标记-清除(Mark-Sweep)** 算法是最基础的垃圾收集算法,后续的收集算法都是基于它的思路并对其不足进行改进而得到的。顾名思义,算法分成“标记”、“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,标记过程在前一节讲述对象标记判定时已经讲过了。 + +标记-清除算法的不足主要有以下两点: + +* **空间问题**,标记清除之后会产生大量不连续的**内存碎片**,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。 +* **效率问题**,因为内存碎片的存在,操作会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。 + +标记-清除算法的执行过程如下图所示: + +![](http://image.ouyangsihai.cn/Fn_nJ2vQKuX47-8rBn7IZrV5LfoH) + + +#### 复制(Copying)算法 + +为了解决标记-清除算法的效率问题,一种称为**“复制”(Copying)**的收集算法出现了,思想为:它**将可用内存按容量分成大小相等的两块**,每次只使用其中的一块。**当这一块内存用完,就将还存活着的对象复制到另一块上面**,然后再把已使用过的内存空间一次清理掉。 + +这样做使得**每次都是对整个半区进行内存回收**,内存分配时也就**不用考虑内存碎片**等复杂情况,只要**移动堆顶指针,按顺序分配内存**即可,实现简单,运行高效。只是这种算法的代价是**将内存缩小为原来的一半**,代价可能过高了。复制算法的执行过程如下图所示: + +![](http://image.ouyangsihai.cn/FoyNQk9dft20afZSCIzC7oVoJIHQ) + + + +##### 标记-整理(Mark-Compact)算法 + +复制算法在对象存活率较高时要进行较多的复制操作,效率将会变低。更关键的是:如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在**老年代一般不能直接选用复制算法**。 + +根据老年代的特点,**标记-整理(Mark-Compact)**算法被提出来,主要思想为:此算法的标记过程与**标记-清除**算法一样,但后续步骤不是直接对可回收对象进行清理,而是**让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。** 具体示意图如下所示: + +![](http://image.ouyangsihai.cn/Fov_rN7qL6R_DbGUNChUUOC4lqHS) + + +##### 分代收集(Generational Collection)算法 + +当前商业虚拟机的垃圾收集都采用**分代收集(Generational Collection)算法**,此算法相较于前几种没有什么新的特征,主要思想为:根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法: + +* **新生代** 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用**复制算法**,只需要付出少量存活对象的复制成本就可以完成收集。 + +* **老年代** 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用**标记-清除**或**标记-整理**算法来进行回收。 + +> 如何减少gc出现的次数 + +上面的面试题已经讲解到了,从年轻代空间(包括Eden和 Survivor 区域)回收内存被称为Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。 + +GC会stop the world。会暂停程序的执行,带来延迟的代价。所以在开发中,我们不希望GC的次数过多。 + +- 对象不用时最好显式置为 Null + +一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。 +- 尽量少用 System.gc() + +此函数建议 JVM 进行主 GC,虽然只是建议而非一定,但很多情况下它会触发主 GC,从而增加主 GC 的频率,也即增加了间歇性停顿的次数。 +- 尽量少用静态变量 + +静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。 +- 尽量使用 StringBuffer,而不用 String 来累加字符串 + +由于 String 是固定长的字符串对象。累加 String 对象时,并非在一个 String对象中扩增,而是重新创建新的 String 对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。 避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象 +- 分散对象创建或删除的时间 + +集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。 + +集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。 +- 尽量少用 finalize 函数 + +因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 +- 使用软引用类型 + +如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 + +- 尽量少用 finalize 函数。 + +因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 + +如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 + +- 能用基本类型如 int,long,就不用 Integer,Long 对象 + +基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。 + +- 增大-Xmx + +- 老年代代空间不足 + +老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: +```java +java.lang.OutOfMemoryError: Java heap space +``` +为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 + +- 永生区空间不足 + +JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,`Permanet Generation`中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,`Permanet Generation`可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: +```java +java.lang.OutOfMemoryError: PermGen space +``` +为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 + +- CMS GC时出现`promotion failed`和`concurrent mode failure` + +对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有`promotion failed`和`concurrent mode failure`两种状况,当这两种状况出现时可能会触发Full GC。 + +promotion failed是在进行Minor GC时,`survivor space`放不下、对象只能放入老年代,而此时老年代也放不下造成的;`concurrent mode failure`是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。 + +对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置`-XX: CMSMaxAbortablePrecleanTime=5`(单位为ms)来避免。 + +- 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间 + +这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 + +例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 + +当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 + +除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过`java -Dsun.rmi.dgc.client.gcInterval=3600000`来设置Full GC执行的间隔时间或通过`-XX:+ DisableExplicitGC`来禁止RMI调用`System.gc`。 + +- 堆中分配很大的对象 + +所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。 + +为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 + +> 数组多大会放在JVM老年代,永久代对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办? + +虚拟机提供了一个`-XX:PretenureSizeThreshold`参数(通常是3MB),令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。(新生代采用复制算法收集内存) + +垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完成垃圾回收(Full GC)。如果仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。 + +让对象实现finalize()方法,一次对象的自我拯救。 + diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" new file mode 100644 index 0000000..4282a2b --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" @@ -0,0 +1,169 @@ + +> 攻击网站的方法有哪些? + +**一是**,**DDOS攻击(分布式拒绝服务)**:利用攻击软件通过大量的机器同时对服务进行攻击,规模大,危害大。 + +目前有两种主流的DDOS攻击:**SYN Flood攻击**和**TCP全连接攻击**。 + +> - SYN Flood攻击:利用tcp协议的缺陷,发送大量伪造的tcp连接请求,导致被攻击方资源耗尽。出现的原因是:在TCP三次握手过程中,假设用户发送了SYN报文后掉线,那么服务器在发出SYN+ACK是无法收到客户端的ACK报文,这时,服务端是会一直不断的重试并等待一段时间后丢弃这个未完成的连接的,如果有大量的伪造的攻击报文,发送到了服务端,服务端将为了维护一个非常大的半连接队列而消耗过多的CPU时间和内存,这就会导致服务器失去响应。 +> - TCP全连接攻击:这种攻击是为了绕过防火墙设计的,它能够绕过防火墙,导致服务器有大量的TCP连接,最终导致服务器拒绝服务。 + +**解决SYN Flood攻击的方法** + +- 设置SYN timeout时间:SYN timeout时间是指服务端一直不断的重试并等待的这段时间。 +- 设置SYN cookie,判断是否连续收到某个ip的重复SYN报文,如果是,以后就丢弃这个ip地址的包 +- 设置SYN cache,先不分配系统资源,现用cache保存半开连接,直到收到正确的ACK在分配资源 +- 硬件防火墙 + +**解决TCP全连接攻击的方法** + +- 限制SYN流量 +- 定期扫描缺陷 +- 在骨干节点设置防火墙 +- 有足够的机器可以承受攻击 +- 过滤不必要的服务和端口 + +另外,**Land攻击**,该攻击是利用了TCP三次握手时,通过向一个主机发送一个用于建立连接的TCP SYN报文而实现对目标主机的攻击,这种方式与正常的TCP SYN报文不同的是:Land攻击的报文源ip地址和目标ip地址是一样的,都是目标主机的ip地址。 + +由于目标IP地址和源IP地址是一样的,因此,ACK报文就发给了主机本身,如果大量的SYN报文进行发送,目标计算机也不能正常工作。 + +**二是**,**XSS攻击(跨站脚本攻击)**,它是指恶意攻击者在web网页中插入恶意html代码,当用户浏览网页时,嵌入其中的html代码会执行,从而达到恶意攻击的目的。 + +**三是**,**CSRF攻击(跨站请求伪造)**,攻击者盗用了你的身份,以你的名义发送恶意请求,一般是在你登录了A网站以后,携带A网站的信息,然后请求危险B网站,从而导致。 + +对于**CSRF攻击(跨站请求伪造)** 可以使用下列方法进行防御: +- 用户关闭页面要及时清理cookie +- 在url后面添加伪随机数 +- 图片验证码等 + +**四是**,**SQL注入**,SQL注入是一种将SQL代码添加到输入参数中,传递到服务器解析并执行的一种攻击手法。 + +**SQL注入攻击**是输入参数未经过滤,然后直接拼接到SQL语句当中解析,执行达到预想之外的一种行为,称之为SQL注入攻击。 + +常见的sql注入,有下列几种: +- 数字注入 + +例如,查询的sql语句为:`SELECT * FROM user WHERE id = 1`,正常是没有问题的,如果我们进行sql注入,写成`SELECT * FROM user WHERE id = 1 or 1=1`,那么,这个语句永远都是成立的,这就有了问题,也就是sql注入。 + +- 字符串注入 + +字符串注入是因为注释的原因,导致sql错误的被执行,例如字符`#`、`--`。 + +例如,`SELECT * FROM user WHERE username = 'sihai'#'ADN password = '123456'`,这个sql语句'#'后面都被注释掉了,相当于`SELECT * FROM user WHERE username = 'sihai' `。 + +这种情况我们在mybatis中也是会存在的,所以在服务端写sql时,需要特别注意此类情况。 + +该如何防范此类问题呢? +- 严格检查输入变量的类型和格式,也就是对相关传入的参数进行验证,尽可能降低风险。 +- 过滤和转义特殊字符。 +- 利用mysql的预编译机制,在Java中mybatis也是有预编译的方法的,所以可以采用这种方式避免。 + +> mybatis中的 # 与 $ 区别? + +这个问题在面试中时常可能被问到,其实,面试官可能一想考考你对mybatis的熟悉程度,二是,想考考你对sql注入的理解。 + +我们都知道,mybatis中的动态sql经常会用到这两个符号。 + +在动态 SQL 解析阶段, #{ } 和 ${ } 会有不同的表现: +- #{ } 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符。 +- ${ } 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。 + +例如,`select * from user where name = #{name};`会解析为`select * from user where name = ?;`,而`select * from user where name = ${name};`如果我们传递参数"sihai"会解析为`select * from user where name = "sihai"`。 + +因此,**${ } 的变量的替换阶段是在动态 SQL 解析阶段,而 #{ }的变量的替换是在 DBMS 中**。 + +由上也可得,**尽量使用`#{ }`,可以防止sql注入,除非表名作为变量时,才使用`${ }`。** + + +> session的原理 + +Session是一种可以存储在内存、文件或者数据库中的键值对。 + +在程序需要为客户端创建一个请求的session时,服务器会先检查客户端的请求里是否包含一个session的标识,这个标识通常称为session id,如果已经存在,说明已经创建了session,就可以直接使用;如果不存在,则需要服务端重新创建一个session。 + +浏览器存储session的方式有三种: +- 使用Cookie存储,这是常见的方式,”记住我“的功能就是这种方式实现的。 +- Url重写,这种方式是直接把session id附加在url路径的后面,例如:www.baidu.com?sessionid=xxx。 +- 在页面表单中增加隐藏域。 + +#### session什么时候创建的呢 + +这个其实很简单,一般都是服务端在需要的时候使用某种方式进行创建,而不同语言的创建方式都是不一样的。 + +#### session什么时候删除? + +删除的时机很难说,但你不需要的时候就可以调用服务端的相关api进行删除,例如,注销登录时,我们就可以对用户session进行删除。 + +> Cookie的相关原理 + +在程序中,会话跟踪是很重要的事情。理论上,一个用户的所有请求操作都应该属于同一个会话,而另一个用户的所有请求操作则应该属于另一个会话,二者不能混淆。例如,用户A在超市购买的任何商品都应该放在A的购物车内,不论是用户A什么时间购买的,这都是属于同一个会话的,不能放入用户B或用户C的购物车内,这不属于同一个会话。 + +![](http://image.ouyangsihai.cn/Fv-jlc7N4TgtooXwIwhIdQUW0s9E) + +而Web应用程序是使用HTTP协议传输数据的。HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话。即用户A购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户A的会话还是用户B的会话了。要跟踪该会话,必须引入一种机制。 + +Cookie就是这样的一种机制。它可以弥补HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。 + +#### Cookie属性项 + +|属性名 |描述| +|-----|-----| +|String name|该Cookie的名称。Cookie一旦创建,名称便不可更改 +|Object value|该Cookie的值。如果值为Unicode字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64编码 +|int maxAge|该Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒之后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为–1 +|boolean secure|该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false +|String path|该Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序可以访问该Cookie。如果设置为“/”,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“/” +|String domain|可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.” +|String comment|该Cookie的用处说明。浏览器显示Cookie信息的时候显示该说明 +|int version|该Cookie使用的版本号。0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC 2109规范 + +#### Cookie的有效期 + +Cookie的maxAge决定着Cookie的有效期,单位为秒(Second)。 + +- 如果maxAge属性为正数,则表示该Cookie会在maxAge秒之后自动失效。浏览器会将maxAge为正数的Cookie持久化,即写到对应的Cookie文件中。无论客户关闭了浏览器还是电脑,只要还在maxAge秒之前,登录网站时该Cookie仍然有效。 +- 如果maxAge为负数,则表示该Cookie仅在本浏览器窗口以及本窗口打开的子窗口内有效,关闭窗口后该Cookie即失效。maxAge为负数的Cookie,为临时性Cookie,不会被持久化,不会被写到Cookie文件中。Cookie信息保存在浏览器内存中,因此关闭浏览器该Cookie就消失了。Cookie默认的maxAge值为–1。 +- 如果maxAge为0,则表示删除该Cookie。Cookie机制没有提供删除Cookie的方法,因此通过设置该Cookie即时失效实现删除Cookie的效果。 + +具体的操作方法每种语言都不一样,可以根据不同语言进行设置。 + +#### Cookie的修改和删除 + +Cookie不提供修改、删除的方法。如果要修改某个Cookie,只需要新建一个同名的Cookie,添加到response中覆盖原来的Cookie。 + +如果要删除某个Cookie,只需要新建一个同名的Cookie,并将maxAge设置为0。 + +#### Cookie跨域问题 + +Cookie是不可以跨域名的,隐私安全机制禁止网站非法获取其他网站的Cookie。 + +同一个一级域名下的两个二级域名也不能交互使用Cookie,比如a1.baidu.com和a2.baidu.com,因为二者的域名不完全相同。如果想要baidu.com名下的二级域名都可以使用该Cookie,需要设置Cookie的domain参数为.baidu.com,这样a1.baidu.com和a2.baidu.com就能访问同一个cookie。 + +#### Cookie被浏览器禁用怎么办? + +我们浏览网站的时候,很多网址会让我们选择是否可以使用cookie,如果你选择了禁用是否就没有方法了呢? + +- Url重写,这种方式是直接把session id附加在url路径的后面,例如:www.java1000.com?sessionid=xxx。 +- 在页面表单中增加隐藏域。 + +> Cookie和Session的区别? + +- 存储位置不同。Cookie存储在客户端,Session一般位于服务器上。 +- Session相对更加安全。Cookie是存在于客户端,对客户是可见的;而Session存储在服务端,对客户是透明的,不存在信息安全问题。因此,我们使用Cookie时,是不建议存储敏感信息的。 +- Session会消耗服务器资源,会为每个用户分配一个session,所以,当用户量很大时,会消耗大量的服务器内存;而Cookie存在于客户端,不占用服务器资源,如果用户量大,并发高,Cookie是很好的选择。 +- Cookie的容量有限制,单个Cookie的容量不能超过4KB,而session没有限制。 + +> Session和Cache的区别? + +- Session是单用户的会话状态。当用户访问网站时,就会对其生成一个Seesion,session的sessionid会保存到cookie中,用于后续会话。 +- Cache是服务端的缓存,是对项目的所有用户都是共享的,因为采用数据库的方式有些场景的接口响应不够好,所以采用缓存来进行优化,比如使用redis进行优化,提高访问速度。 + +> 经典面试题:在浏览器输入url到响应结果的整个过程,会用到哪些协议? + +整个请求的流程是这样的。 +- 输入url,进行域名解析,DNS协议解析域名获得IP +- 依据IP地址浏览器向服务器发送HTTP请求,使用TCP协议与服务器建立连接 +- 连接建立时要发送数据,发送数据在网络层使用IP协议 +- 期间IP数据包在路由器间路由选择使用OPSF协议 +- 路由器与服务器通信,需要将IP转换为MAC地址,使用ARP协议 +- 随即服务器处理请求,发回一个HTML响应,浏览器使用HTTP协议显示HTML页面 \ No newline at end of file diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" deleted file mode 100644 index 8ff7d76..0000000 --- "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" +++ /dev/null @@ -1,21 +0,0 @@ -面试官:OSI与TCP/IP的各层的结构,都有哪些协议呢? -我:哦,我好像知道,我说一下我的理解,这个主要有两种参考模型,一种是基于OSI的参考模型,可以分为7层,分别是:物理层、数据链路层、网络层、运输层、会话层、表示层和应用层;另一种是基于TCP、IP的参考模型,可以分为4层,分别是:网络接口层、网络层、运输层和应用层。 - -关于每一层的协议,我知道的还不少呢,我来说说我知道的吧。 - -应用层:RIP、FTP、DNS、Telnet、SMTP、HTTP、WWW - -我知道的还不少吧,嘚瑟一下,让我一口气说完吧。 - -表示层:JPEG、MPEG、ASCII,MIDI -会话层:RPC、SQL -传输层:TCP、UDP、SPX -网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGMP -数据链路层:PPP、HDLC、VLAN、MAC -物理层:RJ45、IEEE802.3 - -面试官:你能解释一下RJ45吗? - -我:嗯嗯嗯,你自己查一下吧。。。 - - diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" new file mode 100644 index 0000000..3160539 --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" @@ -0,0 +1,172 @@ +> OSI与TCP/IP的各层的结构,都有哪些协议呢? + +哦,我好像知道,我说一下我的理解,这个主要有两种参考模型,一种是基于OSI的参考模型,可以分为7层,分别是:物理层、数据链路层、网络层、运输层、会话层、表示层和应用层;另一种是基于TCP、IP的参考模型,可以分为4层,分别是:网络接口层、网络层、运输层和应用层。 + +关于每一层的协议,我知道的还不少呢,我来说说我知道的吧。 + +应用层:RIP、FTP、DNS、Telnet、SMTP、HTTP、WWW + +我知道的还不少吧,嘚瑟一下,让我一口气说完吧。 + +表示层:JPEG、MPEG、ASCII,MIDI +会话层:RPC、SQL +传输层:TCP、UDP、SPX +网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGMP +数据链路层:PPP、HDLC、VLAN、MAC +物理层:RJ45、IEEE802.3 + +你能解释一下RJ45吗? + +嗯嗯嗯,你自己查一下吧。。。 + +![](https://files.mdnice.com/user/4341/fe41b244-09cc-4fac-83bf-bef92a1388eb.png) + + +> 你能说说三次握手和四次挥手吗? + +![](https://files.mdnice.com/user/4341/0e1881a1-451b-4429-9d0d-8d821cabfcf3.png) + +三次握手需要经过下面的过程状态: +- LISTEN:表示服务器的某个Socket处于监听状态,可以接受连接了。 +- SYN_SENT:当客户端socket执行connet连接时,它首先发送一个SYN报文,紧接着进入SYN_SENT状态,并等待服务端发送三次握手中的第2个报文。 +- SYN_RCVD:这个状态表示收到了SYN报文,在正常状态服务端的socket在建立TCP连接时的三次握手会话中的一个中间状态,但是,需要注意的是netstat是很难看到这种状态。当客户端收到服务端的ACK后,服务端就进入到了ESTABLISHED状态了。 +- ESTABLISHED:表示已经建立连接了。 + +![](https://files.mdnice.com/user/4341/6f201ebf-704b-4cd1-ac1b-dd7e75da8b73.png) + +四次挥手过程状态: +- FIN_WAIT_1:这个状态其实是在建立连接时,一方想要主动关闭连接,然后向对方发送FIN报文,然后自己进入到此状态;同时,当对方回应ACK后,自己就进入到FIN_WAIT_2状态,FIN_WAIT_1也是比较难以看到的。(主动方状态) +- FIN_WAIT_2:这个状态是当主动要求关闭的一方要求关闭时,但暂时还有数据需要传输,稍后再关闭。(主动方状态) +- TIME_WAIT:表示收到了对方的FIN报文,并发送出了ACK报文,等待2MSL后即可回到CLOSED可用状态了。(主动方状态) +- CLOSE_WAIT:该状态表示在等待关闭,当对方发送FIN报文给自己时,你会发送一个ACK报文给对方,此时就进入到了CLOSE_WAIT状态。接下来,实际上你真正需要考虑的就是你是否还有数据发送给对方,如果此时没有了,那么你就可以close这个SOCKET了,发送FIN报文给对方,关闭连接。(被动方状态) +- LAST_ACK:状态是指被动方发送FIN报文后,最后等待对方的ACK报文,收到后,就可以进入到CLOSED状态。(被动方状态) +- CLOSED:表示连接中断。 + +结合上方的解释和两张图,就可以很好的理解三次握手和四次挥手的整个过程的,这个还是比较重要的,很多面试中都会考察,这也是计算机网络最基础的知识。 + +> 在TIME_WAIT状态中,如果TCP客户端的最后一次发送的ACK丢失了,会发生什么? + +如果丢失了,此时会触发重发机制,因为,在此状态下,等待的时间是依赖于实现方法的,一般可以为30s、1min和2min。等待结束后,就关闭连接了,并且所有的资源都会释放。 + +> 为什么收到Server的确认之后,client还需要进行第三次握手? + +可能由于网络连接延时,已经失效的连接到达服务端,这时服务端以为是客户端的请求,所以发送ack到客户端,然后等待客户端的数据传输过来,然而,此时,客户端并没有想要进行连接(因为此时客户端没有发出连接),因此,如果不采用三次握手,进行客户端确认,服务端就会一直等待客户端,导致服务端的资源白白浪费。 + +> 为什么要采用四次挥手 + +确保数据能够完全传输。 +挥手的过程是:主机1发送Fin,表示我没有数据要发送了,然后,主机2发送ACK进行确认,但是,这个时候,主机2还是可以发送数据给主机1的,不能就关闭连接,所以,只有当主机2也发送了Fin之后,才可以进行关闭。 + +> time _wait状态产生的原因 + +- 可靠的实现tcp全双工连接的终止:考虑网络是不可靠的,当客户端发送ACK服务端一定收到了,如果没有收到,可以重发,如果客户端在time-wait状态等待2MSL时长,还没有收到服务的的FIN,就可以关闭自己的连接了。 +- 允许老的重复连接在网络中消逝:网络中可能存在已经失效的连接,time-wait可以使得本连接持续的时间内产生的所有的连接都在网络中消逝,不会出现旧的连接了。 + +> time-wait过多的危害 + +本地端口数量有限,如果有大量的time-wait,会发现本地用于新建连接的端口缺乏,本地很难再建立新的对外连接。 + +> 如何消除大量TCP短连接引发的time—wait + +- 改为长连接 +- 增大可用端口 + +> 当关闭连接时,最后一个ACK丢失怎么办 + +如果在time-wait状态下,最后一个ACK丢失,由于有两个MSL时长等待,所有,会有新的FIN从服务端发送过来,这时,客户端会重新发送ACK。 +如果在closed状态下,客户端收不到服务端重传的FIN,客户端也不会重传ACK,那么服务端就永远无法关闭链接。 + +> TCP如何保证可靠传输 + +- 在传递数据之前,会有**三次握手**来建立连接 +- TCP会将数据分割为最合适发送的数据块,也就是分割为合理的长度 +- TCP发出一个段后,会启动定时器,等待目的端确认,如果不能收到确认,就会重发,这也就是**超时重发机制** +- 当TCP收到另一端的数据时,会向对方发送一个确认,通常会延迟一点发送,这是因为需要对包的完整性进行验证 +- TCP将保持首部和数据的检验和。目的是为了保证数据在传输过程中的任何变化,如果收到的段的检验和由差错,TCP会丢弃这个报文段和不确认收到此报文段 +- TCP会对失序的数据进行数据重新排序,然后再交给应用层 +- TCP会丢去重复的数据 +- TCP会进行**流量控制**。TCP的连接每一方都有一个固定大小的缓冲空间,其只允许接收端只允许另一端发送接收端缓冲区能接纳的数据,这可以防止缓冲区溢出。需要注意的是:TCP使用的流量控制协议是可变大小的滑动窗口协议 +- TCP会进行拥塞控制,当网络拥塞时,会适当的减少数据的发送。 + +> TCP建立连接之后,怎么保持连接? + +目前保持连接的方式有两种技术实现,第一,采用TCP协议层实现的Keepalive机制,第二,由应用层实现的HeartBeat心跳包。 + +**Keepalive机制** +该机制的原理是:TCP协议会向对方发一个keepalive探针包,对方收到包之后,正常会回复一个ACK,出现错误会回复一个RST,如果对方没有任何回复,服务器每隔一段时间再发送keepalive探针包,如果连续发送多个包之后都没有回复,则说明连接断开。 + +**心跳包机制** +该机制原理是:客户端或者服务端会发送一个类似心跳一样每隔固定时间发送一次,客户端在一定时间内没有收到服务端的回应,则可认为服务端不可用,同上,如果服务端在一定时间内没有收到客户端发送的心跳包,则认为客户端掉线。 + +> TCP三次握手有哪些漏洞? + +一是,**DDOS攻击(分布式拒绝服务)**:利用攻击软件通过大量的机器同时对服务进行攻击,规模大,危害大。 + +目前有两种主流的DDOS攻击:**SYN Flood攻击**和**TCP全连接攻击**。 + +> - SYN Flood攻击:利用tcp协议的缺陷,发送大量伪造的tcp连接请求,导致被攻击方资源耗尽。出现的原因是:在TCP三次握手过程中,假设用户发送了SYN报文后掉线,那么服务器在发出SYN+ACK是无法收到客户端的ACK报文,这时,服务端是会一直不断的重试并等待一段时间后丢弃这个未完成的连接的,如果有大量的伪造的攻击报文,发送到了服务端,服务端将为了维护一个非常大的半连接队列而消耗过多的CPU时间和内存,这就会导致服务器失去响应。 +> - TCP全连接攻击:这种攻击是为了绕过防火墙设计的,它能够绕过防火墙,导致服务器有大量的TCP连接,最终导致服务器拒绝服务。 + +**解决SYN Flood攻击的方法** + +- 设置SYN timeout时间:SYN timeout时间是指服务端一直不断的重试并等待的这段时间。 +- 设置SYN cookie,判断是否连续收到某个ip的重复SYN报文,如果是,以后就丢弃这个ip地址的包 +- 设置SYN cache,先不分配系统资源,现用cache保存半开连接,直到收到正确的ACK在分配资源 +- 硬件防火墙 + +**解决TCP全连接攻击的方法** + +- 限制SYN流量 +- 定期扫描缺陷 +- 在骨干节点设置防火墙 +- 有足够的机器可以承受攻击 +- 过滤不必要的服务和端口 + +二是,**Land攻击**,该攻击是利用了TCP三次握手时,通过向一个主机发送一个用于建立连接的TCP SYN报文而实现对目标主机的攻击,这种方式与正常的TCP SYN报文不同的是:Land攻击的报文源ip地址和目标ip地址是一样的,都是目标主机的ip地址。 + +由于目标IP地址和源IP地址是一样的,因此,ACK报文就发给了主机本身,如果大量的SYN报文进行发送,目标计算机也不能正常工作。 + +> tcp如何实现流量控制和拥塞控制? + +流量控制采用**滑动窗口机制**。 +拥塞控制采用**拥塞避免方法**。 + +>**滑动窗口原理** +TCP是全双工通信,因此每一方的滑动窗口都包括了接收窗口+发送窗口,接收窗口负责处理自己接收到的数据,发送窗口负责处理自己要发送出去的数据。滑**动窗口的本质其实就是维护几个变量,通过这些变量将TCP处理的数据分为几类,同时在发送出一个报文、接收一个报文对这些变量做一定的处理维护**。 +(1)N是发送窗口的起始字节,也就是说:字节序号 < N的字节都已经发送出去且已经收到ack,确认无误了; +(2)nextSeq就是下一次发送报文的首部Seq字段(Seq即b第一个字节的序号,这些这里不讲了),表示字节序号在 [N,nextSeq)区间的都已经使用过,发送出去了,但是还未收到ack确认; +(3) N+size就是窗口的最后一个可用字节序号,size是发送窗口的大小,就是每次接收到的报文中的Win字段的值,Win字段其实就是对方接收窗口的大小。 +**如何让维护这几个值呢?** +(1)每接收到一个一个报文要做如下事情:检查接收报文的ack,将N 置为 ack,即往前移到ack这个值;读取报文中的Win字段值,即对方的最新接收窗口大小,从而更新N+size的值。 +(2)每发送一个报文,就更改nextSeq的值,发送了多少个字节就把nextSeq往前移多少,但是不要超出N+size。 +![](https://img2018.cnblogs.com/i-beta/1743446/201912/1743446-20191228103841451-1429620351.png) + +**拥塞避免方法** +慢启动、拥塞避免、快重传和快恢复 + +![](http://image.ouyangsihai.cn/Fs-2RO7AcVWM9MKgh11-wdE8ln10) +![](http://image.ouyangsihai.cn/FuvM3wAfN4tJb7xf2RkSXey7qV0d) + +以上是两张原理图,根据这两张原理图应该可以比较好理解,如果想要深入理解,可以参考这篇文章:https://www.cnblogs.com/a3192048/p/12241296.html。 + +> 经典问题:TCP和UDP的区别? + +- TCP基于连接的协议,UDP是无连接的协议。 +- TCP可以保证数据发送的可靠性,UDP不可靠。 +- TCP可以重排序,UDP不可以。 +- TCP速度慢,UDP较快。 +- TCP是重量级协议,UDP是轻量级协议。 +- TCP有流量控制和拥塞控制机制,UDP没有。 +- TCP面向字节流的协议,UDP面向报文。 +- TCP只能单播,不能发送广播和组播,UDP可以。 + +**TCP应用场景:** 效率要求不高,但需要数据的准确性,例如,文件传输、邮件传输、远程登录等等。 +**UDP应用场景:** 效率要求高,准确性要求不高,例如,微信视频,语音聊天等。 + +> 为什么TCP比UDP安全,还有人用UDP、为什么UDP快? + +- UDP不需要建立连接. +- UDP不需要维护连接的状态. +- UDP头部开销小,只需要8字节. +- UDP没有拥塞控制,不会影响主机的发送频率,所以速度上比tcp有优势. + diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" new file mode 100644 index 0000000..582529e --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" @@ -0,0 +1,264 @@ +> Http的请求报文结构和响应报文结构 + +Http的请求报文主要由`请求行、请求头、空行、请求正文`。 + +**请求行**由请求方法、URL以及协议版本组成。 +请求方法:GET、HEAD、PUT、POST、TRACE、OPTIONS、DELETE以及扩展方法 +协议版本:常用的有HTTP/1.0和HTTP/1.1 + +**HTTP请求头** +| Header | 解释 | 示例 | +| --- | --- | --- | +| Accept | 指定客户端能够接收的内容类型 | Accept: text/plain, text/html | +| Accept-Charset | 浏览器可以接受的字符编码集。 | Accept-Charset: iso-8859-5 | +| Accept-Encoding | 指定浏览器可以支持的web服务器返回内容压缩编码类型。 | Accept-Encoding: compress, gzip | +| Accept-Language | 浏览器可接受的语言 | Accept-Language: en,zh | +| Accept-Ranges | 可以请求网页实体的一个或者多个子范围字段 | Accept-Ranges: bytes | +| Authorization | HTTP授权的授权证书 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Cache-Control | 指定请求和响应遵循的缓存机制 | Cache-Control: no-cache | +| Connection | 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) | Connection: close | +| Cookie | HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 | Cookie: $Version=1; Skin=new; | +| Content-Length | 请求的内容长度 | Content-Length: 348 | +| Content-Type | 请求的与实体对应的MIME信息 | Content-Type: application/x-www-form-urlencoded | +| Date | 请求发送的日期和时间 | Date: Tue, 15 Nov 2010 08:12:31 GMT | +| Expect | 请求的特定的服务器行为 | Expect: 100-continue | +| From | 发出请求的用户的Email | From: user@email.com | +| Host | 指定请求的服务器的域名和端口号 | Host: www.zcmhi.com | +| If-Match | 只有请求内容与实体相匹配才有效 | If-Match: “737060cd8c284d8af7ad3082f209582d” | +| If-Modified-Since | 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 | If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT | +| If-None-Match | 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 | If-None-Match: “737060cd8c284d8af7ad3082f209582d” | +| If-Range | 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag | If-Range: “737060cd8c284d8af7ad3082f209582d” | +| If-Unmodified-Since | 只在实体在指定时间之后未被修改才请求成功 | If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT | +| Max-Forwards | 限制信息通过代理和网关传送的时间 | Max-Forwards: 10 | +| Pragma | 用来包含实现特定的指令 | Pragma: no-cache | +| Proxy-Authorization | 连接到代理的授权证书 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Range | 只请求实体的一部分,指定范围 | Range: bytes=500-999 | +| Referer | 先前网页的地址,当前请求网页紧随其后,即来路 | Referer: http://www.zcmhi.com/archives/71.html | +| TE | 客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息 | TE: trailers,deflate;q=0.5 | +| Upgrade | 向服务器指定某种传输协议以便服务器进行转换(如果支持) | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | +| User-Agent | User-Agent的内容包含发出请求的用户信息 | User-Agent: Mozilla/5.0 (Linux; X11) | +| Via | 通知中间网关或代理服务器地址,通信协议 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) | +| Warning | 关于消息实体的警告信息 | Warn: 199 Miscellaneous warning | + +**请求正文** + +请求正文不一定都有的,比如,get请求方法就没有请求正文部分。 + +![](http://image.ouyangsihai.cn/FhKqcF6tPKzSLW_lstiSDGoNgDqa) + +例如,上图就是一个请求的实例,分别有:**请求行、请求头**,由于是一个Get请求,所以是没有请求体的,当然,比如post、put等请求,就会有请求体。 + +那么,http的响应报文结构,也是类似的。 + +主要由**状态行、响应头、空行、响应正文**组成。 + +![](http://image.ouyangsihai.cn/Fu2xla40yiujD2W7i-JaEhzbIIQT) + +常见的响应头有: + + + +响应头 | 说明 | +---------|----------| + Server | 服务器应用程序软件的名称和版本 | + Content-Type | 响应正文的类型(是图片或者二进制字符串等) | + Content-Length | 响应正文长度 | + Content-Charset | 响应正文使用的编码 | + Content-Encoding | 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader("Accept-Encoding"))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。 | + Content-Language | 响应正文使用的语言 | + Expires |应该在什么时候认为文档已经过期,从而不再缓存它? + Last-Modified | 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。 + + > http状态码 + +**HTTP状态码分类** +HTTP状态码共分为5种类型: +| 分类 | 分类描述 | +|-----|-----| +| 1** | 信息,服务器收到请求,需要请求者继续执行操作 | +| 2** | 成功,操作被成功接收并处理 | +| 3** | 重定向,需要进一步的操作以完成请求 | +| 4** | 客户端错误,请求包含语法错误或无法完成请求 | +| 5** | 服务器错误,服务器在处理请求的过程中发生了错误 | + +HTTP的状态码列表: + +| 状态码 | 状态码英文名称 | 中文描述 | +|-----|-----|-----| +| 100 | Continue | 继续。客户端应继续其请求 | +| 101 | Switching Protocols | 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议 | +| | +| 200 | OK | 请求成功。一般用于GET与POST请求 | +| 201 | Created | 已创建。成功请求并创建了新的资源 | +| 202 | Accepted | 已接受。已经接受请求,但未处理完成 | +| 203 | Non-Authoritative Information | 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本 | +| 204 | No Content | 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 | +| 205 | Reset Content | 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 | +| 206 | Partial Content | 部分内容。服务器成功处理了部分GET请求 | +| | +| 300 | Multiple Choices | 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择 | +| 301 | Moved Permanently | 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 | +| 302 | Found | 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI | +| 303 | See Other | 查看其它地址。与301类似。使用GET和POST请求查看 | +| 304 | Not Modified | 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 | +| 305 | Use Proxy | 使用代理。所请求的资源必须通过代理访问 | +| 306 | Unused | 已经被废弃的HTTP状态码 | +| 307 | Temporary Redirect | 临时重定向。与302类似。使用GET请求重定向 | +| | +| 400 | Bad Request | 客户端请求的语法错误,服务器无法理解 | +| 401 | Unauthorized | 请求要求用户的身份认证 | +| 402 | Payment Required | 保留,将来使用 | +| 403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 | +| 404 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面 | +| 405 | Method Not Allowed | 客户端请求中的方法被禁止 | +| 406 | Not Acceptable | 服务器无法根据客户端请求的内容特性完成请求 | +| 407 | Proxy Authentication Required | 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权 | +| 408 | Request Time-out | 服务器等待客户端发送的请求时间过长,超时 | +| 409 | Conflict | 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突 | +| 410 | Gone | 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置 | +| 411 | Length Required | 服务器无法处理客户端发送的不带Content-Length的请求信息 | +| 412 | Precondition Failed | 客户端请求信息的先决条件错误 | +| 413 | Request Entity Too Large | 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息 | +| 414 | Request-URI Too Large | 请求的URI过长(URI通常为网址),服务器无法处理 | +| 415 | Unsupported Media Type | 服务器无法处理请求附带的媒体格式 | +| 416 | Requested range not satisfiable | 客户端请求的范围无效 | +| 417 | Expectation Failed | 服务器无法满足Expect的请求头信息 | +| | +| 500 | Internal Server Error | 服务器内部错误,无法完成请求 | +| 501 | Not Implemented | 服务器不支持请求的功能,无法完成请求 | +| 502 | Bad Gateway | 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 | +| 503 | Service Unavailable | 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 | +| 504 | Gateway Time-out | 充当网关或代理的服务器,未及时从远端服务器获取请求 | +| 505 | HTTP Version not supported | 服务器不支持请求的HTTP协议的版本,无法完成处理 | + +> http的缓存机制了解吗?有关缓存的首部字段有哪些呢? + +**Last-Modified和If-Modifed-since** + +Last-Modified:是由服务器发往客户端的最后的时间 +If-Modified-since:是由客户端发往服务器的最后时间 + +用于记录页面最后修改的时间的Http头信息:服务器将请求的Last-Modified发往客户端,客户端如果没有缓存,则缓存在浏览器,然后,客户端每次将If-Modified-Since发往服务端,每次如果服务端发现服务端资源没有发生改变,则自动返回304,内容为空,这样节省了传输的数据量,如果服务端发现有新的资源,则返回200,并且返回新的资源,然后浏览器丢弃旧的资源,缓存新的资源。 + +**ETag和if-None-Match** + +ETag是服务器会为每个资源分配对应的ETag值,当资源内容发生改变时,其值也会发生改变。 + +ETag和if-None-Match的工作原理是在Http的Response中添加ETags,当客户端再次请求该资源时,将在Http的Request中加入if-None-Match信息(ETag值)。如果服务器资源验证的ETags没有改变,将返回一个304状态;否则,服务器返回一个200状态,并返回该资源和新的ETags。 + +**Expires/Cache-Control(优先使用)** + +Expires用于控制缓存的失效时间,但是有一个缺点就是失效时间是服务器返回的时间,跟客户端会存在延时,所以,在http1.1中,加入了Cache-Control,这声明是一种相对秒级,可以避免服务端和客户端时间不一致的问题。 + +Expires的格式:`expires=Fri, 17 Jul 2020 21:15:42 GMT`。 + +Cache-Control的格式:`'Cache-Control': 'max-age=2000000, no-store'` + + +> Last-Modified和Etag 如何帮助提高性能? + +一起使用,利用客户端缓存,服务端判断页面是否被修改。 + +> 有了Last-Modified为什么还要用ETag字段呢? + +- 文件修改非常频繁时,If-Modifed-since能检查到的粒度是秒级的,这种修改无法使用If-Modifed-since。 +- 某些文件周期性的修改,但是内容没有改变,这种无法体现,导致重新get。 +- 某些服务器不能精准的得到文件的最后修改时间,利用ETag提供了更加严格的验证。 + + +> http1.1和http1.0的区别? + +- 长连接与短连接的区别:http1.1使用长连接,在请求消息中会包含Connection:Keep-Alive的头域。 +- 分块传输: +在http1.0中,用于指定实体长度的唯一机制是通过content-length,静态资源没有问题,但对于动态资源,为了获取长度,只有等他完全生成以后才能获取content-length的值,这要求缓存整个响应,延长了响应用户的时间。 +在http1.1中,引入了分块的传输方法,该方法使得发送方可以将消息实体分割为任意大小的组块,在每个组块的前面都加上长度,最后的一块为空块,标识完成传输,这样避免了服务器端大量的占用缓存。 +- http状态码100 Continue: +当客户端发送的post请求的数据大于1024字节是,客户端并不是直接发起POST请求,而是分为两步,1)发送一个请求,包含Expect:100-continue,询问服务端是否接受数据;2)接受到服务端返回的100 continue后,才将数据发送给服务端。 +这是为了避免发送一个冗长的请求,占用服务端的资源。 +- Host域: +http1.1中,随着虚拟主机技术的发展,物理服务器上可以存在多个虚拟主机,所以,在1.1中可以在host中存在多个ip地址。 + +> http常用方法有哪些? + +|方法 | 说明 | 支持的HTTP协议版本| +|-----|------|------| +|GET | 获取资源| 1.0、1.1| +|POST | 传输实体主体 | 1.0、1.1| +|PUT | 传输文件 | 1.0、1.1| +|HEAD | 获得报文首部 | 1.0、1.1| +|DELETE | 删除文件 | 1.0、1.1| +|OPTIONS | 询问支持的方法 | 1.1| +|TRACE | 追踪路径 | 1.1| +|CONNECT | 要求用隧道协议连接代理 | 1.1| + +> http哪些是幂等的呢? + +幂等性是指一次或者多次操作所产生的副作用是一样的。 + +GET、Put、Delete是幂等的,Post不是幂等的。 + +> http的请求方法get和post的区别? + +- 从幂等性和安全性来说:Get是指查询或者获取资源,它是幂等的,同时也是安全的,而post一般是更新资源,既不是幂等的也不是安全的。 +- 从传输方式和传输数量大小限制方面:get请求放在请求行后面,有传输大小限制;post请求放在消息体中,没有传输大小限制。 +- 从是否缓存的角度:get请求的数据会被浏览器缓存起来,post则不会。 + +> 为什么http是无状态的,如何保持状态? + +无状态性是指:对事务处理没有记忆的能力,每一次的请求信息返回之后,就会丢弃,服务器无法知道与上次请求的联系, +好处:不用分配内存记录大量状态,节省服务端资源; +缺点:每次需要重新传输大量的数据。 + +保持状态采用的是会话跟踪技术(解决无状态性),主要的方法有:Cookie、Url重写、session和利用html嵌入表单域。 + +> http短连接和长连接原理 + +http1.0默认使用短连接,http1.1默认使用长连接,长连接是request中添加Connection:keep-alive,这样就是长连接。 + +另外,http的短连接和长连接,实质上就是TCP协议的长连接和短连接。 + +http长连接的优点: +- 通过开启或者关闭更少的连接,节约cpu时间和内存 +- 通过减少TCP开启和关闭引起的包的数量,降低网络阻塞 + +http长连接的缺点:服务器维护一个长连接会增加开销,消耗服务器资源。 + +> 说说http的特点 + +- 支持客户端、服务端通信模式 +- 简单方便快捷:发送请求,只需要简单的输入请求路径和请求方法即可,然后通过浏览器发送就可以了。 +- 灵活:http协议允许客户端和服务端传输任意类型任意合适的数据对象,这些不同类型由Content-Type标记。 +- 无连接:每次客户端发送请求到服务端,服务端响应之后,就会断开连接。 +- 无状态:http是无状态的协议,请求的处理没有记忆能力。 + +> http存在哪些安全性问题?怎么解决 + +- 通过使用明文不加密,内容可能被窃听 +- 不验证通信方的身份,可能遭到伪装 +- 无法验证报文的完整性,可能被篡改 + +可以采用https进行解决。 + +> https的作用,https中的安全性技术有哪些 + +https可以对内容进行加密,进行身份验证,同时可以检验数据完整性。 + +https的安全性技术: +- 对称性加密算法:用于真正传输的数据进行加密。 +- 非对称性加密算法:用于在握手过程中加密生成的密码,非对称性加密算法会生成公钥和私钥。 +- 散列算法:用于验证数据的完整性。 +- 数字证书:使用数字证书进行证明自己的身份。 + +> https和http的区别? + +- https更加安全:https可以通过加密、数据完整性验证和身份验证的方法保证数据的安全。 +- https需要申请证书,在CA申请证书。 +- 端口不同:http采用80,https采用443。 +- http协议运行在tcp之上,https运行在ssl、TLS之上的http协议,ssl、TLS运行在TCP之上。 + +> http和socket的区别? + +- http只能走tcp协议,socket可以走tcp和udp协议。 +- http基于请求响应模式,只有客户端发送给了服务端,服务端才可以响应,socket则可以通过服务端推送给客户端。 +- socket效率高,因为不需要解析报文头部字段等。 + From 2c20613f05cfbebd965a2f8f15a3b36a9286b51c Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Tue, 28 Dec 2021 08:45:51 +0800 Subject: [PATCH 06/17] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 37d6ef0..fa43602 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ _config.yml .settings .springBeans .sts4-cache +.vscode ### IntelliJ IDEA ### .idea From 4dfdb43eb6867fc25afb92558103f8c7d7df8118 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 2 Jan 2022 15:31:17 +0800 Subject: [PATCH 07/17] =?UTF-8?q?update:=20=E6=96=B0=E5=A2=9E=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +- ...10\351\235\242\350\257\225\351\242\230.md" | 0 ...37\346\234\272\351\235\242\350\257\225.md" | 961 +++++++++++++++++- 3 files changed, 953 insertions(+), 21 deletions(-) create mode 100644 "docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" diff --git a/README.md b/README.md index 6d273d1..f84ec42 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ ### 目录(ctrl + f 查找更香:不能点击的,还在写) +- [个人经验](#个人经验) - [项目准备](#项目准备) - [面试知识点](#面试知识点) - [公司面经](#公司面经) @@ -100,11 +101,16 @@ - [联系我](#联系我) - [公众号](#公众号) +## 个人经验 + +- [应届生如何准备校招,用我这一年的校招经历告诉你](https://sihai.blog.csdn.net/article/details/114258312?spm=1001.2014.3001.5502) +- [【大学到研究生自学Java的学习路线】这是一份最适合普通大众、非科班的路线,帮你快速找到一份满意的工作](https://sihai.blog.csdn.net/article/details/105964718?spm=1001.2014.3001.5502) +- [两个月的面试真实经历,告诉大家如何能够进入大厂工作?](https://sihai.blog.csdn.net/article/details/105807642) ## 项目准备 - [我的个人项目介绍模板](docs/interview/自我介绍和项目介绍.md) -- [本人真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!](https://mp.weixin.qq.com/s/JBtCRaw9Nabk9aIImjkFIQ) +- [本人面试两个月真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!](https://sihai.blog.csdn.net/article/details/105854760) - [项目必备知识及解决方案](docs/project/秒杀项目总结.md) ## 面试知识点 @@ -226,11 +232,12 @@ ### 算法 - +- [从大学入门到研究生拿大厂offer,必须看的数据结构与算法书籍推荐,不好不推荐!](https://sihai.blog.csdn.net/article/details/106011624?spm=1001.2014.3001.5502) - [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) - [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) - [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) -- 回溯算法不会,这篇文章一定得看 +- [dfs题目这样去接题,秒杀leetcode题目](https://sihai.blog.csdn.net/article/details/106895319) +- [回溯算法不会,这篇文章一定得看](https://sihai.blog.csdn.net/article/details/106993339) - 动态规划你了解多少,我来帮你入个们 - 链表的题目真的不难,看了这篇文章你就知道有多简单了 - 还在怕二叉树的题目吗? diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..e69de29 diff --git "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" index ce68d03..d8269e5 100644 --- "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" +++ "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" @@ -17,7 +17,7 @@ Java 虚拟机所管理的内存一共分为Method Area(方法区)、VM Stac 上图介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:**元数据区取代了永久代**。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:**元数据空间并不在虚拟机中,而是使用本地内存**。 -### 1 程序计数器(Program Counter Register) +#### 1 程序计数器(Program Counter Register) **程序计数器(Program Counter Register)**是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器**。在虚拟机概念模型中,**字节码解释器**工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 @@ -35,7 +35,7 @@ Java 虚拟机所管理的内存一共分为Method Area(方法区)、VM Stac 其实,我感觉这块区域,作为我们开发人员来说是不能过多的干预的,我们只需要了解有这个区域的存在就可以,并且也没有虚拟机相应的参数可以进行设置及控制。 -### 2 Java虚拟机栈(Java Virtual Machine Stacks) +#### 2 Java虚拟机栈(Java Virtual Machine Stacks) ![](http://image.ouyangsihai.cn/FksaMoFlAkSPkTB84bV4cK7xa8L3) @@ -60,7 +60,7 @@ Java虚拟机规范中对这个区域规定了两种异常状况: 一直觉得上面的概念性的知识还是比较抽象的,下面我们通过JVM参数的方式来控制栈的内存容量,模拟StackOverflowError异常现象。 -### 3 本地方法栈(Native Method Stack) +#### 3 本地方法栈(Native Method Stack) **本地方法栈(Native Method Stack)** 与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 @@ -109,11 +109,11 @@ public class Test_02 { -### 4 Java堆(Heap) +#### 4 Java堆(Heap) -对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: @@ -192,7 +192,7 @@ public class HeapTest { 如上图,Eden新生代总共48次minor gc,耗时1.168s,基本满足要求,但是survivor却没有,这不正常,同时Old Gen老年代总共27次full gc,耗时4.266s,耗时长,gc多,这正是因为大量的大对象进入到老年代导致的,所以,导致full gc频繁。 -### 5 方法区(Method Area) +#### 5 方法区(Method Area) **方法区(Method Area)** 与Java堆一样,是各个线程共享的内存区域。它用于存储一杯`虚拟机加载`的**类信息、常量、静态变量、及时编译器编译后的代码**等数据。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 **Non-Heap**。 @@ -249,7 +249,7 @@ vm参数介绍: >**注意:** 在JDK1.6及以前的版本中运行以上代码,因为我们通过`-XX:PermSize=10M -XX:MaxPermSize=10M`设置了方法区的大小,所以也就是设置了常量池的容量,所以运行之后,会报错:`java.lang.OutOfMemoryError:PermGen space`,这说明常量池溢出;在JDK1.7及以后的版本中,将会一直运行下去,不会报错,在前面也说到,JDK1.7及以后,去掉了永久代。 -### 6 直接内存 +#### 6 直接内存 **直接内存(Direct Memory)**并不是虚拟机**运行时数据区**的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致**OutOfMemoryError**异常出现。 @@ -369,7 +369,7 @@ class Instance{ 这个面试题其实和上面的有点重合,但是,这里单独拿出来再介绍一下,因为这个确实是比较常见的,这样大家也都有印象。 -对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: @@ -504,21 +504,21 @@ GC会stop the world。会暂停程序的执行,带来延迟的代价。所以 - 对象不用时最好显式置为 Null -一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。 +一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。 - 尽量少用 System.gc() -此函数建议 JVM 进行主 GC,虽然只是建议而非一定,但很多情况下它会触发主 GC,从而增加主 GC 的频率,也即增加了间歇性停顿的次数。 +此函数建议 JVM 进行主 GC,虽然只是建议而非一定,但很多情况下它会触发主 GC,从而增加主 GC 的频率,也即增加了间歇性停顿的次数。 - 尽量少用静态变量 -静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。 -- 尽量使用 StringBuffer,而不用 String 来累加字符串 +静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。 +- 尽量使用 StringBuffer,而不用 String 来累加字符串 -由于 String 是固定长的字符串对象。累加 String 对象时,并非在一个 String对象中扩增,而是重新创建新的 String 对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。 避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象 +由于 String 是固定长的字符串对象。累加 String 对象时,并非在一个 String对象中扩增,而是重新创建新的 String 对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。 避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象 - 分散对象创建或删除的时间 -集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。 +集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。 -集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。 +集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。 - 尽量少用 finalize 函数 因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 @@ -532,9 +532,9 @@ GC会stop the world。会暂停程序的执行,带来延迟的代价。所以 如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 -- 能用基本类型如 int,long,就不用 Integer,Long 对象 +- 能用基本类型如 int,long,就不用 Integer,Long 对象 -基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。 +基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。 - 增大-Xmx @@ -576,7 +576,7 @@ promotion failed是在进行Minor GC时,`survivor space`放不下、对象只 所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。 -为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 +为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 > 数组多大会放在JVM老年代,永久代对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办? @@ -586,3 +586,928 @@ promotion failed是在进行Minor GC时,`survivor space`放不下、对象只 让对象实现finalize()方法,一次对象的自我拯救。 +> JVM 常见的参数有哪些,介绍几个常见的,并说说在你工作中实际用到的地方? + +首先,JVM 中的参数是非常多的,而且可以分为不同的类型,主要可以分为以下三种类型: +- `标准参数(-)`,所有的JVM实现都必须实现这些参数的功能,而且向后兼容。 +- `非标准参数(-X)`,默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容。 +- `非Stable参数(-XX)`,此类参数各个JVM实现会有所不同,将来可能会随时取消,需要慎重使用。 + +虽然是这么分类的,实际上呢,非标准参数和非稳定的参数实际的使用中还是用的非常的多的。 + +#### 标准参数 +这一类参数可以说是我们刚刚开始Java是就用的非常多的参数了,比如`java -version`、`java -jar`等等,我们在CMD中输入`java -help`就可以获得Java当前版本的所有标准参数了。 + +![](http://image.ouyangsihai.cn/FsGtetpK2vpkQRFke44AyrDqbsl2) + +如上图就是JDK1.8的所有标准参数了,下面我们将介绍一些我们会用的比较多的参数。 + +- -client + +以client模式启动JVM,这种方式启动速度快,但运行时性能和内存管理效率不高,适合客户端程序或者开发调试。 + +- -server + +以server模式启动JVM,与client情况恰好相反。适合生产环境,适用于服务器。64位的JVM自动以server模式启动。 + +- -classpath或者-cp + +通知JVM类搜索路径。如果指定了`-classpath`,则JVM就忽略`CLASSPATH`中指定的路径。各路径之间以分号隔开。如果`-classpath`和`CLASSPATH`都没有指定,则JVM从当前路径寻找class。 + +JVM搜索路径的顺序: + +**1.先搜索JVM自带的jar或zip包。** + +Bootstrap,搜索路径可以用`System.getProperty("sun.boot.class.path")`获得; + +**2.搜索`JRE_HOME/lib/ext`下的jar包。** + +Extension,搜索路径可以用`System.getProperty("java.ext.dirs")`获得; + +**3.搜索用户自定义目录,顺序为:当前目录(.),CLASSPATH,-cp。** + +搜索路径用`System.getProperty("java.class.path")`获得。 + +```java +System.out.println(System.getProperty("sun.boot.class.path")); +System.out.println(System.getProperty("java.ext.dirs")); +System.out.println(System.getProperty("java.class.path")); +``` +![](http://image.ouyangsihai.cn/FnHtFC8cd9UucBd39nAiLcCrT5mY) + +如上就是我电脑的JVM的路径。 + +- -DpropertyName=value + +定义系统的全局属性值,如配置文件地址等,如果value有空格,则需要使用双引号。 + +另外用`System.getProperty("hello")`可以获得这些定义的属性值,在代码中也可以用`System.setProperty("hello","world")`的形式来定义属性。 + +如键值对设置为hello=world。 +![](http://image.ouyangsihai.cn/Fp74h47lvnSt4LAq4S7bKH_Qcilr) + +```java +System.out.println(System.getProperty("hello")); +``` +运行结果就是: +![](http://image.ouyangsihai.cn/Fu6bGzdxxsaJ_bOzjDbd1RUxFKIM) + +- -verbose + +查询GC问题最常用的命令之一,参数如下: +**-verbose:class** +输出JVM载入类的相关信息,当JVM报告说找不到类或者类冲突时可此进行诊断。 +**-verbose:gc** +输出每次GC的相关情况。 +**-verbose:jni** +输出native方法调用的相关情况,一般用于诊断jni调用错误信息。 + +另外,控制台**输出GC信息**还可以使用如下命令: + +在JVM的启动参数中加入`-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime`,按照参数的顺序分别输出GC的简要信息,GC的详细信息、GC的时间信息及GC造成的应用暂停的时间。 + +#### 非标准参数 + +非标准的参数主要是关于Java内存区域的设置参数,所以在看这些参数之前,应该先查看Java内存区域的基础知识,可以查看这篇文章:[深入理解Java虚拟机-Java内存区域透彻分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-java-nei-cun-qu-yu-tou-che-fen-xi.html)。 + +非标准参数实在标准参数的基础上的一些扩充参数,可以输入`java -X`,获得当前JVM支持的非标准参数。 + +![](http://image.ouyangsihai.cn/FkMW25ImmKNr7dZDD0pyysui2wYl) + +从图片中可以看出来,这些非标准的参数其实不多的,下面我们再 讲解一些比较常用的参数。 + +- -Xmn + +新生代内存大小的最大值,包括E区和两个S区的总和。设置方法:`-Xmn512m、-Xmn2g`。 + +- -Xms + +初始堆的大小,也是堆大小的最小值,默认值是总共的物理内存/64(且小于1G)。默认情况下,当堆中可用内存小于40%,堆内存会开始增加,一直增加到-Xmx的大小。 + +- -Xmx + +堆的最大值,默认值是总共的物理内存/64(且小于1G),默认情况下,当堆中可用内存大于70%,堆内存会开始减少,一直减小到-Xms的大小。 + +因此,为了避免这种浮动,所以在设置`-Xms`和`-Xmx`参数时,一般会设置成一样的,能够提高性能。 + +另外,官方默认的配置为**年老代大小:年轻代大小=2:1**左右,使用`-XX:NewRatio`可以设置年老代和年轻代之比,例如,`-XX:NewRatio=4`,表示**年老代:年轻代=4:1** + +- -Xss + +设置每个线程的栈内存,默认1M,一般来说是不需要改的。 + +- -Xprof + +跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。 + +- -Xnoclassgc + +禁用类垃圾收集,关闭针对class的gc功能;因为其阻止内存回收,所以可能会导致OutOfMemoryError错误,慎用。 + +- -Xincgc + +开启增量gc(默认为关闭);这有助于减少长时间GC时应用程序出现的停顿;但由于可能和应用程序并发执行,所以会降低CPU对应用的处理能力。 + +- -Xloggc:file + +与`-verbose:gc`功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。 + 若与verbose命令同时出现在命令行中,则以-Xloggc为准。 + +#### 4 非Stable参数 + +这类参数你一看官网以为不能使用呢,官网给你的建议就是这些参数不稳定,慎用,其实这主要的原因还是因为每个公司的实现都是不一样的,所以就是导致不稳定。但是呢,在实际的使用中却是非常的多的,而且这部分的参数很重要。 + +这些参数大致可以分为三类: + +- 性能参数(Performance Options):用于JVM的性能调优和内存分配控制,如初始化内存大小的设置; +- 行为参数(Behavioral Options):用于改变JVM的基础行为,如GC的方式和算法的选择; +- 调试参数(Debugging Options):用于监控、打印、输出等jvm参数,用于显示jvm更加详细的信息; + +**注意:以下参数都是JDK1.7及以下可以使用。** + +- 性能参数 + +|参数及其默认值| 描述| +|-----|-----| +|-XX:LargePageSizeInBytes=4m |设置用于Java堆的大页面尺寸| +|-XX:MaxHeapFreeRatio=70 |GC后java堆中空闲量占的最大比例| +|-XX:MinHeapFreeRatio=40 | GC后java堆中空闲量占的最小比例| +|**-XX:MaxNewSize=size** | 新生成对象能占用内存的最大值| +|**-XX:MaxPermSize=64m** |老生代对象能占用内存的最大值| +|**-XX:NewRatio=2** |新生代内存容量与老生代内存容量的比例| +|**-XX:NewSize=2.125m** | 新生代对象生成时占用内存的默认值| +|-XX:ReservedCodeCacheSize=32m |保留代码占用的内存容量| +|-XX:ThreadStackSize=512 |设置线程栈大小,若为0则使用系统默认值| +|-XX:+UseLargePages |使用大页面内存| + +- 行为参数 + + +|参数及其默认值 |描述| +|-----|-----| +|-XX:+ScavengeBeforeFullGC |新生代GC优先于Full GC执行| +|-XX:+UseGCOverheadLimit |在抛出OOM之前限制jvm耗费在GC上的时间比例| +|**-XX:-UseParNewGC**| 打开此开关,使用`ParNew+Serial Old`收集器| +|**-XX:-UseConcMarkSweepGC** |使用`ParNew+CMS+Serial Old`收集器对老生代采用并发标记交换算法进行GC| +|**-XX:-UseParallelGC** |启用并行GC,使用`ParallelScavenge+Serial Old`收集器| +|**-XX:-UseParallelOldGC** |对Full GC启用并行,当`-XX:-UseParallelGC`启用时该项自动启用,`ParallelScavenge+Parallel Old`收集器| +|**-XX:-UseSerialGC** |启用串行GC| +|**-XX:+UseG1GC**| 使用垃圾优先(G1)收集器| +|-XX:SurvivorRatio=n |Eden区域与Survivor区域大小之比。预设值为8| +|-XX:PretenureSizeThreshold=n|直接晋升到老年代的**对象大小**,设置这个参数之后,大于这个参数的对象直接进入到老年代分配| +|-XX:MaxTenuringThreshold=n|晋升到老年代的**对象年龄**,每个对象在坚持过一次Minor GC之后,年龄加1,当超过这个值之后就进入老年代。预设值为15| +|-XX:+UseAdaptiveSizePolicy|动态调整Java堆中各个区域的大小以及进入老年代的年龄| +| -XX:ParallelGCThreads=n|设置并行收集器收集时使用的CPU数。并行收集线程数| +|-XX:MaxGCPauseMillis=n|设置并行收集最大暂停时间| +|-XX:GCTimeRatio=n|设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+N)| +|-XX:+UseThreadPriorities |启用本地线程优先级| +|-XX:-DisableExplicitGC |禁止调用`System.gc()`;但jvm的gc仍然有效| +|-XX:+MaxFDLimit |最大化文件描述符的数量限制| + +前面6个参数都是关于**垃圾收集器**的行为参数,也是经常会用到的参数。 + +- 调试参数 + +|参数及其默认值| 描述| +|-----|-----| +|-XX:-CITime | 打印消耗在JIT编译的时间| +|-XX:ErrorFile=./hs_err_pid\.log |保存错误日志或者数据到文件中| +|-XX:HeapDumpPath=./java_pid\.hprof |指定导出堆信息时的路径或文件名| +|**-XX:-HeapDumpOnOutOfMemoryError** |当首次遭遇OOM时导出此时堆中相关信息| +|-XX:OnError="\;\" |出现致命ERROR之后运行自定义命令| +|-XX:OnOutOfMemoryError="\;\" |当首次遭遇OOM时执行自定义命令| +|-XX:-PrintClassHistogram |遇到Ctrl-Break后打印类实例的柱状信息,与`jmap -histo`功能相同| +|-XX:-PrintConcurrentLocks |遇到Ctrl-Break后打印并发锁的相关信息,与`jstack -l`功能相同| +|-XX:-PrintCommandLineFlags |打印在命令行中出现过的标记| +|-XX:-PrintCompilation |当一个方法被编译时打印相关信息| +|**-XX:-PrintGC** |每次GC时打印相关信息| +|**-XX:-PrintGCDetails** |每次GC时打印详细信息| +|-XX:-PrintGCTimeStamps |打印每次GC的时间戳| +|-XX:-TraceClassLoading |跟踪类的加载信息| +|-XX:-TraceClassLoadingPreorder |跟踪被引用到的所有类的加载信息| +|-XX:-TraceClassResolution | 跟踪常量池| +|-XX:-TraceClassUnloading | 跟踪类的卸载信息| +|-XX:-TraceLoaderConstraints | 跟踪类加载器约束的相关信息| + +以上标黑的就是常用的一些参数。 + +最后,给大家一个实例,看看在工作中是怎么去运用这些参数的,怎么在工作中去解决这些问题的。 + +#### **参数实例** + +设置`-Xms`、`-Xmn`和`-Xmx`参数分别为`-Xms512m -Xmx512m -Xmn128m`。同时设置新生代和老生代之比为1:4,E:S0:S1=8:1:1。除此之外,当然,你还可以设置一些其他的参数,比如,前面说到的,性能参数 `-XX:MaxNewSize`、 `-XX:NewRatio=`、,行为参数 `-XX:-UseParNewGC`,调试参数 `-XX:-PrintGCDetails`。 + +这些参数都是可以在 IDEA 中启动时直接设置的。 + +``` +** + * @ClassName MethodTest + * @Description vm参数设置:-Xms512m -Xmx512m -Xmn128m -XX:NewRatio=4 -XX:SurvivorRatio=8 + * @Author 欧阳思海 + * @Date 2019/11/25 20:06 + * @Version 1.0 + **/ + +public class MethodTest { + + public static void main(String[] args) { + List list = new ArrayList(); + long i = 0; + while (i < 1000000000) { + System.out.println(i); + list.add(String.valueOf(i++).intern()); + } + } +} +``` + +运行之后,用VisualVM查看相关信息是否正确。 + +当我们**没有设置**`-XX:NewRatio=4 -XX:SurvivorRatio=8`时,使用官方默认的情况如下: + +![](http://image.ouyangsihai.cn/FgIr8Niw2F9Cs1IK6ABn74oEi66T) + +上图可以看出,**新生代(Eden Space + Survivor 0 + Survivor 1):老年代(Old Gen)≈ 1:2**。 + +当我们**设置了**`-XX:NewRatio=4 -XX:SurvivorRatio=8`时,情况如下: + +![](http://image.ouyangsihai.cn/Fkc4DTkISF1LA9fpO54VEinaYvlc) + +变成了**新生代(Eden Space + Survivor 0 + Survivor 1):老年代(Old Gen)≈ 1:4**,Eden Space:Survivor 0: Survivor 1 = 8:1:1。 + +![](http://image.ouyangsihai.cn/FphOpfRSOQYu-pcR6xf6rDPcQAF9) + +从上图可知,堆的信息是正确的。 + +更多的使用方法可以参考这篇文章 [如何利用VisualVM对高并发项目进行性能分析](http://www.java1000.com/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html),有更加细致的介绍。 + +> 说说你了解的常见的内存调试工具:jps、jmap、jhat等。 + +|名称|解释| +|-----|-----| +|jps|显示指定系统内所有的HotSpot虚拟机的进程| +|jstat|用于收集HotSpot虚拟机各方面的运行数据| +|jinfo|显示虚拟机配置信息| +|jmap|生成虚拟机的内存转存储快照(heapdump文件),利用这个文件就可以分析内存等情况| +|jhat|用于分析上面jmap生成的heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果| +|jstack|显示虚拟机的线程快照| + +#### 1 jps:虚拟机进程状况工具 + +这个工具使用的频率还是非常高的,因为你需要查看你的程序的运行情况的时候,首先需要知道的就是程序所运行的**进程本地虚拟机唯一ID(LVMID)**,知道这个ID之后,才可以进行其他监控的操作。 + +- 命令格式: + +``` +jps [选项] [主机id] +``` + +- jps主要选项: + +|选项|解释| +|-----|-----| +|-q|只输出LVMID,省略主类名称| +|-m|输出虚拟机进程启动时传递给主类main()函数的参数| +|-l|输出主类的全名| +|-v|输出虚拟机进程启动时的 JVM 参数| + + + +- 实例 + +``` +jps -l +jps -v +``` +![](http://image.ouyangsihai.cn/Fq2nwxo46q4EpQ08Ba_qKIU2JN-J) + +![](http://image.ouyangsihai.cn/FmEvrlfb6kTEk8u_Hc-NcGHg8wW9) + +#### 2 jinfo:Java配置信息工具 + +jinfo的功能很简单,主要就是显示和查看调整虚拟机的各种参数。 + +- jinfo命令格式: + +``` +jinfo [选项] pid +``` + +- 相关选项 + +![](http://image.ouyangsihai.cn/FpSz04ZVzS6K6FJ3zVtnoYqMdLt7) + + +- 实例 + +我们在启动程序的时候设置一些JVM参数:`-Xms512m -Xmx512m -Xmn128m -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15`。 + +我们先使用`jps -l`查看虚拟机进程ID; +![](http://image.ouyangsihai.cn/FtsaKW9WE0lT-Vn_OH4CjpqWXqwz) + +再使用pid为1584进行查询参数; + +![](http://image.ouyangsihai.cn/FgOjSz1U0kS2yt2V-k_huM-f0zJ_) + +#### 3 jmap:Java内存映射工具 + +jmap的主要功能就是生成**堆转存储快照**,之后,我们再利用这个快照文件进行分析。 + +- jmap命令格式: + +``` +jmap [选项] vmid +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|-dump|生成Java堆转存储快照,格式:`-dump:[live,]format=b,file=`,其中`live`子参数说明是否只dump出存活的对象| +|-finalizerinfo|显示在F-Queue中等待Finalizer线程执行finalize方法的对象,**只在linux/Solaris有效**| +|-heap|显示Java堆详细信息,如使用哪种回收期、参数配置、分代状况等等。**只在Linux/Solaris有效**| +|-histo|显示堆中对象统计信息,包括类、实例数量、合计容量| +|-permstat|以ClassLoader为统计口径显示永久代内存状态,**只在Linux/Solaris有效**| +|-F|当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照,**只在Linux/Solaris有效**| + +- 实例 + +首先还是查看虚拟机ID; + +![](http://image.ouyangsihai.cn/FtuNhdGkitnyOGWFegoT2ETIOidf) + +然后再运行下面命令行; +![](http://image.ouyangsihai.cn/Fvi9c3BD9bSt4Qi1ptDkHaVH6jA1) + +打开这个dump文件如下; +![](http://image.ouyangsihai.cn/Fvd2a3KTLCykCyBX7fAosH65tvCw) + +ok,现在已经有了生成的dump文件,所以,我们就需要对这个文件进行解析,看看**jhat命令**。 + +#### 4 jhat:虚拟机堆转存储快照分析工具 + +虽然好像这个命令挺牛逼,但是,其实,由于这个工具的功能不太够,分析也得不到太多的结果,所以我们一般可以会将得到的dump文件用其他的工具进行分析,比如,可视化工具VisualVM等等。 + +所以,这里简单的做一个例子。 +- 实例 + +``` +jhat D:\dump.bin +``` + +![](http://image.ouyangsihai.cn/FsLOf6D2M9Ta8YiA9DCJ-zj6I-_X) + +如果有兴趣查看如何利用VisualVM进行查看,可以查看这篇文章:[深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html)。 + +#### 5 jstack:Java堆栈跟踪工具 + +`jstack` 命令主要用于生成虚拟机当前时刻的**线程快照**。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的**主要目的**:定位线程出现长时间停顿的原因、请求外部资源导致的长时间等待等这些原因。 + +- jstack命令格式: + +``` +jstack [选项] vmid +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|-F|当正常输出的请求不被响应时,强制输出线程堆栈| +|-l|除堆栈外,显示关于锁的附加信息| +|-m|如果调用到本地方法的话,可以显示C/C++的堆栈| + +- 实例 + +``` +jstack -l 6708 +``` +![](http://image.ouyangsihai.cn/FrcFS7iWpybtIg48OoDjrdmsSDJa) + + +#### 6 jstat:虚拟机统计信息监视工具 + +jstat这个工具还是很有作用的,他可以显示本地或者远程**虚拟机进程中的类装载、内存、垃圾收集、JIT编译**等运行数据,在服务器上,他是运行期定位虚拟机性能问题的首选工具。 + +- jstat命令格式: + +``` +jstat [选项 vmid [interval[s|ms] [count]]] +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|**-class**|监视类装载、卸载数量、总空间以及类装载所耗费的时间| +|**-gc**|监视Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等容量、已用空间、GC时间合计等信息| +|-gccapacity|监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间| +|**-gcutil**|监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比| +|-gccause|与-gcutil功能一样,但是会输出额外导致上一次GC产生的原因| +|-gcnew|监视新生代GC状况| +|-gcnewcapacity|监控内容与-gcnew一样,输出主要关注使用到的最大最小空间| +|-gcold|监控老年代GC状况| +|-gcoldcapacity|监控内容与-gcold一样,输出主要关注使用到的最大最小空间| +|-gcpermcapacity|输出永久代使用到的最大最小空间| +|-compiler|输出JIT编译器编译过的方法、耗时等信息| +|-printcompilation|输出已经被JIT编译过的方法| + +- 实例 + + +我们这里还关注一下虚拟机GC的状况。 + +``` +jstat -gcutil 9676 +``` +![](http://image.ouyangsihai.cn/FkelCR8JIup_jpEFY9UQvqD2l0EC) + +上面参数的含义: +>S0:第一个Survivor区的空间使用(%)大小 +S1:第二个Survivor区的空间使用(%)大小 +E:Eden区的空间使用(%)大小 +O:老年代空间使用(%)大小 +M:方法区空间使用(%)大小 +CCS:压缩类空间使用(%)大小 +YGC:年轻代垃圾回收次数 +YGCT:年轻代垃圾回收消耗时间 +FGC:老年代垃圾回收次数 +FGCT:老年代垃圾回收消耗时间 +GCT:垃圾回收消耗总时间 + +了解了这些参数的意义之后,我们就可以对虚拟机GC状况进行分析。我们发现年轻代回收次数`12`次,使用时间`1.672`s,老年代回收`0`次,使用时间`0`s,所有GC总耗时`1.672`s。 + +通过以上参数分析,发现老年代Full GC没有,说明没有大对象进入到老年代,整个老年代的GC情况还是不错的。另外,年轻代回收次数`12`次,使用时间`1.672`s,每次用时100ms左右,这个时间稍微长了一点,可以将新生代的空间调低一点,以降低每一次的GC时间。 + +> 常见的垃圾回收器有哪些? + +先上一张图,这张图是Java虚拟机的jdk1.7及以前版本的所有垃圾回收器,也可以说是比较成熟的垃圾回收器,除了这些垃圾回收器,面试的时候最多也就再怼怼G1和ZGC了。 + + + +上面的表示是年轻代的垃圾回收器:Serial、ParNew、Parallel Scavenge,下面表示是老年代的垃圾回收器:CMS、Parallel Old、Serial Old,以及不分老年代和年轻代的G1。之间的相互的连线表示可以相互配合使用。 + +说完是不是一篇明朗,其实也就是那么回事。 + +#### 新生代垃圾回收器 + +##### Serial + +Serial(串行)收集器是**最基本、发展历史最悠久**的收集器,它是采用**复制算法**的新生代收集器,曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个**单线程收集器**,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。 + +其实对于这个垃圾回收器,你只要记住是一个**单线程、采用复制算法的,会进行“Stop The World”** 即可,因为面试官一般不问这个,为什么,因为太简单了,没什么可问的呗。 + +好了,再放一张图好吧,说明一下Serial的回收过程,完事。 + + + +说明:这张图的意思就是**单线程,新生代使用复制算法标记、老年代使用标记整理算法标记**,就是这么简单。 + +##### ParNew + +**ParNew收集器就是Serial收集器的**多线程**版本**,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同。 + +需要注意一点是:**除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作。** + +最后再放一张回收过程图; + +![](http://image.ouyangsihai.cn/FvsKnXGzEQd6WYdUmIgLcWoSHG4H) + +*** 是不是很简单,我在这里讲这些知识点并不是为了深入去了解这些原理,基本的知道对于工作已经够了,其实,主要还是应付面试官,哈哈。 + +##### Parallel Scavenge + +Parallel Scavenge收集器也是一个**并行**的**多线程**新生代收集器,它也使用**复制算法**。 + +Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。 + +这里需要注意的唯一的区别是:Parallel Scavenge收集器的目标是**达到一个可控制的吞吐量(Throughput)**。 + +我们知道,停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而**高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务**。 + +#### 老年代垃圾回收器 + +##### Serial Old + +Serial Old 是Serial收集器的老年代版本,它同样是一个**单线程**收集器,使用“**标记-整理**”(Mark-Compact)算法。 + +在这里就可以出一个面试题了。 +- 为什么Serial使用的是**复制算法**,而Serial Old使用是**标记-整理**算法? +同一个爸爸,儿子长的天差地别,当然也有啊,哈哈。 + +> + 其实,看了我前面的文章你可能就知道了,因为在新生代绝大多数的内存都是会被回收的,所以留下来的需要回收的垃圾就很少了,所以复制算法更合适,你可以发现,基本的老年代的都是使用标记整理算法,当然,CMS是个杂种哈。 + + +它的工作流程与Serial收集器相同,下图是Serial/Serial Old配合使用的工作流程图: + + + +##### Parallel Old + +Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用**多线程**和“**标记-整理**”算法,是不是前面说的,老年代出了杂种CMS不是“**标记-整理**”算法,其他都是。 + +另外,有了Parallel Old垃圾回收器后,就出现了以“**吞吐量优先**”著称的“男女朋友”收集器了,这就是:**Parallel Old和Parallel Scavenge收集器的组合**。 + +Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图: + + + +你是不是以为我还要讲CMS和G1,我任性,我就是要接着讲,哈哈哈。 + + +#### CMS + +##### 小伙子,你说一下 CMS 垃圾回收器吧! + +这个题目一来,吓出一身冷汗,差点就没有复习这个CMS,还好昨晚抱佛脚看了一下哈。 + +于是我。。。一顿操作猛如虎。 + +CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是基于“标记-清除”算法实现的,并且常见的应用场景是**互联网站或者B/S系统的服务端上的Java应用**。 + +结果就一紧张就记得这么多,面试官肯定不满意了,这个时候,面试官的常规操作是,**继续严刑拷打,他想,你可能忘记了,我来提醒提醒你!** + +##### CMS收集器工作的整个流程是怎么样的,你能给我讲讲吗? + +这个时候,面试官还会安慰你说不用紧张,但是,安慰归安慰,最后挂不挂可是另一回事。 + +于是,我又开始回答问题。 + +CMS 处理过程有七个步骤: + +- **初始标记**,会导致stw; +- **并发标记**,与用户线程同时运行; +- **预清理**,与用户线程同时运行; +- **可被终止的预清理**,与用户线程同时运行; +- **重新标记** ,会导致swt; +- **并发清除**,与用户线程同时运行; + +其实,只要回答四个就差不多了,是这几个。 + +- **初始标记**:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。 +- **并发标记**:进行GC Roots Tracing的过程,在整个过程中耗时最长。 +- **重新标记**:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。 +- **并发清除**。 + +你以为这样子就可以了,面试官就会说可以了,如果可以了,那估计你凉了! + +##### 面试官说:CMS这么好,那有没有什么缺点呢? + +我。。。好吧,谁怪我这么强呢,对吧。 + +其实,CMS虽然经过这么些年的考验,已经是一个值得信赖的GC回收器了,但是,其实也是有一些他的不足的, + +第一,**垃圾碎片的问题**,我们都知道CMS是使用的是**标记-清除**算法的,所以不可避免的就是会出现垃圾碎片的问题。 +第二,**一般CMS的GC耗时80%都在remark阶段,remark阶段停顿时间会很长**,在CMS的这四个主要的阶段中,最费时间的就是重新标记阶段。 +第三,**concurrent mode failure**,说出这个的时候,面试官就会觉得,小伙子,哎呦,不错哟,掌握的比较清楚,那这个是什么意思呢,其实是说: + +>这个异常发生在cms正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。 + +第四,**promotion failed**,这个问题是指,在进行Minor GC时,Survivor空间不足,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年代有足够的空闲空间,但是由于碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的。 + +面试官看到你掌握的这么好,心里已经给你竖起来大拇指,但是,面试官觉得你优秀啊,就还想看看你到底还有多少东西。 + +##### 既然你知道有这么多的缺点,那么你知道怎么解决这些问题吗? + +这个真的被问蒙了,你以为我什么都会吗!!!! + +但是,我还是得给大家讲讲,不然下次被问到,可能会把锅甩给我。 + +- **垃圾碎片的问题**:针对这个问题,这时候我们需要用到这个参数:`-XX:CMSFullGCsBeforeCompaction=n` 意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次`full GC`才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 + +- **concurrent mode failure** + +解决这个问题其实很简单,只需要设置两个参数即可 + +`-XX:+UseCMSInitiatingOccupancyOnly` +`-XX:CMSInitiatingOccupancyFraction=60`:是指设定CMS在对内存占用率达到60%的时候开始GC。 + +为什么设置这两个参数呢?由于在垃圾收集阶段用户线程还需要运行,那也就还需要**预留有足够的内存空间给用户线程使用**,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集。 + +当然也不能设置过高,比如90%,这时候虽然GC次数少,但是,却会导致用于用户线程空间小,效率不高,太低10%,你自己想想会怎么样,体会体会! + +**哈哈,万事大吉,这一点说出了,估计面试官已经爱上我了吧,赶紧把我招进去干活吧。** + +- **remark阶段停顿时间会很长的问题**:解决这个问题巨简单,加入`-XX:+CMSScavengeBeforeRemark`。在执行remark操作之前先做一次`Young GC`,目的在于减少年轻代对老年代的无效引用,降低remark时的开销。 + +#### G1 (Garbage-First) + +G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。 + +- 可以像CMS收集器一样, GC 操作与应用的线程一起并发执行 +- 紧凑的空闲内存区间且没有很长的 GC 停顿时间 +- 需要可预测的GC暂停耗时 +- 不想牺牲太多吞吐量性能. +- 启动后不需要请求更大的Java堆 + +那么 G1 相对于 CMS 的区别在: + +- G1 在压缩空间方面有优势 + +- G1 通过将内存空间分成区域(Region)的方式避免内存碎片问题 + +- Eden, Survivor, Old 区不再固定、在内存使用效率上来说更灵活 + +- G1 可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象 + +- G1 在回收内存后会马上同时做合并空闲内存的工作、而 CMS 默认是在STW(stop the world)的时候做 + +- G1 会在 Young GC 中使用,而 CMS 只能在O区使用 + +就目前而言、CMS 还是默认首选的 GC 策略、可能在以下场景下 G1 更适合: + +- 服务端多核 CPU、JVM 内存占用较大的应用(至少大于4G) + +- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间 + +- 想要更可控、可预期的 GC 停顿周期;防止高并发下应用雪崩现象 + +G1的内存使用示意图: +![](http://image.ouyangsihai.cn/FgFGLjZMCGmjKscUAj3hBfK1HLKM) + +G1在运行过程中主要包含如下4种操作方式: + +- YGC(不同于CMS) + +- 并发阶段 + +- 混合模式 + +- full GC (一般是G1出现问题时发生) + +##### YGC(年轻代GC) +下面是一次 YGC 前后内存区域是示意图: +![](http://image.ouyangsihai.cn/FjX3b5ZidxSGhDRF-qiJ4SXm-ji5) + +图中每个小区块都代表 G1 的一个区域(Region),区块里面的字母代表不同的分代内存空间类型(如[E]Eden,[O]Old,[S]Survivor)空白的区块不属于任何一个分区;G1 可以在需要的时候任意指定这个区域属于 Eden 或是 O 区之类的。 + +YoungGC 在 Eden 充满时触发,在回收之后所有之前属于 Eden 的区块全变成空白。然后至少有一个区块是属于 S 区的(如图半满的那个区域),同时可能有一些数据移到了 O 区。 + +目前大都使用 PrintGCDetails 参数打出GC日志、这个参数对G1同样有效、但日志内容颇为不同。 + +下面是一个Young GC的例子: +``` +23.430: [GC pause (young), 0.23094400 secs] +... +[Eden: 1286M(1286M)->0B(1212M) +Survivors: 78M->152M Heap: 1454M(4096M)->242M(4096M)] +[Times: user=0.85 sys=0.05, real=0.23 secs] +``` +上面日志的内容解析:Young GC实际占用230毫秒、其中GC线程占用850毫秒的CPU时间 + +- E:内存占用从 1286MB 变成 0、都被移出 +- S:从 78M 增长到了 152M、说明从 Eden 移过来 74M +- Heap: 占用从 1454 变成 242M、说明这次 Young GC 一共释放了 1212M 内存空间 + +很多情况下,S 区的对象会有部分晋升到 Old 区,另外如果 S 区已满、Eden 存活的对象会直接晋升到 Old 区,这种情况下 Old 的空间就会涨。 + +##### 并发阶段 +一个并发G1回收周期前后内存占用情况如下图所示: +![](http://image.ouyangsihai.cn/Flo526oI2G_UIbkT-Jibo5ZoX27u) + +从上面的图表可以看出以下几点: + +- Young 区发生了变化、这意味着在 G1 并发阶段内至少发生了一次 YGC(这点和 CMS 就有区别),Eden 在标记之前已经被完全清空,因为在并发阶段应用线程同时在工作、所以可以看到 Eden 又有新的占用 +- 一些区域被X标记,这些区域属于 O 区,此时仍然有数据存放、不同之处在 G1 已标记出这些区域包含的垃圾最多、也就是回收收益最高的区域 +- 在并发阶段完成之后实际上 O 区的容量变得更大了(O+X 的方块)。这时因为这个过程中发生了 YGC 有新的对象进入所致。此外,这个阶段在 O 区没有回收任何对象:它的作用主要是标记出垃圾最多的区块出来。对象实际上是在后面的阶段真正开始被回收 + +G1 并发标记周期可以分成几个阶段、其中有些需要暂停应用线程。第一个阶段是初始标记阶段。这个阶段会暂停所有应用线程-部分原因是这个过程会执行一次 YGC、下面是一个日志示例: +``` +50.541: [GC pause (young) (initial-mark), 0.27767100 secs] +[Eden: 1220M(1220M)->0B(1220M) +Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)] +[Times: user=1.02 sys=0.04, real=0.28 secs] +``` +上面的日志表明发生了 YGC 、应用线程为此暂停了 280 毫秒,Eden 区被清空(71MB 从 Young 区移到了 O 区)。 + +日志里面 initial-mark 的字样表明后台的并发 GC 阶段开始了。因为初始标记阶段本身也是要暂停应用线程的,G1 正好在 YGC 的过程中把这个事情也一起干了。为此带来的额外开销不是很大、增加了 20% 的 CPU ,暂停时间相应的略微变长了些。 + +接下来,G1 开始扫描根区域、日志示例: +``` +50.819: [GC concurrent-root-region-scan-start] +51.408: [GC concurrent-root-region-scan-end, 0.5890230] +``` +一共花了 580 毫秒,这个过程没有暂停应用线程;是后台线程并行处理的。这个阶段不能被 YGC 所打断、因此后台线程有足够的 CPU 时间很关键。如果 Young 区空间恰好在 Root 扫描的时候满了、YGC 必须等待 root 扫描之后才能进行。带来的影响是 YGC 暂停时间会相应的增加。这时的 GC 日志是这样的: +``` +350.994: [GC pause (young) +351.093: [GC concurrent-root-region-scan-end, 0.6100090] +351.093: [GC concurrent-mark-start],0.37559600 secs] +``` +GC 暂停这里可以看出在 root 扫描结束之前就发生了,表明 YGC 发生了等待,等待时间大概是100毫秒。 + +在 root 扫描完成后,G1 进入了一个并发标记阶段。这个阶段也是完全后台进行的;GC 日志里面下面的信息代表这个阶段的开始和结束: +``` +111.382: [GC concurrent-mark-start] +.... +120.905: [GC concurrent-mark-end, 9.5225160 sec] +``` +并发标记阶段是可以被打断的,比如这个过程中发生了 YGC 就会。这个阶段之后会有一个二次标记阶段和清理阶段: +``` +120.910: [GC remark 120.959: +[GC ref-PRC, 0.0000890 secs], 0.0718990 secs] +[Times: user=0.23 sys=0.01, real=0.08 secs] +120.985: [GC cleanup 3510M->3434M(4096M), 0.0111040 secs] +[Times: user=0.04 sys=0.00, real=0.01 secs] +``` +这两个阶段同样会暂停应用线程,但时间很短。接下来还有额外的一次并发清理阶段: +``` +120.996: [GC concurrent-cleanup-start] +120.996: [GC concurrent-cleanup-end, 0.0004520] +``` +到此为止,正常的一个 G1 周期已完成–这个周期主要做的是发现哪些区域包含可回收的垃圾最多(标记为 X ),实际空间释放较少。 + +##### 混合 GC + +接下来 G1 执行一系列的混合 GC。这个时期因为会同时进行 YGC 和清理上面已标记为 X 的区域,所以称之为混合阶段,下面是一个混合 GC 执行的前后示意图: +![](http://image.ouyangsihai.cn/Flo526oI2G_UIbkT-Jibo5ZoX27u) + +像普通的 YGC 那样、G1 完全清空掉 Eden 同时调整 survivor 区。另外,两个标记也被回收了,他们有个共同的特点是包含最多可回收的对象,因此这两个区域绝对部分空间都被释放了。这两个区域任何存活的对象都被移到了其他区域(和 YGC 存活对象晋升到 O 区类似)。这就是为什么 G1 的堆比 CMS 内存碎片要少很多的原因——移动这些对象的同时也就是在压缩对内存。下面是一个混合GC的日志: +``` +79.826: [GC pause (mixed), 0.26161600 secs] +.... +[Eden: 1222M(1222M)->0B(1220M) +Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)] +[Times: user=1.01 sys=0.00, real=0.26 secs] +``` +上面的日志可以注意到 Eden 释放了 1222 MB、但整个堆的空间释放内存要大于这个数目。数量相差看起来比较少、只有 16 MB,但是要考虑同时有 survivor 区的对象晋升到 O 区;另外,每次混合 GC 只是清理一部分的 O 区内存,整个 GC 会一直持续到几乎所有的标记区域垃圾对象都被回收,这个阶段完了之后 G1 会重新回到正常的 YGC 阶段。周期性的,当O区内存占用达到一定数量之后 G1 又会开启一次新的并行 GC 阶段. + +G1来源:https://blog.51cto.com/lqding/1770055 + +### 总结 + +这里把上面的这些垃圾回收器做个总结,看完这个,面试给面试官讲的时候思路就非常清晰了。 + +|收集器|串行、并行or并发|新生代/老年代|算法|目标|适用场景| +|------|------|------|------|------|------| +|**Serial**|串行|新生代|复制算法|响应速度优先|单CPU环境下的Client模式 +|**Serial Old**|串行|老年代|标记-整理|响应速度优先|单CPU环境下的Client模式、CMS的后备预案 +|**ParNew**|并行|新生代|复制算法|响应速度优先|多CPU环境时在Server模式下与CMS配合 +|**Parallel Scavenge**|并行|新生代|复制算法|吞吐量优先|在后台运算而不需要太多交互的任务 +|**Parallel Old**|并行|老年代|标记-整理|吞吐量优先|在后台运算而不需要太多交互的任务 +|**CMS**|并发|老年代|标记-清除|一种以获取最短回收停顿时间为目标的收集器|互联网站或者B/S系统的服务端上的Java应用 +|**G1**|并发|老年代|标记-整理|高吞吐量|面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器 + + +> 内存泄漏和内存溢出,什么时候会出现,怎么解决? + +内存泄漏:(Memory Leak) 不再会被使用的对象的内存不能被回收,就是内存泄露。 + +强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象。 + +意思就是你用资源的时候为他开辟了一块空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出。 + +内存溢出:(Out Of Memory —— OOM) 指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM ,即所谓的内存溢出,简单来说就是自己所需要使用的空间比我们拥有的内存大内存不够使用所造成的内存溢出。 + +内存的释放:即清理那些不可达的对象,是由 GC 决定和执行的,所以 GC 会监控每一个对象的状态,包括申请、引用、被引用和赋值等。释放对象的根本原则就是对象不会再被使用。 + +#### 内存泄漏的原因 + +- 静态集合类引起内存泄漏; + +- 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。JDK1.8 貌似修正了引用对象修改参数,导致hashCode变更的问题; + +- 监听器 Listener 各种连接 Connection,没有及时关闭; + +- 内部类和外部模块的引用(尽量使用静态内部类); + +- 单例模式(静态类持有引用,导致对象不可回收); + +#### 解决方法 + +- 尽早释放无用对象的引用,及时关闭使用的资源,数据库连接等; +- 特别注意一些像 HashMap 、ArrayList 的集合对象,它们经常会引发内存泄漏。当它们被声明为 static 时,它们的生命周期就会和应用程序一样长。 +- 注意 事件监听 和 回调函数 。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册。 + +#### 内存溢出的情况和解决方法 + +- OOM for Heap (java.lang.OutOfMemoryError: Java heap space) + +此 OOM 是由于 JVM 中 heap 的最大值不满足需要,将设置 heap 的最大值调高即可,如,-Xmx8G。 + +- OOM for StackOverflowError (Exception in thread "main" java.lang.StackOverflowError) + +如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。 + +如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。 + +检查程序是否有深度递归。 + +- OOM for Perm (java.lang.OutOfMemoryError: PermGen space) + +调高 Perm 的最大值,即 -XX:MaxPermSize 的值调大。 + +- OOM for GC (java.lang.OutOfMemoryError: GC overhead limit exceeded) + +此 OOM 是由于 JVM 在 GC 时,对象过多,导致内存溢出,建议调整 GC 的策略,在一定比例下开始 GC 而不要使用默认的策略,或者将新代和老代设置合适的大小,需要进行微调存活率。 + +改变 GC 策略,在老代 80% 时就是开始 GC ,并且将`-XX:SurvivorRatio(-XX:SurvivorRatio=8)`和`-XX:NewRatio(-XX:NewRatio=4)`设置的更合理。 + +- OOM for native thread created (java.lang.OutOfMemoryError: unable to create new native thread) + +将 heap 及 perm 的最大值下调,并将线程栈调小,即 -Xss 调小,如:-Xss128k。 + +在 JVM 内存不能调小的前提下,将 -Xss 设置较小,如:-Xss:128k。 + +- OOM for allocate huge array (Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit) + +此类信息表明应用程序试图分配一个大于堆大小的数组。例如,如果应用程序 new 一个数组对象,大小为 512M,但是最大堆大小为 256M,因此 OutOfMemoryError 会抛出,因为数组的大小超过虚拟机的限制。 +1) 首先检查 heap 的 -Xmx 是不是设置的过小; +2) 如果 heap 的 -Xmx 已经足够大,那么请检查应用程序是不是存在 bug,例如:应用程序可能在计算数组的大小时,存在算法错误,导致数组的 size 很大,从而导致巨大的数组被分配。 + +- OOM for small swap (Exception in thread "main": java.lang.OutOfMemoryError: request bytes for . Out of swap space? ) + +由于从 native 堆中分配内存失败,并且堆内存可能接近耗尽。 + +1) 检查 os 的 swap 是不是没有设置或者设置的过小; +2) 检查是否有其他进程在消耗大量的内存,从而导致当前的 JVM 内存不够分配。 + +- OOM for exhausted native memory (java.lang.OutOfMemoryErr java.io.FileInputStream.readBytes(Native Method)) + +从错误日志来看,在 OOM 后面没有提示引起 OOM 的原因,进一步查看 stack trace 发现,导致 OOM 的原因是由Native Method 的调用引起的,另外检查 Java heap ,发现 heap 的使用正常,因而需要考虑问题的发生是由于 Native memory 被耗尽导致的。 + +从根本上来说,解决此问题的方法应该通过检测发生问题时的环境下,native memory 为什么被占用或者说为什么 native memory 越来越小,从而去解决引起 Native memory 减小的问题。但是如果此问题不容易分析时,可以通过以下方法或者结合起来处理。 + +1) cpu 和 os 保证是 64 位的,并且 jdk 也换为 64 位的。 +2) 将 java heap 的 -Xmx 尽量调小,但是保证在不影响应用使用的前提下。 +3) 限制对 native memory 的消耗,比如:将 thread 的 -Xss 调小,并且限制产生大量的线程;限制文件的 io 操作次数和数量;限制网络的使用等等。 + +> Java 的类加载过程 + +JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。 + +![](http://image.ouyangsihai.cn/FpontTFCT65kRlJvGhuMsP9lkRkZ) + + +#### 加载 + +加载过程主要完成三件事情: + +1. 通过类的全限定名来获取定义此类的二进制字节流 +2. 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构 +3. 在堆中生成一个代表此类的 java.lang.Class 对象,作为访问方法区这些数据结构的入口。 + +#### 校验 + +此阶段主要确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。 + +1. 文件格式验证:基于字节流验证。 +2. 元数据验证:基于**_方法区_**的存储结构验证。 +3. 字节码验证:基于方法区的存储结构验证。 +4. 符号引用验证:基于方法区的存储结构验证。 + +#### 准备 + +为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如: + +```java +public static int value = 123; +``` + +此时在准备阶段过后的初始值为0而不是 123 ;将 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器方法之中。特例: + +```java +public static final int value = 123; +``` + +此时 value 的值在准备阶段过后就是 123 。 + +#### 解析 + +把类型中的符号引用转换为直接引用。 + +* 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的 Class 文件格式中。 +* 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在 + +主要有以下四种: + +1. 类或接口的解析 +2. 字段解析 +3. 类方法解析 +4. 接口方法解析 + +#### 初始化 + +初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。 + +Java 中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始): + +1. 使用 new 关键字实例化对象、访问或者设置一个类的静态字段(被 final 修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。 +2. 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。 +3. 使用 java.lang.reflect 包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。 +4. 虚拟机启动时,用户会先初始化要执行的主类(含有main) +5. jdk 1.7后,如果 java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic 方法句柄,并且这个方法所在类没有初始化,则先初始化。 + +> 聊聊双亲委派机制 + +![](http://image.ouyangsihai.cn/FoFkvh_XGVUdHkp3F6dYBpwE8tgb) + +**工作过程** + +如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每个层次的类加载器都是如此,因此,所有的加载请求最终都会传送到 Bootstrap 类加载器(启动类加载器)中,只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 + +**优点** + +Java 类随着它的加载器一起具备了一种带有优先级的层次关系. + +例如类 java.lang.Object ,它存放在 rt.jar 之中,无论哪一个类加载器都要加载这个类,最终都是双亲委派模型最顶端的 Bootstrap 类加载器去加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户编写了一个称为 “java.lang.Object” 的类,并存放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,java类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。 From de13edb3ce98d0ff91366560f6fc3e69ae6b07c3 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 13 Feb 2022 12:36:08 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E5=92=8C=E9=A1=B9=E7=9B=AE=E5=AE=9E=E6=88=98=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++++---- ...350\241\250\351\235\242\350\257\225\351\242\230.md" | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 "docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" diff --git a/README.md b/README.md index f84ec42..8e5becc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 👉 Java学习资源汇总(个人总结) -- Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 +- Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://github.com/hello-go-maker/cs-learn-source - 面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi @@ -233,8 +233,8 @@ ### 算法 - [从大学入门到研究生拿大厂offer,必须看的数据结构与算法书籍推荐,不好不推荐!](https://sihai.blog.csdn.net/article/details/106011624?spm=1001.2014.3001.5502) -- [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) -- [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) +- [2021年面试高频算法题题解](docs/dataStructures-algorithms/高频算法题目总结.md) +- [2021年最新剑指offer难题解析](docs/dataStructures-algorithms/剑指offer难点总结.md) - [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) - [dfs题目这样去接题,秒杀leetcode题目](https://sihai.blog.csdn.net/article/details/106895319) - [回溯算法不会,这篇文章一定得看](https://sihai.blog.csdn.net/article/details/106993339) @@ -392,19 +392,21 @@ ## Java学习资源 +- [2021年Java视频学习教程+项目实战](https://github.com/hello-go-maker/cs-learn-source) - [2021 Java 1000G 最新学习资源大汇总](https://mp.weixin.qq.com/s/I0jimqziHqRNaIy0kXRCnw) ## Java书籍推荐 - [从入门到拿大厂offer,必须看的数据结构与算法书籍推荐](https://blog.ouyangsihai.cn/cong-ru-men-dao-na-da-han-offer-bi-xu-kan-de-suan-fa-shu-ji-tui-jian-bu-hao-bu-tui-jian.html) +- [全网最全电子书下载](https://github.com/hello-go-maker/cs-books) ## 实战项目推荐 >小心翼翼的告诉你,上面的资源当中就有很多**企业级项目**,没有项目一点不用怕,因为你看到了这个。 - [找工作,没有上的了台面的项目怎么办?](https://mp.weixin.qq.com/s/0oK43_z99pVY9dYVXyIeiw) - +- [Java 实战项目推荐](https://github.com/hello-go-maker/cs-learn-source) ## 程序人生 diff --git "a/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" "b/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..45fdc1a --- /dev/null +++ "b/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,6 @@ +### 链表基础结构 + +### 链表的常见操作 + +### 链表常见面试题 + From b012b6fb2033875a683c59ba4ec2c9f38b4f7c41 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 13 Feb 2022 16:40:37 +0800 Subject: [PATCH 09/17] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Java=E7=BB=93=E5=90=88?= =?UTF-8?q?=E9=9D=A2=E8=AF=95=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...10\351\235\242\350\257\225\351\242\230.md" | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" index e69de29..85a698b 100644 --- "a/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,114 @@ +### 集合面试题 + +> ArrayList、LinkedList和Vector的区别和实现原理 + +#### 数据结构实现 + +ArrayList和Vector都是基于可改变大小的数据实现的,而LinkedList是基于双链表实现的。 + +#### 增删改查效率对比 + +ArrayList和Vector都是基于可改变大小的数据实现的,因此,从指定的位置检索对象时,或在集合的末尾插入对象、删除一个对象的时间都是O(1),但是如果在其他位置增加或者删除对象,花费的时间是O(n); + +而LinkedList是基于双链表实现的,因此,在插入、删除集合中的任何位置上的对象,所花费的时间都是O(1),但基于链表的数据结构在查找元素时的效率是更低的,花费的时间为O(n)。 + +因此,从以上分析我们可以知道,查找特定的对象或者在集合末端增加或者删除对象,ArrayList和Vector的效率是ok的,如果在指定的位置删除或者插入,LinkedList的效率则更高。 + +#### 线程安全 + +ArrayList、LinkedList不具有线程安全性,在多线程的问题下是不能使用的,如果想要在多线程的环境下使用怎么办呢?我们可以采用Collections的静态方法synchronizedList包装一下,就可以保证线程安全了,但是在实际情况下,并不会使用这种方式,而是会采用更高级的集合进行线程安全的操作。 + +Vector是线程安全的,其保证线程安全的机制是采用synchronized关键字,我们都知道,这个关键字的效率是不高的,在后续的很多版本中,线程安全的机制都不会采用这种方式,因此,Vector的效率是比ArrayList、LinkedList更低效的。 + +#### 扩容机制 + +ArrayList和Vector都是基于数据这种数据结构实现的,因此,在集合的容量满了时,是需要进行扩容操作的。 + +在扩容时,ArrayList扩容后的容量是原先的1.5倍,扩容后,再将原先的数组中的数据拷贝到新建的数组中。 + +Vector默认情况下,扩容后的容量是原先的2倍,除此之外,Vector还有一种可以设置**容量增量**的机制,在Vector中有capacityIncrement变量用于控制扩容时的增量,具体的规则是:当capacityIncrement大于0时,扩容时增加的大小就是capacityIncrement的大小,如果capacityIncrement小于等于0时,则将容量增加为之前的2倍。 + +> HashMap原理分析 + +在分析HashMap的原理之前,先说明一下,大家应该都知道HashMap在JDK1.7和1.8的实现上是有较大的区别的,而面试官也是非常喜欢考察这一个点,因此,这里也是采用这两个JDK版本对比来进行分析,这样也可以印象更加深刻一些。 + +#### 数据结构 + +在数据结构的实现上,大家应该都知道,JDK1.7是数组+单链表的形式,而1.8采用的是数组+单链表+红黑树,具体的表现如下: + +|版本|数据结构|数组+链表的实现形式|红黑树实现形式| +|-|-|-|-| +|JDK1.8|数组+单链表+红黑树|Node|TreeNode| +|JDK1.7|数组+单链表|Entry|-| + +为了更好的让大家理解后续的讲解,这里先讲解一下HashMap中实现的一些重要参数。 + +- 容量(capacity): HashMap中数组的长度 + - 容量范围:必须是 2 的幂 + - 初始容量 = 哈希表创建时的容量 + - 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的 2^4 = 16 + `static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;` + - 最大容量 = 2的30次方 + `static final int MAXIMUM_CAPACITY = 1 << 30;` + +- 加载因子(Load factor):HashMap在其容量自动增加时,会设置加载因子,当达到设置的值时,就会触发自动扩容。 + - 加载因子越大、填满的元素越多,也就是说,空间利用率高、但冲突的机会加大、查找效率变低 + - 加载因子越小、填满的元素越少,也就是说,空间利用率小、冲突的机会减小、查找效率高 + // 实际加载因子 + `final float loadFactor;` + // 默认加载因子 = 0.75 + `static final float DEFAULT_LOAD_FACTOR = 0.75f;` + +- 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)。 + - 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数 + - 扩容阈值 = 容量 x 加载因子 + +#### 获取数据(get) + +HashMap的获取数据的过程大致如下: + +- 首先,根据key判断是否为空值; +- 如果为空,则到hashmap数组的第1个位置,寻找对应key为null的键; +- 如果不为空,则根据key计算hash值; +- 根据得到的hash值采用`hash & (length - 1)`的计算方式得到key在数组中的位置; +- 结束。 + +以上就是大致的数据获取流程,接下来,我们再对JDK1.7和1.8获取数据的细节做一个对比。 + +|版本|hash值的计算方式| +|-|-| +|JDK1.8|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 2次扰动 = 1次位运算+1次异或运算| +|JDK1.7|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算| + +#### 保存数据(put) + +HashMap的保存数据的过程大致如下: + +- 判读HashMap是否初始化,如果没有则进行初始化; +- 判断key是否为null,如果为null,则将key-value的数据存储在数组的第1个位置,这里与获取数据时对应的;否则,进行后续操作; +- 根据key计算数据存放的位置; +- 根据位置判断key是否存在,如果存在,则用新值替换旧值;如果不存在,则直接设置; + +这里也对保存数据的过程进行一个更加细致的对比。 + +|版本|hash值的计算方式|存放数据方式|插入数据方式| +|-|-|-|-| +|JDK1.8|1. hash = (key == null) ? 0 : hash(key);
2. 扰动处理 = 2次扰动 = 1次位运算+1次异或运算|数组+单链表+红黑树
- 无冲突,直接保存数据
- 冲突时,当链表长度小于8时,存放到单链表,当长度大于8时,存到到红黑树|尾插法| +|JDK1.7|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算|数组+单链表
- 无冲突,直接保存数据
- 冲突时,存放到单链表|头插法| + +#### 扩容机制 + +HashMap的扩容的过程大致如下: + +- 当发现容量不足时,开始扩容机制; +- 首先,保存旧数组,再根据旧容量的2倍新建数组; +- 遍历旧数组的每个元素,采用头插法的方式,将每个元素保存到新数组; +- 将新数组引用到hashmap的table属性上; +- 重新设置扩容阀值,完成扩容操作。 + +最后,也对扩容的过程进行一个更加细致的对比。 + +|版本|扩容后的位置计算方式|数据转移方式| +|-|-|-| +|JDK1.8|扩容后的位置 = 原位置 or 原位置+旧容量|尾插法| +|JDK1.7|扩容后的位置 = hashCode() -> 扰动处理 -> h & (length - 1)|头插法| From 4748d914b241e6b90656b3dd679a3e0b0b00a211 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 19 Jun 2022 09:31:47 +0800 Subject: [PATCH 10/17] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=9D=A2=E8=AF=95=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...36\347\216\260\347\211\210\346\234\254.md" | 3 + ...27\346\263\225\345\256\236\347\216\260.md" | 4 + ...5\215\227-Java\345\256\236\347\216\260.md" | 1 + ...72\347\241\200\347\237\245\350\257\206.md" | 1 + docs/java/IO/java IO.md | 1 + ...06\347\202\271\346\200\273\347\273\223.md" | 1 + ...00\345\255\243\347\254\224\350\256\260.md" | 2 + ...11\345\255\243\347\254\224\350\256\260.md" | 1 + ...14\345\255\243\347\254\224\350\256\260.md" | 0 .../Java\345\271\266\345\217\221.md" | 2 + ...04\346\226\231\346\261\207\346\200\273.md" | 25 + .../Dubbo.md" | 516 +++++ .../JavaIO.md" | 949 ++++++++++ .../Java\345\237\272\347\241\200.md" | 1313 +++++++++++++ .../Java\345\271\266\345\217\221.md" | 1478 +++++++++++++++ .../Java\345\274\202\345\270\270.md" | 739 ++++++++ ...va\350\231\232\346\213\237\346\234\272.md" | 627 ++++++ .../Java\351\233\206\345\220\210.md" | 923 +++++++++ .../Linux.md" | 676 +++++++ .../Nginx.md" | 516 +++++ .../elasticSearch.md" | 296 +++ .../kafka.md" | 1 + .../mybatis.md" | 565 ++++++ .../mysql.md" | 1682 +++++++++++++++++ .../netty.md" | 1 + .../rabbitMQ.md" | 358 ++++ .../redis.md" | 774 ++++++++ .../spring mvc.md" | 267 +++ .../spring.md" | 782 ++++++++ .../springboot.md" | 307 +++ .../springcloud.md" | 446 +++++ .../tomcat.md" | 253 +++ .../zookeeper.md" | 438 +++++ ...27\346\234\272\347\275\221\347\273\234.md" | 644 +++++++ ...76\350\256\241\346\250\241\345\274\217.md" | 1620 ++++++++++++++++ 35 files changed, 16212 insertions(+) create mode 100644 "docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" create mode 100644 "docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" create mode 100644 "docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" create mode 100644 "docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" create mode 100644 docs/java/IO/java IO.md create mode 100644 "docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" create mode 100644 "docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" create mode 100644 "docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" create mode 100644 "docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" create mode 100644 "docs/java/Multithread/Java\345\271\266\345\217\221.md" create mode 100644 "docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mysql.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/netty.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/rabbitMQ.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/redis.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/spring mvc.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/spring.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/springboot.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/springcloud.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/tomcat.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/zookeeper.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/\350\256\276\350\256\241\346\250\241\345\274\217.md" diff --git "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" new file mode 100644 index 0000000..256f99f --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" @@ -0,0 +1,3 @@ +https://blog.csdn.net/weixin_43774841/article/details/112912070 +https://zhuanlan.zhihu.com/p/84481303 +https://zhuanlan.zhihu.com/p/84481166 \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" "b/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" new file mode 100644 index 0000000..8c53251 --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" @@ -0,0 +1,4 @@ +https://juejin.cn/post/6844903779289006094 +https://juejin.cn/post/6844903779289022478 +https://juejin.cn/post/6844903779289006093 +https://juejin.cn/post/6844903779289022471 \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" "b/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" new file mode 100644 index 0000000..6e99702 --- /dev/null +++ "b/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" @@ -0,0 +1 @@ +https://github.com/LyricYang/Internet-Recruiting-Algorithm-Problems/blob/master/CodeInterviewGuide/README.md \ No newline at end of file diff --git "a/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" new file mode 100644 index 0000000..5a97f4b --- /dev/null +++ "b/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -0,0 +1 @@ +https://juejin.cn/post/6844904127059738631 \ No newline at end of file diff --git a/docs/java/IO/java IO.md b/docs/java/IO/java IO.md new file mode 100644 index 0000000..c197d76 --- /dev/null +++ b/docs/java/IO/java IO.md @@ -0,0 +1 @@ +https://juejin.cn/post/6844904125700784136 \ No newline at end of file diff --git "a/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" "b/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" new file mode 100644 index 0000000..459cd09 --- /dev/null +++ "b/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" @@ -0,0 +1 @@ +https://github.com/AobingJava/JavaFamily \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..6052a3f --- /dev/null +++ "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" @@ -0,0 +1,2 @@ +https://www.yuque.com/books/share/327d9543-85d2-418f-9315-41c3e19d2768/0dca325c876a0e85f0ba4ea48042e61d +https://github.com/shishan100/Java-Interview-Advanced#%E5%88%86%E5%B8%83%E5%BC%8F%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97 \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..42773c7 --- /dev/null +++ "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" @@ -0,0 +1 @@ +https://blog.csdn.net/u013073869/article/details/105271345 \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..e69de29 diff --git "a/docs/java/Multithread/Java\345\271\266\345\217\221.md" "b/docs/java/Multithread/Java\345\271\266\345\217\221.md" new file mode 100644 index 0000000..4a068da --- /dev/null +++ "b/docs/java/Multithread/Java\345\271\266\345\217\221.md" @@ -0,0 +1,2 @@ +参考:https://juejin.cn/post/6844904063687983111 +参考:https://www.cmsblogs.com/category/1391296887813967872 \ No newline at end of file diff --git "a/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" "b/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" new file mode 100644 index 0000000..e69961f --- /dev/null +++ "b/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" @@ -0,0 +1,25 @@ +## Java 集合 + +参考:https://www.cmsblogs.com/article/1391291996752187392 +参考:https://juejin.cn/post/6844904125939843079 + +- ArrayList + +- LinkedList + +- HashMap + +- TreeMap + +- TreeSet + +- LinkedHashMap + +- ConcurrentHashMap + +- ArrayBlockingQueue + +- LinkedBlockingQueue + +- PriorityBlockingQueue + diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" new file mode 100644 index 0000000..ab40459 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" @@ -0,0 +1,516 @@ + + +## 基础知识 + +### 为什么要用 Dubbo? + +* 随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。就这样为分布式系统的服务治理框架就出现了,Dubbo 也就这样产生了。 + +### Dubbo 是什么? + +* Dubbo 是一款高性能、轻量级的开源 RPC 框架,提供服务自动注册、自动发现等高效服务治理方案, 可以和 Spring 框架无缝集成。 + +### Dubbo 的使用场景有哪些? + +* 透明化的远程方法调用:就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。 +* 软负载均衡及容错机制:可在内网替代 F5 等硬件负载均衡器,降低成本,减少单点。 +* 服务自动注册与发现:不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。 + +### Dubbo 核心功能有哪些? + +* Remoting:网络通信框架,提供对多种NIO框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。 +* Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 +* Registry:服务注册,基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。 + +### Dubbo 核心组件有哪些? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2bd87ef0~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Provider:暴露服务的服务提供方 +* Consumer:调用远程服务消费方 +* Registry:服务注册与发现注册中心 +* Monitor:监控中心和访问调用统计 +* Container:服务运行容器 + +### Dubbo 服务器注册与发现的流程? + +* 服务容器Container负责启动,加载,运行服务提供者。 +* 服务提供者Provider在启动时,向注册中心注册自己提供的服务。 +* 服务消费者Consumer在启动时,向注册中心订阅自己所需的服务。 +* 注册中心Registry返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +* 服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +* 服务消费者Consumer和提供者Provider,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心Monitor。 + +## 架构设计 + +### Dubbo 的整体架构设计有哪些分层? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2d11703a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 接口服务层(Service):该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现 +* 配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心 +* 服务代理层(Proxy):服务接口透明代理,生成服务的客户端 Stub 和 服务端的 Skeleton,以 ServiceProxy 为中心,扩展接口为 ProxyFactory +* 服务注册层(Registry):封装服务地址的注册和发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry、RegistryService +* 路由层(Cluster):封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce +* 监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService +* 远程调用层(Protocal):封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter +* 信息交换层(Exchange):封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer +* 网络 传输 层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec +* 数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool + +### Dubbo Monitor 实现原理? + +* Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。 + +1. MonitorFilter 向 DubboMonitor 发送数据 + +2. DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference + +3. SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000) + +4. SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写) + +5. SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表 + +## 分布式框架 + +### Dubbo 类似的分布式框架还有哪些? + +* 比较著名的就是 Spring Cloud。 + +### Dubbo 和 Spring Cloud 有什么关系? + +* Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。而 Spring Cloud 诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了 Spring、Spring Boot 的优势之上,两个框架在开始目标就不一致,Dubbo 定位服务治理、Spring Cloud 是打造一个生态。 + +#### Dubbo 和 Spring Cloud 有什么哪些区别? + +* Dubbo 底层是使用 Netty 这样的 NIO 框架,是基于 TCP 协议传输的,配合以 Hession 序列化完成 RPC 通信。 + +* Spring Cloud 是基于 Http 协议 Rest 接口调用远程过程的通信,相对来说 Http 请求会有更大的报文,占的带宽也会更多。但是 REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更为合适,至于注重通信速度还是方便灵活性,具体情况具体考虑。 + +### Dubbo 和 Dubbox 之间的区别? + +* Dubbox 是继 Dubbo 停止维护后,当当网基于 Dubbo 做的一个扩展项目,如加了服务可 Restful 调用,更新了开源组件等。 + +## 注册中心 + +### Dubbo 有哪些注册中心? + +* Multicast 注册中心:Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。 +* Zookeeper 注册中心:基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。 +* Redis 注册中心:基于 Redis 实现,采用 key/map 存储,key 存储服务名和类型,map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。 +* Simple 注册中心。 +* 推荐使用 Zookeeper 作为注册中心 + +### Dubbo 的注册中心集群挂掉,发布者和订阅者之间还能通信么? + +* 可以通讯。启动 Dubbo 时,消费者会从 Zookeeper 拉取注册的生产者的地址接口等数据,缓存在本地。每次调用时,按照本地存储的地址进行调用。 + +## 集群 + +### Dubbo集群提供了哪些负载均衡策略? + +* Random LoadBalance: 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀。 + +* RoundRobin LoadBalance: 轮循选取提供者策略,平均分布,但是存在请求累积的问题。 + +* LeastActive LoadBalance: 最少活跃调用策略,解决慢提供者接收更少的请求。 + +* ConstantHash LoadBalance: 一致性 Hash 策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动。 + +`默认为 Random 随机调用。` + +### Dubbo的集群容错方案有哪些? + +* Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。 +* Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +* Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 +* Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 +* Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。 +* Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。 + +`默认的容错方案是 Failover Cluster。` + +## 配置 + +### Dubbo 配置文件是如何加载到 Spring 中的? + +* Spring 容器在启动的时候,会读取到 Spring 默认的一些 schema 以及 Dubbo 自定义的 schema,每个 schema 都会对应一个自己的 NamespaceHandler,NamespaceHandler 里面通过 BeanDefinitionParser 来解析配置信息并转化为需要加载的 bean 对象! + +### 说说核心的配置有哪些? + +| 标签 | 用途 | 解释 | +| --- | --- | --- | +| | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | +| | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | +| | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | +| | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | +| | 模块配置 | 用于配置当前模块信息,可选 | +| | 注册中心配置 | 用于配置连接注册中心相关信息 | +| | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | +| | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | +| | 消费方配置 | 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选 | +| | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | +| | 参数配置 | 用于指定方法参数配置 | + +`如果是SpringBoot项目就只需要注解,或者开Application配置文件!!!` + +### Dubbo 超时设置有哪些方式? + +**Dubbo 超时设置有两种方式:** + +* 服务提供者端设置超时时间,在Dubbo的用户文档中,推荐如果能在服务端多配置就尽量多配置,因为服务提供者比消费者更清楚自己提供的服务特性。 +* 服务消费者端设置超时时间,如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。因为服务调用方设置超时时间控制性更灵活。如果消费方超时,服务端线程不会定制,会产生警告。 + +### 服务调用超时会怎么样? + +* dubbo 在调用服务不成功时,默认是会重试两次。 + +## 通信协议 + +### Dubbo 使用的是什么通信框架? + +* 默认使用 Netty 作为通讯框架。 + +### Dubbo 支持哪些协议,它们的优缺点有哪些? + +* Dubbo: 单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步 Hessian 序列化。Dubbo推荐使用dubbo协议。 + +* RMI: 采用 JDK 标准的 RMI 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 Java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。 多个短连接 TCP 协议传输,同步传输,适用常规的远程服务调用和 RMI 互操作。在依赖低版本的 Common-Collections 包,Java 序列化存在安全漏洞。 + +* WebService:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用。 + +* HTTP: 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。 + +* Hessian:集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。 + +* Memcache:基于 Memcache实现的 RPC 协议。 + +* Redis:基于 Redis 实现的RPC协议。 + +## 设计模式 + +### Dubbo 用到哪些设计模式? + +`Dubbo 框架在初始化和通信过程中使用了多种设计模式,可灵活控制类加载、权限控制等功能。` + +* **工厂模式** + + Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 + +* 工厂模式 + + Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 + +Dubbo 里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 JDKSPI 的机制。这么实现的优点是可扩展性强,想要扩展实现,只需要在 classpath下增加个文件就可以了,代码零侵入。另外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。 + +* **装饰器模式** + + Dubbo 在启动和调用阶段都大量使用了装饰器模式。以 Provider 提供的调用链为例,具体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是: + +EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter -> +ExecuteLimitFilter -> TraceFilter -> TimeoutFilter -> MonitorFilter -> +ExceptionFilter +复制代码 + +更确切地说,这里是装饰器和责任链模式的混合使用。例如,EchoFilter 的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像ClassLoaderFilter 则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式。 + +* **观察者模式** + + Dubbo 的 Provider 启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时,采用了观察者模式,开启一个 listener。注册中心会每 5 秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个 notify 消息,provider 接受到 notify 消息后,运行 NotifyListener 的 notify 方法,执行监听器方法。 + +* **动态代理模式** + + Dubbo 扩展 JDK SPI 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。Dubbo 需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的 createAdaptiveExtensionClassCode 方法。代理类主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。 + +## 运维管理 + +### 服务上线怎么兼容旧版本? + +* 可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。 + +### Dubbo telnet 命令能做什么? + +* dubbo 服务发布之后,我们可以利用 telnet 命令进行调试、管理。Dubbo2.0.5 以上版本服务提供端口支持 telnet 命令 + +### Dubbo 支持服务降级吗? + +* 以通过 dubbo:reference 中设置 mock=“return null”。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 + +### Dubbo 如何优雅停机? + +* Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果使用kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。 + +## SPI + +### Dubbo SPI 和 Java SPI 区别? + +* JDK SPI: + + JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了 + +* DUBBO SPI: + + 1、对 Dubbo 进行扩展,不需要改动 Dubbo 的源码 + + 2、延迟加载,可以一次只加载自己想要加载的扩展实现。 + + 3、增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。 + + 4、Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。 + +## 其他 + +### Dubbo 支持分布式事务吗? + +* 目前暂时不支持,可与通过 tcc-transaction 框架实现 + +* 介绍:tcc-transaction 是开源的 TCC 补偿性分布式事务框架 + +* TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。 + +### Dubbo 可以对结果进行缓存吗? + +* 为了提高数据访问的速度。Dubbo 提供了声明式缓存,以减少用户加缓存的工作量 +* 其实比普通的配置文件就多了一个标签 cache=“true” + +### Dubbo 必须依赖的包有哪些? + +* Dubbo 必须依赖 JDK,其他为可选。 + +### Dubbo 支持哪些序列化方式? + +* 默认使用 Hessian 序列化,还有 Duddo、FastJson、Java 自带序列化。 + +### Dubbo 在安全方面有哪些措施? + +* Dubbo 通过 Token 令牌防止用户绕过注册中心直连,然后在注册中心上管理授权。 +* Dubbo 还提供服务黑白名单,来控制服务所允许的调用方。 + +### 服务调用是阻塞的吗? + +* 默认是阻塞的,可以异步调用,没有返回值的可以这么做。Dubbo 是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。 + +### 服务提供者能实现失效踢出是什么原理? + +* 服务失效踢出基于 zookeeper 的临时节点原理。 + +### 同一个服务多个注册的情况下可以直连某一个服务吗? + +* 可以点对点直连,修改配置即可,也可以通过 telnet 直接某个服务。 + +### Dubbo 服务降级,失败重试怎么做? + +* 可以通过 dubbo:reference 中设置 mock=“return null”。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 + +### Dubbo 使用过程中都遇到了些什么问题? + +* 在注册中心找不到对应的服务,检查 service 实现类是否添加了@service 注解无法连接到注册中心,检查配置文件中的对应的测试 ip 是否正确 + +## RPC + +### 为什么要有RPC + +* http接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。第三个来说就是安全性。最后就是最近流行的服务化架构、服务化治理,RPC框架是一个强力的支撑。 + +* socket只是一个简单的网络通信方式,只是创建通信双方的通信通道,而要实现rpc的功能,还需要对其进行封装,以实现更多的功能。 + +* RPC一般配合netty框架、spring自定义注解来编写轻量级框架,其实netty内部是封装了socket的,较新的jdk的IO一般是NIO,即非阻塞IO,在高并发网站中,RPC的优势会很明显 + +### 什么是RPC + +* RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。比较关键的一些方面包括:通讯协议、序列化、资源(接口)描述、服务框架、性能、语言支持等。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2ec735fd~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。 + +### PRC架构组件 + +* 一个基本的RPC架构里面应该至少包含以下4个组件: + + 1、客户端(Client):服务调用方(服务消费者) + + 2、客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端 + + 3、服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理4、服务端(Server):服务的真正提供者 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2fb08565~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 具体调用过程: + + 1、服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务; + + 2、客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体; + + 3、客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端; + + 4、服务端存根(server stub)收到消息后进行解码(反序列化操作); + + 5、服务端存根(server stub)根据解码结果调用本地的服务进行相关处理; + + 6、本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub); + + 7、服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方; + + 8、客户端存根(client stub)接收到消息,并进行解码(反序列化); + + 9、服务消费方得到最终结果; + +`而RPC框架的实现目标则是将上面的第2-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。` + +### RPC和SOA、SOAP、REST的区别 + +* 1、REST + + 可以看着是HTTP协议的一种直接应用,默认基于JSON作为传输格式,使用简单,学习成本低效率高,但是安全性较低。 + +* 2、SOAP + + SOAP是一种数据交换协议规范,是一种轻量的、简单的、基于XML的协议的规范。而SOAP可以看着是一个重量级的协议,基于XML、SOAP在安全方面是通过使用XML-Security和XML-Signature两个规范组成了WS-Security来实现安全控制的,当前已经得到了各个厂商的支持 。 + + 它有什么优点?简单总结为:易用、灵活、跨语言、跨平台。 + +* 3、SOA + + 面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用。服务层是SOA的基础,可以直接被应用调用,从而有效控制系统中与软件代理交互的人为依赖性。 + + SOA是一种粗粒度、松耦合服务架构,服务之间通过简单、精确定义接口进行通讯,不涉及底层编程接口和通讯模型。SOA可以看作是B/S模型、XML(标准通用标记语言的子集)/Web Service技术之后的自然延伸。 + +* 4、REST 和 SOAP、RPC 有何区别呢? + + 没什么太大区别,他们的本质都是提供可支持分布式的基础服务,最大的区别在于他们各自的的特点所带来的不同应用场景 。 + +### RPC框架需要解决的问题? + +* 1、如何确定客户端和服务端之间的通信协议? +* 2、如何更高效地进行网络通信? +* 3、服务端提供的服务如何暴露给客户端? +* 4、客户端如何发现这些暴露的服务? +* 5、如何更高效地对请求对象和响应结果进行序列化和反序列化操作? + +### RPC的实现基础? + +* 1、需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架; +* 2、需要有比较高效的序列化框架,比如谷歌的Protobuf序列化框架; +* 3、可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等; +* 4、如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能; + +### RPC使用了哪些关键技术? + +* 1、动态代理 + + 生成Client Stub(客户端存根)和Server Stub(服务端存根)的时候需要用到Java动态代理技术,可以使用JDK提供的原生的动态代理机制,也可以使用开源的:CGLib代理,Javassist字节码生成技术。 + +* 2、序列化和反序列化 + + 在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。 + + * 序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。 目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。 + * 反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。 目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。 +* 3、NIO通信 + + 出于并发性能的考虑,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。可以选择Netty或者MINA来解决NIO数据传输的问题。 + +* 4、服务注册中心 + + 可选:Redis、Zookeeper、Consul 、Etcd。一般使用ZooKeeper提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。 + +### 主流RPC框架有哪些 + +* 1、RMI + + 利用java.rmi包实现,基于Java远程方法协议(Java Remote Method Protocol) 和java的原生序列化。 + +* 2、Hessian + + 是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。 基于HTTP协议,采用二进制编解码。 + +* 3、protobuf-rpc-pro + + 是一个Java类库,提供了基于 Google 的 Protocol Buffers 协议的远程方法调用的框架。基于 Netty 底层的 NIO 技术。支持 TCP 重用/ keep-alive、SSL加密、RPC 调用取消操作、嵌入式日志等功能。 + +* 4、Thrift + + 是一种可伸缩的跨语言服务的软件框架。它拥有功能强大的代码生成引擎,无缝地支持C + +,C#,Java,Python和PHP和Ruby。thrift允许你定义一个描述文件,描述数据类型和服务接口。依据该文件,编译器方便地生成RPC客户端和服务器通信代码。 + + 最初由facebook开发用做系统内个语言之间的RPC通信,2007年由facebook贡献到apache基金 ,现在是apache下的opensource之一 。支持多种语言之间的RPC方式的通信:php语言client可以构造一个对象,调用相应的服务方法来调用java语言的服务,跨越语言的C/S RPC调用。底层通讯基于SOCKET。 + +* 5、Avro + + 出自Hadoop之父Doug Cutting, 在Thrift已经相当流行的情况下推出Avro的目标不仅是提供一套类似Thrift的通讯中间件,更是要建立一个新的,标准性的云计算的数据交换和存储的Protocol。支持HTTP,TCP两种协议。 + +* 6、Dubbo + + Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。 + +### RPC的实现原理架构图 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c300aefca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 + +比如说,A服务器想调用B服务器上的一个方法: + +* 1、建立通信 + + 首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。 + + 主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。 + + 通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。 + +* 2、服务寻址 + + 要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么。 + + 通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。 + + 可靠的寻址方式(主要是提供服务的发现)是RPC的实现基石,比如可以采用Redis或者Zookeeper来注册服务等等。 + + * 2.1、从服务提供者的角度看: + + 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找; + + 当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务; + + 服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。 + + * 2.2、从调用者的角度看: + + 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息; + + 当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者; + + 服务调用者下线的时候,则取消订阅。 + +* 3、网络传输 + + * 3.1、序列化 + + 当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。 + + * 3.2、反序列化 + + 当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用, 通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。 + +* 4、服务调用 + + B机器进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。 + +`通常,经过以上四个步骤之后,一次完整的RPC调用算是完成了,另外可能因为网络抖动等原因需要重试等。` + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127076499463 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" new file mode 100644 index 0000000..d28772b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" @@ -0,0 +1,949 @@ + + +## BIO、NIO、AIO、Netty + +#### 什么是IO + +* Java中I/O是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互。 + +* 在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,**网络上的数据传输流**,字符串流,对象流等等等。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b746125c6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 比如程序从服务器上下载图片,就是通过流的方式从网络上以流的方式到程序中,在到硬盘中 + +#### 在了解不同的IO之前先了解:同步与异步,阻塞与非阻塞的区别 + +* 同步,一个任务的完成之前不能做其他操作,必须等待(等于在打电话) +* 异步,一个任务的完成之前,可以进行其他操作(等于在聊QQ) +* 阻塞,是相对于CPU来说的, 挂起当前线程,不能做其他操作只能等待 +* 非阻塞,,无须挂起当前线程,可以去执行其他操作 + +#### 什么是BIO + +* BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。 + +#### 什么是NIO + +* NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。 + +#### 什么是AIO + +* AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。. + +* AIO属于NIO包中的类实现,其实IO主要分为BIO和NIO,AIO只是附加品,解决IO不能异步的实现 + +* 在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作 + +#### 什么Netty + +* Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 + +* Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b74fff5c8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO + +#### BIO和NIO、AIO的区别 + +* BIO是阻塞的,NIO是非阻塞的. +* BIO是面向流的,只能单向读写,NIO是面向缓冲的, 可以双向读写 +* 使用BIO做Socket连接时,由于单向读写,当没有数据时,会挂起当前线程,阻塞等待,为防止影响其它连接,,需要为每个连接新建线程处理.,然而系统资源是有限的,,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗,因此需要使用NIO进行BIO多路复用,使用一个线程来监听所有Socket连接,使用本线程或者其他线程处理连接 +* AIO是非阻塞 以异步方式发起 I/O 操作。当 I/O 操作进行时可以去做其他操作,由操作系统内核空间提醒IO操作已完成(不懂的可以往下看) + +#### IO流的分类 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b771fca9c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**按照读写的单位大小来分:** + +* `字符流`:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为`char数组,也可以是别的`) +* 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为`byte数组`) + +**按照实际IO操作来分:** + +* 输出流:从内存读出到文件。只能进行写操作。 +* 输入流:从文件读入到内存。只能进行读操作。 +* **注意**:输出流可以帮助我们创建文件,而输入流不会。 + +**按照读写时是否直接与硬盘,内存等节点连接分:** + +* 节点流:直接与数据源相连,读入或读出。 +* 处理流:也叫包装流,是对一个对于已存在的流的连接进行封装,通过所封装的流的功能调用实现数据读写。如添加个Buffering缓冲区。(意思就是有个缓存区,等于软件和mysql中的redis) +* **注意**:为什么要有处理流?主要作用是在读入或写出时,对数据进行缓存,以减少I/O的次数,以便下次更好更快的读写文件,才有了处理流。 + +#### 什么是内核空间 + +* 我们的应用程序是不能直接访问硬盘的,我们程序没有权限直接访问,但是操作系统(Windows、Linux......)会给我们一部分权限较高的内存空间,他叫内核空间,和我们的实际硬盘空间是有区别的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b7790530d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 五种IO模型 + +* **注意:我这里的用户空间就是应用程序空间** + +##### 1.阻塞BIO(blocking I/O) + +* A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。 + +* 在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b82fb2f64~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +##### 2.非阻塞NIO(noblocking I/O) + +* B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。 **B在检查鱼竿是否有鱼,是一个轮询的过程。** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b830f1cd3~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 3.异步AIO(asynchronous I/O) + +* C也想钓鱼,但C有事情,于是他雇来了D、E、F,让他们帮他等待鱼上钩,一旦有鱼上钩,就打电话给C,C就会将鱼钓上去。![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba0e18c2d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)当应用程序请求数据时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。 + +##### 4.信号驱动IO(signal blocking I/O) + +* G也在河边钓鱼,但与A、B、C不同的是,G比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,G就会将鱼钓上来。![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba21e5d95~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。 + +##### 5.IO多路转接(I/O multiplexing) + +* H同样也在河边钓鱼,但是H生活水平比较好,H拿了很多的鱼竿,一次性有很多鱼竿在等,H不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba9450627~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。 +* IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。 + +#### 什么是比特(Bit),什么是字节(Byte),什么是字符(Char),它们长度是多少,各有什么区别 + +* Bit最小的二进制单位 ,是计算机的操作部分取值0或者1 +* Byte是计算机中存储数据的单元,是一个8位的二进制数,(计算机内部,一个字节可表示一个英文字母,两个字节可表示一个汉字。) `取值(-128-127)` +* Char是用户的可读写的最小单位,他只是抽象意义上的一个符号。如‘5’,‘中’,‘¥’ 等等等等。在java里面由16位bit组成Char 取值`(0-65535)` +* Bit 是最小单位 计算机他只能认识0或者1 +* Byte是8个字节 是给计算机看的 +* 字符 是看到的东西 一个字符=二个字节 + +#### 什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作 + +* 对象序列化,将对象以二进制的形式保存在硬盘上 +* 反序列化;将二进制的文件转化为对象读取 +* 实现serializable接口,不想让字段放在硬盘上就加transient + +#### 在实现序列化接口是时候一般要生成一个serialVersionUID字段,它叫做什么,一般有什么用 + +* 如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID +* 但是强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException这个异常。 +* (比如说先进行序列化,然后在反序列化之前修改了类,那么就会报错。因为修改了类,对应的SerialversionUID也变化了,而序列化和反序列化就是通过对比其SerialversionUID来进行的,一旦SerialversionUID不匹配,反序列化就无法成功。 + +#### 怎么生成SerialversionUID + +* 可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段**必须是静态 (static)、最终 (final) 的 long 型字段**)显式声明其自己的 serialVersionUID + +* 两种显示的生成方式(当你一个类实现了Serializable接口,如果没有显示的定义serialVersionUID,Eclipse会提供这个提示功能告诉你去定义 。在Eclipse中点击类中warning的图标一下,Eclipse就会自动给定两种生成的方式。 + +#### BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法 + +* 属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法 + +#### Java中流类的超类主要有那些? + +* 超类代表顶端的父类(都是抽象类) + +* java.io.InputStream + +* java.io.OutputStream + +* java.io.Reader + +* java.io.Writer + +#### 为什么图片、视频、音乐、文件等 都是要字节流来读取 + +* 这个很基础,你看看你电脑文件的属性就好了,CPU规定了计算机存储文件都是按字节算的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2bae24bd8b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### IO的常用类和方法,以及如何使用 + +[注意,如果懂IO的普通文件读写操作可以直接点击此处跳过,直接看网络操作IO编程,那个才是重点,点击即会跳转](#Mark "#Mark") + +前面讲了那么多废话,现在我们开始进入主题,后面很长,从开始的文件操作到后面的**网络IO操作**都会有例子: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2bb4bd288c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)[注意,如果懂IO的普通文件读写操作可以直接点击此处跳过,直接看网络操作IO编程,那个才是重点,点击即会跳转](#Mark "#Mark") +#### IO基本操作讲解 + +* `这里的基本操作就是普通的读取操作,如果想要跟深入的了解不同的IO开发场景必须先了解IO的基本操作` + +##### 1 按`字符`流读取文件 + +###### 1.1 按字符流的·节点流方式读取 + +* 如果我们要取的数据基本单位是字符,那么用(**字符流**)这种方法读取文件就比较适合。比如:读取test.txt文件 + +**注释:** + +* `字符流`:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为`char数组,也可以是别的`) + +* 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为`byte数组`) + +* **FileReader 类:**(字符输入流) 注意:new FileReader("D:\test.txt");//文件必须存在 + +```java +package com.test.io; + +import java.io.FileReader; +import java.io.IOException; + +public class TestFileReader { + public static void main(String[] args) throws IOException { + int num=0; + //字符流接收使用的char数组 + char[] buf=new char[1024]; + //字符流、节点流打开文件类 + FileReader fr = new FileReader("D:\\test.txt");//文件必须存在 + //FileReader.read():取出字符存到buf数组中,如果读取为-1代表为空即结束读取。 + //FileReader.read():读取的是一个字符,但是java虚拟机会自动将char类型数据转换为int数据, + //如果你读取的是字符A,java虚拟机会自动将其转换成97,如果你想看到字符可以在返回的字符数前加(char)强制转换如 + while((num=fr.read(buf))!=-1) { } + //检测一下是否取到相应的数据 + for(int i=0;i 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + out = socket.getOutputStream(); + out.write("hello!".getBytes()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +* TCP协议Socket使用BIO进行通信:客户端(第二执行) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket使用BIO进行通信:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + //基于字节流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + String str = new Scanner(System.in).nextLine(); + byte[] car = str.getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + //关闭连接 + socket.close(); + } +} +``` + +* TCP协议Socket使用BIO进行通信:客户端(第三执行) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client02 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + //基于字节流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + String str = new Scanner(System.in).nextLine(); + byte[] car = str.getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + //关闭连接 + socket.close(); + } +} +``` + +`为了解决堵塞问题,可以使用多线程,请看下面` + +##### 2 多线程解决BIO编程会出现的问题 + +**这时有人就会说,我多线程不就解决了吗?** + +* 使用多线程是可以解决堵塞等待时间很长的问题,因为他可以充分发挥CPU +* 然而系统资源是有限的,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dd68f7c10~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**万一请求越来越多,线程越来越多那我CPU不就炸了?** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d696381b8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**多线程BIO代码示例:** + +* 四个客户端,这次我多复制了俩个一样客户端类![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d7a9bf162~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`先启动服务端,在启动所有客户端,测试`,发现连接成功(`后面有代码`)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d94da3441~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)在所有客户端输入消息(`Client01、Client02这些是我在客户端输入的消息`):发现没有问题![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d94251e4b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**多线程BIO通信代码:** + +* `服务端的代码,客户端的代码还是上面之前的代码` + +```java +package com.test.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; + +//TCP协议Socket使用多线程BIO进行通行:服务端 +public class BIOThreadService { + public static void main(String[] args) { + try { + ServerSocket server = new ServerSocket(8000); + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接... "); + while (true) { + Socket socket = server.accept();//等待客户连接 + System.out.println("客户连接成功,客户信息为:" + socket.getRemoteSocketAddress()); + //针对每个连接创建一个线程, 去处理I0操作 + //创建多线程创建开始 + Thread thread = new Thread(new Runnable() { + public void run() { + try { + InputStream in = socket.getInputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + //读取客户端的数据 + while ((len = in.read(buffer)) > 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + OutputStream out = socket.getOutputStream(); + out.write("hello".getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + thread.start(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +`为了解决线程太多,这时又来了,线程池` + +##### 3 线程池解决多线程BIO编程会出现的问题 + +**这时有人就会说,我TM用线程池?** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dab69e263~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 线程池固然可以解决这个问题,万一需求量还不够还要扩大线程池。当是这是我们自己靠着自己的思想完成的IO操作,Socket 上来了就去创建线程去抢夺CPU资源,MD,线程都TM做IO去了,CPU也不舒服呀 + +* 这时呢:Jdk官方坐不住了,兄弟BIO的问题交给我,我来给你解决:`NIO的诞生` + +**线程池BIO代码示例:** + +* 四个客户端![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2da7389a42~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`先启动服务端,在启动所有客户端,测试`,(`后面有代码`)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc163e25a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)在所有客户端输入消息(`Client01、Client02这些是我在客户端输入的消息`):发现没有问题![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc2080d3a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**线程池BIO通信代码:** + +* `服务端的代码,客户端的代码还是上面的代码` + +```java +package com.test.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +//TCP协议Socket使用线程池BIO进行通行:服务端 +public class BIOThreadPoolService { + public static void main(String[] args) { + //创建线程池 + ExecutorService executorService = Executors.newFixedThreadPool(30); + try { + ServerSocket server = new ServerSocket(8000); + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接..."); + while (true) { + Socket socket = server.accept();//等待客户连接 + System.out.println("客户连接成功,客户信息为:" + socket.getRemoteSocketAddress()); + //使用线程池中的线程去执行每个对应的任务 + executorService.execute(new Thread(new Runnable() { + public void run() { + try { + InputStream in = socket.getInputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + //读取客户端的数据 + while ((len = in.read(buffer)) > 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + OutputStream out = socket.getOutputStream(); + out.write("hello".getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }) + ); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +##### 4 使用NIO实现网络通信 + +* NIO是JDK1.4提供的操作,他的流还是流,没有改变,服务器实现的还是一个连接一个线程,当是:`客户端发送的连接请求都会注册到多路复用器上`,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc3e76709~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`看不懂介绍可以认真看看代码实例,其实不难` +###### 什么是通道(Channel) + +* Channel是一个对象,可以通过它读取和写入数据。 通常我们都是将数据写入包含一个或者多个字节的缓冲区,然后再将缓存区的数据写入到通道中,将数据从通道读入缓冲区,再从缓冲区获取数据。 + +* Channel 类似于原I/O中的流(Stream),但有所区别: + + * 流是单向的,通道是双向的,可读可写。 + * 流读写是阻塞的,通道可以异步读写。 + +###### 什么是选择器(Selector) + +* Selector可以称他为通道的集合,每次客户端来了之后我们会把Channel注册到Selector中并且我们给他一个状态,在用死循环来环判断(`判断是否做完某个操作,完成某个操作后改变不一样的状态`)状态是否发生变化,知道IO操作完成后在退出死循环 + +###### 什么是Buffer(缓冲区) + +* Buffer 是一个缓冲数据的对象, 它包含一些要写入或者刚读出的数据。 + +* 在普通的面向流的 I/O 中,一般将数据直接写入或直接读到 Stream 对象中。当是有了Buffer(缓冲区)后,数据第一步到达的是Buffer(缓冲区)中 + +* 缓冲区实质上是一个数组(`底层完全是数组实现的,感兴趣可以去看一下`)。通常它是一个字节数组,内部维护几个状态变量,可以实现在同一块缓冲区上反复读写(不用清空数据再写)。 + +###### 代码实例: + +* 目录结构![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dd02ee59c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 运行示例,先运行服务端,在运行所有客户端控制台输入消息就好了。:`我这客户端和服务端代码有些修该变,后面有代码`![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2de8321be5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* `服务端示例,先运行,想要搞定NIO请认真看代码示例,真的很清楚` + +```java +package com.test.io; + +import com.lijie.iob.RequestHandler; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +public class NIOServer { + public static void main(String[] args) throws IOException { + //111111111 + //Service端的Channel,监听端口的 + ServerSocketChannel serverChannel = ServerSocketChannel.open(); + //设置为非阻塞 + serverChannel.configureBlocking(false); + //nio的api规定这样赋值端口 + serverChannel.bind(new InetSocketAddress(8000)); + //显示Channel是否已经启动成功,包括绑定在哪个地址上 + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接..."+ serverChannel.getLocalAddress()); + + //22222222 + //声明selector选择器 + Selector selector = Selector.open(); + //这句话的含义,是把selector注册到Channel上面, + //每个客户端来了之后,就把客户端注册到Selector选择器上,默认状态是Accepted + serverChannel.register(selector, SelectionKey.OP_ACCEPT); + + //33333333 + //创建buffer缓冲区,声明大小是1024,底层使用数组来实现的 + ByteBuffer buffer = ByteBuffer.allocate(1024); + RequestHandler requestHandler = new RequestHandler(); + + //444444444 + //轮询,服务端不断轮询,等待客户端的连接 + //如果有客户端轮询上来就取出对应的Channel,没有就一直轮询 + while (true) { + int select = selector.select(); + if (select == 0) { + continue; + } + //有可能有很多,使用Set保存Channel + Set selectionKeys = selector.selectedKeys(); + Iterator iterator = selectionKeys.iterator(); + while (iterator.hasNext()) { + //使用SelectionKey来获取连接了客户端和服务端的Channel + SelectionKey key = iterator.next(); + //判断SelectionKey中的Channel状态如何,如果是OP_ACCEPT就进入 + if (key.isAcceptable()) { + //从判断SelectionKey中取出Channel + ServerSocketChannel channel = (ServerSocketChannel) key.channel(); + //拿到对应客户端的Channel + SocketChannel clientChannel = channel.accept(); + //把客户端的Channel打印出来 + System.out.println("客户端通道信息打印:" + clientChannel.getRemoteAddress()); + //设置客户端的Channel设置为非阻塞 + clientChannel.configureBlocking(false); + //操作完了改变SelectionKey中的Channel的状态OP_READ + clientChannel.register(selector, SelectionKey.OP_READ); + } + //到此轮训到的时候,发现状态是read,开始进行数据交互 + if (key.isReadable()) { + //以buffer作为数据桥梁 + SocketChannel channel = (SocketChannel) key.channel(); + //数据要想读要先写,必须先读取到buffer里面进行操作 + channel.read(buffer); + //进行读取 + String request = new String(buffer.array()).trim(); + buffer.clear(); + //进行打印buffer中的数据 + System.out.println(String.format("客户端发来的消息: %s : %s", channel.getRemoteAddress(), request)); + //要返回数据的话也要先返回buffer里面进行返回 + String response = requestHandler.handle(request); + //然后返回出去 + channel.write(ByteBuffer.wrap(response.getBytes())); + } + iterator.remove(); + } + } + } +} +``` + +* 客户端示例:(`我这用的不是之前的了,有修改`)运行起来客户端控制台输入消息就好了。 `要模拟测试,请复制粘贴改一下,修改客户端的类名就行了,四个客户端代码一样的`,![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dee3a5661~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + while(true){ + byte[] car = new Scanner(System.in).nextLine().getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + } + } +} +``` + +##### 5 使用Netty实现网络通信 + +* Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 + +* Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的Socket服务开发。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e0284e9ca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO +* Netty的原里就是NIO,他是基于NIO的一个完美的封装,并且优化了NIO,使用他非常方便,简单快捷 + +* 我直接上代码: + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e0e2ef4ca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 1、先添加依赖: + + + io.netty + netty-all + 4.1.16.Final + +``` + +* 2、NettyServer 模板,看起来代码那么多,`其实只需要添加一行消息就好了` +* `请认真看中间的代码` + +```java +package com.lijie.iob; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.serialization.ClassResolvers; +import io.netty.handler.codec.serialization.ObjectEncoder; +import io.netty.handler.codec.string.StringDecoder; + +public class NettyServer { + public static void main(String[] args) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addLast(new StringDecoder()); + pipeline.addLast("encoder", new ObjectEncoder()); + pipeline.addLast(" decoder", new io.netty.handler.codec.serialization.ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null))); + + //重点,其他的都是复用的 + //这是真正的I0的业务代码,把他封装成一个个的个Hand1e类就行了 + //把他当成 SpringMVC的Controller + pipeline.addLast(new NettyServerHandler()); + + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + ChannelFuture f = b.bind(8000).sync(); + System.out.println("服务端启动成功,端口号为:" + 8000); + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } +} +``` + +* 3、需要做的IO操作,重点是继承ChannelInboundHandlerAdapter类就好了 + +```java +package com.lijie.iob; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +public class NettyServerHandler extends ChannelInboundHandlerAdapter { + RequestHandler requestHandler = new RequestHandler(); + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + Channel channel = ctx.channel(); + System.out.println(String.format("客户端信息: %s", channel.remoteAddress())); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = ctx.channel(); + String request = (String) msg; + System.out.println(String.format("客户端发送的消息 %s : %s", channel.remoteAddress(), request)); + String response = requestHandler.handle(request); + ctx.write(response); + ctx.flush(); + } +} +``` + +* 4 客户的代码还是之前NIO的代码,我在复制下来一下吧 + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + while(true){ + byte[] car = new Scanner(System.in).nextLine().getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + } + } +} +``` + +* 运行测试,还是之前那样,启动服务端,在启动所有客户端控制台输入就好了:![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e4e740749~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125700784136 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" new file mode 100644 index 0000000..e4df817 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" @@ -0,0 +1,1313 @@ + + +## Java概述 + +### 何为编程 + +* 编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。 + +* 为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机,使得计算机能够根据人的指令一步一步去工作,完成某种特定的任务。这种人和计算机之间交流的过程就是编程。 + +### 什么是Java + +* Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。 + +### jdk1.5之后的三大版本 + +* Java SE(J2SE,Java 2 Platform Standard Edition,标准版) + Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。 +* Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版) + Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEE +* Java ME(J2ME,Java 2 Platform Micro Edition,微型版) + Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。 + +### 3 Jdk和Jre和JVM的区别 + +`看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM` + +* JDK :Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具 Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等 + +* JRE :Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包 + + 如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。 + +* Jvm:在倒数第二层 由他可以在(最后一层的)各种平台上运行 Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c434318a82~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 什么是跨平台性?原理是什么 + +* 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 + +* 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 + +### Java语言有哪些特点 + +* 简单易学(Java语言的语法与C语言和C++语言很接近) + +* 面向对象(封装,继承,多态) + +* 平台无关性(Java虚拟机实现平台无关性) + +* 支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的) + +* 支持多线程(多线程机制使应用程序在同一时间并行执行多项任) + +* 健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等) + +* 安全性好 + +### 什么是字节码?采用字节码的最大好处是什么 + +* **字节码**:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。 + +* **采用字节码的好处**: + + Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。 + +* **先看下java中的编译器和解释器**: + + Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。 + + Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。 + +### 什么是Java程序的主类?应用程序和小程序的主类有何不同? + +* 一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。 + +### Java应用程序与小程序之间有那些差别? + +* 简单说应用程序是从主线程启动(也就是main()方法)。applet小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。 + +### Java和C++的区别 + +`我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!` + +* 都是面向对象的语言,都支持封装、继承和多态 +* Java不提供指针来直接访问内存,程序内存更加安全 +* Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。 +* Java有自动内存管理机制,不需要程序员手动释放无用内存 + +### Oracle JDK 和 OpenJDK 的对比 + +1. Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次; + +2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的; + +3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题; + +4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能; + +5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; + +6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 + +## 基础语法 + +### 数据类型 + +#### Java有哪些数据类型 + +**定义**:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。 + +**分类** + +* 基本数据类型 + * 数值型 + * 整数类型(byte,short,int,long) + * 浮点类型(float,double) + * 字符型(char) + * 布尔型(boolean) +* 引用数据类型 + * 类(class) + * 接口(interface) + * 数组([]) + +**Java基本数据类型图** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c434465b69~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上 + +* 在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。 + +#### 用最有效率的方法计算 2 乘以 8 + +* 2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。 + +#### Math.round(11.5) 等于多少?Math.round(-11.5)等于多少 + +* Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。 + +#### float f=3.4;是否正确 + +* 不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。 + +#### short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗 + +* 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。 + +* 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。 + +### 编码 + +#### Java语言采用何种编码方案?有何特点? + +* Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。 + +### 注释 + +#### 什么Java注释 + +**定义**:用于解释说明程序的文字 + +**分类** + +* 单行注释 + 格式: // 注释文字 +* 多行注释 + 格式: /* 注释文字 */ +* 文档注释 + 格式:/** 注释文字 */ + +**作用** + +* 在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。 + +`注意事项:多行和文档注释都不能嵌套使用。` + +### 访问修饰符 + +#### 访问修饰符 public,private,protected,以及不写(默认)时的区别 + +* **定义**:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。 + +* **分类** + + * private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类) + + * default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。 + + * protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。 + + * public : 对所有类可见。使用对象:类、接口、变量、方法 + +**访问修饰符图** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c433bcfd38~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 运算符 + +#### &和&&的区别 + +* &运算符有两种用法:(1)按位与;(2)逻辑与。 + +* &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 + +`注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。` + +### 关键字 + +#### Java 有没有 goto + +* goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。 + +#### final 有什么用? + +`用于修饰类、属性和方法;` + +* 被final修饰的类不可以被继承 +* 被final修饰的方法不可以被重写 +* 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的 + +#### final finally finalize区别 + +* final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表 示该变量是一个常量不能被重新赋值。 +* finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 +* finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的 最后判断。 + +#### this关键字的用法 + +* this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。 + +* this的用法在java中大体可以分为3种: + + * 1.普通的直接引用,this相当于是指向当前对象本身。 + + * 2.形参与成员名字重名,用this来区分: + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + 复制代码 + * 3.引用本类的构造函数 + + class Person{ + private String name; + private int age; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + public Person(String name, int age) { + this(name); + this.age = age; + } + } + 复制代码 + +#### super关键字的用法 + +* super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 + +* super也有三种用法: + + * 1.普通的直接引用 + + 与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。 + + * 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分 + + class Person{ + protected String name; + + public Person(String name) { + this.name = name; + } + + } + + class Student extends Person{ + private String name; + + public Student(String name, String name1) { + super(name); + this.name = name1; + } + + public void getInfo(){ + System.out.println(this.name); //Child + System.out.println(super.name); //Father + } + + } + + public class Test { + public static void main(String[] args) { + Student s1 = new Student("Father","Child"); + s1.getInfo(); + + } + } + 复制代码 + * 3.引用父类构造函数 + + * super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。 + * this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 + +#### this与super的区别 + +* super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) +* this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名) +* super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。 +* super()和this()均需放在构造方法内第一行。 +* 尽管可以用this调用一个构造器,但却不能调用两个。 +* this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。 +* this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。 +* 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。 + +#### static存在的主要意义 + +* static的主要意义是在于创建独立于具体对象的域变量或者方法。**以致于即使没有创建对象,也能使用属性和调用方法**! + +* static关键字还有一个比较关键的作用就是 **用来形成静态代码块以优化程序性能**。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 + +* 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。 + +#### static的独特之处 + +* 1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法**不属于任何一个实例对象,而是被类的实例对象所共享**。 + +> 怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩? + +* 2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。 + +* 3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的! + +* 4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。 + +#### static应用场景 + +* 因为static是被类的实例对象所共享,因此如果**某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量**。 + +* 因此比较常见的static应用场景有: + +> 1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包 + +#### static注意事项 + +* 1、静态只能访问静态。 +* 2、非静态既可以访问非静态的,也可以访问静态的。 + +### 流程控制语句 + +#### break ,continue ,return 的区别及作用 + +* break 跳出总上一层循环,不再执行循环(结束当前的循环体) + +* continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) + +* return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) + +#### 在 Java 中,如何跳出当前的多重嵌套循环 + +* 在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如: + + public static void main(String[] args) { + ok: + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + System.out.println("i=" + i + ",j=" + j); + if (j == 5) { + break ok; + } + } + } + } + 复制代码 + +## 面向对象 + +### 面向对象概述 + +#### 面向对象和面向过程的区别 + +* **面向过程**: + + * 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 + + * 缺点:没有面向对象易维护、易复用、易扩展 + +* **面向对象**: + + * 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 + + * 缺点:性能比面向过程低 + +`面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。` + +`面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。` + +`面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。` + +### 面向对象三大特性 + +#### 面向对象的特征有哪些方面 + +**面向对象的特征主要有以下几个方面**: + +* **抽象**:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。 + +* **封装**把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 + +* **继承**是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 + + * 关于继承如下 3 点请记住: + + * 子类拥有父类非 private 的属性和方法。 + + * 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 + + * 子类可以用自己的方式实现父类的方法。(以后介绍)。 + +* **多态**:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 + +#### 什么是多态机制?Java语言是如何实现多态的? + +* 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。 + +* 多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。 + +**多态的实现** + +* Java实现多态有三个必要条件:继承、重写、向上转型。 + + * 继承:在多态中必须存在有继承关系的子类和父类。 + + * 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 + + * 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。 + +`只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。` + +`对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。` + +#### 面向对象五大基本原则是什么(可选) + +* 单一职责原则SRP(Single Responsibility Principle) + 类的功能要单一,不能包罗万象,跟杂货铺似的。 +* 开放封闭原则OCP(Open-Close Principle) + 一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。 +* 里式替换原则LSP(the Liskov Substitution Principle LSP) + 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~ +* 依赖倒置原则DIP(the Dependency Inversion Principle DIP) + 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。 +* 接口分离原则ISP(the Interface Segregation Principle ISP) + 设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。 + +### 类与接口 + +#### 抽象类和接口的对比 + +* 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。 + +* 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 + +**相同点** + +* 接口和抽象类都不能实例化 +* 都位于继承的顶端,用于被其他实现或继承 +* 都包含抽象方法,其子类都必须覆写这些抽象方法 + +**不同点** + +| 参数 | 抽象类 | 接口 | +| --- | --- | --- | +| 声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 | +| 实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 | +| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | +| 访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected | +| 多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 | +| 字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 | + +**备注**:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 + +`现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。` + +* 接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则: + * 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。 + * 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。 + +#### 普通类和抽象类有哪些区别? + +* 普通类不能包含抽象方法,抽象类可以包含抽象方法。 +* 抽象类不能直接实例化,普通类可以直接实例化。 + +#### 抽象类能使用 final 修饰吗? + +* 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类 + +#### 创建一个对象用什么关键字?对象实例与对象引用有何不同? + +* new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球) + +### 变量与方法 + +#### 成员变量与局部变量的区别有哪些 + +* 变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域 + +* 成员变量:方法外部,类内部定义的变量 + +* 局部变量:类的方法中的变量。 + +* 成员变量和局部变量的区别 + +**作用域** + +* 成员变量:针对整个类有效。 +* 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内) + +**存储位置** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。 +* 局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。 + +**生命周期** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失 +* 局部变量:当方法调用完,或者语句结束后,就自动释放。 + +**初始值** + +* 成员变量:有默认初始值。 +* 局部变量:没有默认初始值,使用前必须赋值。 + +#### 在Java中定义一个不做事且没有参数的构造方法的作用 + +* Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 + +#### 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? + +* 帮助子类做初始化工作。 + +#### 一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么? + +* 主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 + +#### 构造方法有哪些特性? + +* 名字与类名相同; + +* 没有返回值,但不能用void声明构造函数; + +* 生成类的对象时自动执行,无需调用。 + +#### 静态变量和实例变量区别 + +* 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。 + +* 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。 + +#### 静态变量与普通变量区别 + +* static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 + +* 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。 + +#### 静态方法和实例方法有何不同? + +`静态方法和实例方法的区别主要体现在两个方面:` + +* 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 + +* 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 + +#### 在一个静态方法内调用一个非静态成员为什么是非法的? + +* 由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 + +#### 什么是方法的返回值?返回值的作用是什么? + +* 方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! + +### 内部类 + +#### 什么是内部类? + +* 在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是**内部类**。内部类本身就是类的一个属性,与其他属性定义方式一致。 + +#### 内部类的分类有哪些 + +`内部类可以分为四种:**成员内部类、局部内部类、匿名内部类和静态内部类**。` + +##### 静态内部类 + +* 定义在类内部的静态类,就是静态内部类。 + + public class Outer { + + private static int radius = 1; + + static class StaticInner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + } + } + } + 复制代码 +* 静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,`new 外部类.静态内部类()`,如下: + + Outer.StaticInner inner = new Outer.StaticInner(); + inner.visit(); + 复制代码 + +##### 成员内部类 + +* 定义在类内部,成员位置上的非静态类,就是成员内部类。 + + public class Outer { + + private static int radius = 1; + private int count =2; + + class Inner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + System.out.println("visit outer variable:" + count); + } + } + } + 复制代码 +* 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式`外部类实例.new 内部类()`,如下: + + Outer outer = new Outer(); + Outer.Inner inner = outer.new Inner(); + inner.visit(); + 复制代码 + +##### 局部内部类 + +* 定义在方法中的内部类,就是局部内部类。 + + public class Outer { + + private int out_a = 1; + private static int STATIC_b = 2; + + public void testFunctionClass(){ + int inner_c =3; + class Inner { + private void fun(){ + System.out.println(out_a); + System.out.println(STATIC_b); + System.out.println(inner_c); + } + } + Inner inner = new Inner(); + inner.fun(); + } + public static void testStaticFunctionClass(){ + int d =3; + class Inner { + private void fun(){ + // System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量 + System.out.println(STATIC_b); + System.out.println(d); + } + } + Inner inner = new Inner(); + inner.fun(); + } + } + 复制代码 +* 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,`new 内部类()`,如下: + + public static void testStaticFunctionClass(){ + class Inner { + } + Inner inner = new Inner(); + } + 复制代码 + +##### 匿名内部类 + +* 匿名内部类就是没有名字的内部类,日常开发中使用的比较多。 + + public class Outer { + + private void test(final int i) { + new Service() { + public void method() { + for (int j = 0; j < i; j++) { + System.out.println("匿名内部类" ); + } + } + }.method(); + } + } + //匿名内部类必须继承或实现一个已有的接口 + interface Service{ + void method(); + } + 复制代码 +* 除了没有名字,匿名内部类还有以下特点: + + * 匿名内部类必须继承一个抽象类或者实现一个接口。 + * 匿名内部类不能定义任何静态成员和静态方法。 + * 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 + * 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。 +* 匿名内部类创建方式: + + new 类/接口{ + //匿名内部类实现部分 + } + 复制代码 + +#### 内部类的优点 + +`我们为什么要使用内部类呢?因为它有以下优点:` + +* 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据! +* 内部类不为同一包的其他类所见,具有很好的封装性; +* 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。 +* 匿名内部类可以很方便的定义回调。 + +#### 内部类有哪些应用场景 + +1. 一些多算法场合 +2. 解决一些非面向对象的语句块。 +3. 适当使用内部类,使得代码更加灵活和富有扩展性。 +4. 当某个类除了它的外部类,不再被其他的类使用时。 + +#### 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final? + +* 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码: + + public class Outer { + + void outMethod(){ + final int a =10; + class Inner { + void innerMethod(){ + System.out.println(a); + } + } + } + } + 复制代码 +* 以上例子,为什么要加final呢?是因为**生命周期不一致**, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。 + +#### 内部类相关,看程序说出运行结果 + +public class Outer { + private int age = 12; + + class Inner { + private int age = 13; + public void print() { + int age = 14; + System.out.println("局部变量:" + age); + System.out.println("内部类变量:" + this.age); + System.out.println("外部类变量:" + Outer.this.age); + } + } + + public static void main(String[] args) { + Outer.Inner in = new Outer().new Inner(); + in.print(); + } + +} +复制代码 + +运行结果: + +局部变量:14 +内部类变量:13 +外部类变量:12 +复制代码 +### 重写与重载 + +#### 构造器(constructor)是否可被重写(override) + +* 构造器不能被继承,因此不能被重写,但可以被重载。 + +#### 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分? + +* 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 + +* 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分 + +* 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。 + +### 对象相等判断 + +#### == 和 equals 的区别是什么 + +* **==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址) + +* **equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: + + * 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 + + * 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 + + * **举个例子:** + + public class test1 { + public static void main(String[] args) { + String a = new String("ab"); // a 为一个引用 + String b = new String("ab"); // b为另一个引用,对象的内容一样 + String aa = "ab"; // 放在常量池中 + String bb = "ab"; // 从常量池中查找 + if (aa == bb) // true + System.out.println("aa==bb"); + if (a == b) // false,非同一对象 + System.out.println("a==b"); + if (a.equals(b)) // true + System.out.println("aEQb"); + if (42 == 42.0) { // true + System.out.println("true"); + } + } + } + 复制代码 +* **说明:** + + * String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。 + * 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。 + +#### hashCode 与 equals (重要) + +* HashSet如何检查重复 + +* 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗? + +* hashCode和equals方法的关系 + +* 面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?” + +**hashCode()介绍** + +* hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。 + +* 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) + +**为什么要有 hashCode** + +`我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:` + +* 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 + +**hashCode()与equals()的相关规定** + +* 如果两个对象相等,则hashcode一定也是相同的 + +* 两个对象相等,对两个对象分别调用equals方法都返回true + +* 两个对象有相同的hashcode值,它们也不一定是相等的 + +`因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖` + +`hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)` + +#### 对象的相等与指向他们的引用相等,两者有什么不同? + +* 对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。 + +### 值传递 + +#### 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递 + +* 是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的 + +#### 为什么 Java 中只有值传递 + +* 首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。 + +* **Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。** + + * **下面通过 3 个例子来给大家说明** + +##### example 1 + +public static void main(String[] args) { + int num1 = 10; + int num2 = 20; + + swap(num1, num2); + + System.out.println("num1 = " + num1); + System.out.println("num2 = " + num2); +} + +public static void swap(int a, int b) { + int temp = a; + a = b; + b = temp; + + System.out.println("a = " + a); + System.out.println("b = " + b); +} +复制代码 + +* 结果: + + a = 20 b = 10 num1 = 10 num2 = 20 + +* 解析: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c436af3af1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 + +`通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example.` + +##### example 2 + + public static void main(String[] args) { + int[] arr = { 1, 2, 3, 4, 5 }; + System.out.println(arr[0]); + change(arr); + System.out.println(arr[0]); + } + + public static void change(int[] array) { + // 将数组的第一个元素变为0 + array[0] = 0; + } +复制代码 + +* 结果: + + 1 0 + +* 解析: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c4372f6ac8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 + +`通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。` + +`很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。` + +##### example 3 + +public class Test { + + public static void main(String[] args) { + // TODO Auto-generated method stub + Student s1 = new Student("小张"); + Student s2 = new Student("小李"); + Test.swap(s1, s2); + System.out.println("s1:" + s1.getName()); + System.out.println("s2:" + s2.getName()); + } + + public static void swap(Student x, Student y) { + Student temp = x; + x = y; + y = temp; + System.out.println("x:" + x.getName()); + System.out.println("y:" + y.getName()); + } +} +复制代码 + +* 结果: + + x:小李 y:小张 s1:小张 s2:小李 + +* 解析: + +* 交换之前: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c445af6270~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 交换之后: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c45facc688~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 通过上面两张图可以很清晰的看出:`方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝` + +* 总结 + + * `Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。` +* 下面再总结一下Java中方法参数的使用情况: + + * 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型》 + * 一个方法可以改变一个对象参数的状态。 + * 一个方法不能让对象参数引用一个新的对象。 + +#### 值传递和引用传递有什么区别 + +* 值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。 + +* 引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。 + +### Java包 + +#### JDK 中常用的包有哪些 + +* java.lang:这个是系统的基础类; +* java.io:这里面是所有输入输出有关的类,比如文件操作等; +* java.nio:为了完善 io 包中的功能,提高 io 包中性能而写的一个新包; +* java.net:这里面是与网络有关的类; +* java.util:这个是系统辅助类,特别是集合类; +* java.sql:这个是数据库操作的类。 + +#### import java和javax有什么区别 + +* 刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。 + +`所以,实际上java和javax没有区别。这都是一个名字。` + +## IO流 + +### java 中 IO 流分为几种? + +* 按照流的流向分,可以分为输入流和输出流; +* 按照操作单元划分,可以划分为字节流和字符流; +* 按照流的角色划分为节点流和处理流。 + +`Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。` + +* InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +* OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +`按操作方式分类结构图:` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c4799a7a74~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +`按操作对象分类结构图:` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c479a04121~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### BIO,NIO,AIO 有什么区别? + +* 简答 + + * BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 + * NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。 + * AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。 +* 详细回答 + + * **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 + * **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 + * **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 + +### Files的常用方法都有哪些? + +* Files. exists():检测文件路径是否存在。 +* Files. createFile():创建文件。 +* Files. createDirectory():创建文件夹。 +* Files. delete():删除一个文件或目录。 +* Files. copy():复制文件。 +* Files. move():移动文件。 +* Files. size():查看文件个数。 +* Files. read():读取文件。 +* Files. write():写入文件。 + +## 反射 + +### 什么是反射机制? + +* JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 + +* 静态编译和动态编译 + + * 静态编译:在编译时确定类型,绑定对象 + + * 动态编译:运行时确定类型,绑定对象 + +### 反射机制优缺点 + +* **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 +* **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。 + +### 反射机制的应用场景有哪些? + +* 反射是框架设计的灵魂。 + +* 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 + +* 举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性 + +### Java获取反射的三种方法 + +1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制 + +public class Student { + private int id; + String name; + protected boolean sex; + public float score; +} + +public class Get { + //获取反射机制三种方式 + public static void main(String[] args) throws ClassNotFoundException { + //方式一(通过建立对象) + Student stu = new Student(); + Class classobj1 = stu.getClass(); + System.out.println(classobj1.getName()); + //方式二(所在通过路径-相对路径) + Class classobj2 = Class.forName("fanshe.Student"); + System.out.println(classobj2.getName()); + //方式三(通过类名) + Class classobj3 = Student.class; + System.out.println(classobj3.getName()); + } +} +复制代码 +## 常用API + +### String相关 + +#### 字符型常量和字符串常量的区别 + +1. 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符 +2. 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置) +3. 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志) + +#### 什么是字符串常量池? + +* 字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。 + +#### String 是最基本的数据类型吗 + +* 不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。 + +`这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char\[\] chars = {‘你’,‘好’};` + +`但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。` + +#### String有哪些特性 + +* 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。 + +* 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。 + +* final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。 + +#### String为什么是不可变的吗? + +* 简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以: + + /** The value is used for character storage. */ private final char value[]; + +#### String真的是不可变的吗? + +* 我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子: + +**1 String不可变但不代表引用不可以变** + +String str = "Hello"; +str = str + " World"; +System.out.println("str=" + str); +复制代码 + +* 结果: + + str=Hello World + +* 解析: + +* 实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。 + +**2.通过反射是可以修改所谓的“不可变”对象** + +// 创建字符串"Hello World", 并赋给引用s +String s = "Hello World"; + +System.out.println("s = " + s); // Hello World + +// 获取String类中的value字段 +Field valueFieldOfString = String.class.getDeclaredField("value"); + +// 改变value属性的访问权限 +valueFieldOfString.setAccessible(true); + +// 获取s对象上的value属性的值 +char[] value = (char[]) valueFieldOfString.get(s); + +// 改变value所引用的数组中的第5个字符 +value[5] = '_'; + +System.out.println("s = " + s); // Hello_World +复制代码 + +* 结果: + + s = Hello World s = Hello_World + +* 解析: + +* 用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。 + +#### 是否可以继承 String 类 + +* String 类是 final 类,不可以被继承。 + +#### String str="i"与 String str=new String(“i”)一样吗? + +* 不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。 + +#### String s = new String(“xyz”);创建了几个字符串对象 + +* 两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。 + + String str1 = "hello"; //str1指向静态区 String str2 = new String("hello"); //str2指向堆上的对象 String str3 = "hello"; String str4 = new String("hello"); System.out.println(str1.equals(str2)); //true System.out.println(str2.equals(str4)); //true System.out.println(str1 == str3); //true System.out.println(str1 == str2); //false System.out.println(str2 == str4); //false System.out.println(str2 == "hello"); //false str2 = str1; System.out.println(str2 == "hello"); //true + +#### 如何将字符串反转? + +* 使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。 + +* 示例代码: + + // StringBuffer reverse StringBuffer stringBuffer = new StringBuffer(); stringBuffer. append("abcdefg"); System. out. println(stringBuffer. reverse()); // gfedcba // StringBuilder reverse StringBuilder stringBuilder = new StringBuilder(); stringBuilder. append("abcdefg"); System. out. println(stringBuilder. reverse()); // gfedcba + +#### 数组有没有 length()方法?String 有没有 length()方法 + +* 数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。 + +#### String 类的常用方法都有那些? + +* indexOf():返回指定字符的索引。 +* charAt():返回指定索引处的字符。 +* replace():字符串替换。 +* trim():去除字符串两端空白。 +* split():分割字符串,返回一个分割后的字符串数组。 +* getBytes():返回字符串的 byte 类型数组。 +* length():返回字符串长度。 +* toLowerCase():将字符串转成小写字母。 +* toUpperCase():将字符串转成大写字符。 +* substring():截取字符串。 +* equals():字符串比较。 + +#### 在使用 HashMap 的时候,用 String 做 key 有什么好处? + +* HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。 + +#### String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的 + +**可变性** + +* String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。 + +**线程安全性** + +* String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。 + +**性能** + +* 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +**对于三者使用的总结** + +* 如果要操作少量的数据用 = String + +* 单线程操作字符串缓冲区 下操作大量数据 = StringBuilder + +* 多线程操作字符串缓冲区 下操作大量数据 = StringBuffer + +### Date相关 + +### 包装类相关 + +#### 自动装箱与拆箱 + +* **装箱**:将基本类型用它们对应的引用类型包装起来; + +* **拆箱**:将包装类型转换为基本数据类型; + +#### int 和 Integer 有什么区别 + +* Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。 + +* Java 为每个原始类型提供了包装类型: + + * 原始类型: boolean,char,byte,short,int,long,float,double + + * 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double + +#### Integer a= 127 与 Integer b = 127相等吗 + +* 对于对象引用类型:==比较的是对象的内存地址。 +* 对于基本数据类型:==比较的是值。 + +`如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false` + +public static void main(String[] args) { + Integer a = new Integer(3); + Integer b = 3; // 将3自动装箱成Integer类型 + int c = 3; + System.out.println(a == b); // false 两个引用没有引用同一对象 + System.out.println(a == c); // true a自动拆箱成int类型再和c比较 + System.out.println(b == c); // true + + Integer a1 = 128; + Integer b1 = 128; + System.out.println(a1 == b1); // false + + Integer a2 = 127; + Integer b2 = 127; + System.out.println(a2 == b2); // true +} + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127059738631 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" new file mode 100644 index 0000000..50d9a6a --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" @@ -0,0 +1,1478 @@ +https://juejin.cn/post/6844904119338024974#heading-17 + +## 基础知识 + +#### 为什么要使用并发编程 + +* 提升多核CPU的利用率:一般来说一台主机上的会有多个CPU核心,我们可以创建多个线程,理论上讲操作系统可以将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了CPU的使用效率,如果使用单线程就只能有一个CPU核心被使用。 + +* 比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。 + +* 简单来说就是: + + * 充分利用多核CPU的计算能力; + * 方便进行业务拆分,提升应用性能 + +#### 多线程应用场景 + +* 例如: 迅雷多线程下载、数据库连接池、分批发送短信等。 + +#### 并发编程有什么缺点 + +* 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。 + +#### 并发编程三个必要因素是什么? + +* 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。 +* 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile) +* 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序) + +#### 在 Java 程序中怎么保证多线程的运行安全? + +* 出现线程安全问题的原因一般都是三个原因: + + * 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)。 + + * 缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题 + + * 编译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题 + +#### 并行和并发有什么区别? + +* 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。 +* 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 +* 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。 + +**做一个形象的比喻:** + +* 并发 = 俩个人用一台电脑。 + +* 并行 = 俩个人分配了俩台电脑。 + +* 串行 = 俩个人排队使用一台电脑。 + +#### 什么是多线程 + +* 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。 + +#### 多线程的好处 + +* 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。 + +#### 多线程的劣势: + +* 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; + +* 多线程需要协调和管理,所以需要 CPU 时间跟踪线程; + +* 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。 + +#### 线程和进程区别 + +* 什么是线程和进程? + + * 进程 + + 一个在内存中运行的应用程序。 每个正在系统上运行的程序都是一个进程 + + * 线程 + + 进程中的一个执行任务(控制单元), 它负责在程序里独立执行。 + +`一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。` + +* 进程与线程的区别 + + * 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 + + * 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 + + * 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 + + * 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的 + + * 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有可能导致整个进程都死掉。所以多进程要比多线程健壮。 + + * 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 + +#### 什么是上下文切换? + +* 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 + +* 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。 + +* 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 + +* Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + +#### 守护线程和用户线程有什么区别呢? + +* 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 +* 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作 + +#### 如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高? + +* windows上面用任务管理器看,linux下可以用 top 这个工具看。 + * 找出cpu耗用厉害的进程pid, 终端执行top命令,然后按下shift+p (shift+m是找出消耗内存最高)查找出cpu利用最厉害的pid号 + * 根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查找出cpu利用率最厉害的线程号,比如top -H -p 1328 + * 将获取到的线程号转换成16进制,去百度转换一下就行 + * 使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat,比如jstack 31365 > /tmp/t.dat + * 编辑/tmp/t.dat文件,查找线程号对应的信息 + +`或者直接使用JDK自带的工具查看“jconsole” 、“visualVm”,这都是JDK自带的,可以直接在JDK的bin目录下找到直接使用` + +#### 什么是线程死锁 + +* 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。 +* 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 +* 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab440e1912~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 形成死锁的四个必要条件是什么 + +* 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。 +* 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。 +* 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 +* 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A) + +#### 如何避免线程死锁 + +1. 避免一个线程同时获得多个锁 +2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源 +3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制 + +#### 创建线程的四种方式 + +* 继承 Thread 类; + +```java +public class MyThread extends Thread { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); + } +``` +* 实现 Runnable 接口; + +```java +public class MyRunnable implements Runnable { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); + } +``` +* 实现 Callable 接口; + +```java +public class MyCallable implements Callable { + @Override + public Integer call() { + System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); + return 1; + } +``` +* 使用匿名内部类方式 + +```java +public class CreateRunnable { + public static void main(String[] args) { + //创建多线程创建开始 + Thread thread = new Thread(new Runnable() { + public void run() { + for (int i = 0; i < 10; i++) { + System.out.println("i:" + i); + } + } + }); + thread.start(); + } + } +``` + +#### 说一下 runnable 和 callable 有什么区别 + +**相同点:** + +* 都是接口 +* 都可以编写多线程程序 +* 都采用Thread.start()启动线程 + +**主要区别:** + +* Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果 +* Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。 + +#### 线程的 run()和 start()有什么区别? + +* 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。 + +* start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 + +* start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。 + +* run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。 + +#### 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? + +这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! + +* new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到`时间片`后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 + +* 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 + +总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。 + +#### 什么是 Callable 和 Future? + +* Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 + +* Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。 + +#### 什么是 FutureTask + +* FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。 + +#### 线程的状态 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab4672a149~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 新建(new):新创建了一个线程对象。 + +* 就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。 + +* 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; + +* 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。 + + * 阻塞的情况分三种: + * (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态; + * (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态; + * (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。 +* 死亡(dead)(结束):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。 + +#### Java 中用到的线程调度算法是什么? + +* 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度) + +* 有两种调度模型:分时调度模型和抢占式调度模型。 + + * 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。 + + * Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。 + +#### 线程的调度策略 + +`线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:` + +* (1)线程体中调用了 yield 方法让出了对 cpu 的占用权利 + +* (2)线程体中调用了 sleep 方法使线程进入睡眠状态 + +* (3)线程由于 IO 操作受到阻塞 + +* (4)另外一个更高优先级线程出现 + +* (5)在支持时间片的系统中,该线程的时间片用完 + +#### 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )? + +* 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 + +* 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。 + +* 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。 + +#### 请说出与线程同步以及线程调度相关的方法。 + +* (1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; + +* (2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常; + +* (3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关; + +* (4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; + +#### sleep() 和 wait() 有什么区别? + +`两者都可以暂停线程的执行` + +* 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。 +* 是否释放锁:sleep() 不释放锁;wait() 释放锁。 +* 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 +* 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。 + +#### 你是如何调用 wait() 方法的?使用 if 块还是循环?为什么? + +* 处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。 + +* wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码: + +synchronized (monitor) { + // 判断条件谓词是否得到满足 + while(!locked) { + // 等待唤醒 + monitor.wait(); + } + // 处理其他的业务逻辑 +} +复制代码 +#### 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里? + +* 因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。 + +* 有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。 + +#### 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用? + +* 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。 + +#### Thread 类中的 yield 方法有什么作用? + +* 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。 + +* 当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了。 + +#### 为什么 Thread 类的 sleep()和 yield ()方法是静态的? + +* Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 + +#### 线程的 sleep()方法和 yield()方法有什么区别? + +* (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; + +* (2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态; + +* (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; + +* (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。 + +#### 如何停止一个正在运行的线程? + +* 在java中有以下3种方法可以终止正在运行的线程: + * 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 + * 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 + * 使用interrupt方法中断线程。 + +#### Java 中 interrupted 和 isInterrupted 方法的区别? + +* interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。 + + 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。 + +* interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。 + +* isInterrupted:是可以返回当前中断信号是true还是false,与interrupt最大的差别 + +#### 什么是阻塞式方法? + +* 阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。 + +#### Java 中你怎样唤醒一个阻塞的线程? + +* 首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行; + +* 其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。 + +#### notify() 和 notifyAll() 有什么区别? + +* 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。 + +* notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。 + +* notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。 + +#### 如何在两个线程间共享数据? + +* 在两个线程间共享变量即可实现共享。 + +`一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。` + +#### Java 如何实现多线程之间的通讯和协作? + +* 可以通过中断 和 共享变量的方式实现线程间的通讯和协作 + +* 比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。 + +* Java中线程通信协作的最常见方式: + + * 一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll() + + * 二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll() + +* 线程间直接的数据交换: + + * 三.通过管道进行线程间通信:字节流、字符流 + +#### 同步方法和同步块,哪个是更好的选择? + +* 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。 + +* 同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。 + +`请知道一条原则:同步的范围越小越好。` + +#### 什么是线程同步和线程互斥,有哪几种实现方式? + +* 当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。 + +* 在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。 + +* 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 + +* 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 + +* 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。 + +* 实现线程同步的方法 + + * 同步代码方法:sychronized 关键字修饰的方法 + + * 同步代码块:sychronized 关键字修饰的代码块 + + * 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制 + + * 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义 + +#### 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步? + +* 在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。 + +* 一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码 + +* 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案 + +#### 如果你提交任务时,线程池队列已满,这时会发生什么 + +* 有俩种可能: + + (1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务 + + (2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy + +#### 什么叫线程安全?servlet 是线程安全吗? + +* 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 + +* Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。 + +* Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。 + +* SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。 + +* Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。 + +#### 在 Java 程序中怎么保证多线程的运行安全? + +* 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger + +* 方法二:使用自动锁 synchronized。 + +* 方法三:使用手动锁 Lock。 + +* 手动锁 Java 示例代码如下: + +```java +Lock lock = new ReentrantLock(); + lock. lock(); + try { + System. out. println("获得锁"); + } catch (Exception e) { + // TODO: handle exception + } finally { + System. out. println("释放锁"); + lock. unlock(); + } +``` + +#### 你对线程优先级的理解是什么? + +* 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。 + +* Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。 + +* 当然,如果你真的想设置优先级可以通过setPriority()方法设置,但是设置了不一定会该变,这个是不准确的 + +#### 线程类的构造方法、静态块是被哪个线程调用的 + +* 这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。 + +* 如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么: + +(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的 + +(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的 + +#### Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈? + +* Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。 + +* 在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。 + +* 在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。 + +#### 一个线程运行时发生异常会怎样? + +* 如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。 + +#### Java 线程数过多会造成什么异常? + +* 线程的生命周期开销非常高 + +* 消耗过多的 CPU + + 资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。 + +* 降低稳定性JVM + + 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。 + +#### 多线程的常用方法 + +| 方法 名 | 描述 | +| --- | --- | +| sleep() | 强迫一个线程睡眠N毫秒 | +| isAlive() | 判断一个线程是否存活。 | +| join() | 等待线程终止。 | +| activeCount() | 程序中活跃的线程数。 | +| enumerate() | 枚举程序中的线程。 | +| currentThread() | 得到当前线程。 | +| isDaemon() | 一个线程是否为守护线程。 | +| setDaemon() | 设置一个线程为守护线程。 | +| setName() | 为线程设置一个名称。 | +| wait() | 强迫一个线程等待。 | +| notify() | 通知一个线程继续运行。 | +| setPriority() | 设置一个线程的优先级。 | + +## 并发理论 + +#### Java中垃圾回收有什么目的?什么时候进行垃圾回收? + +* 垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。 + +* 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。 + +#### 线程之间如何通信及线程之间如何同步 + +* 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。 + +* Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。 + +#### Java内存模型 + +* 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab46986d99~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤: + 1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 + 2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。 + +**下面通过示意图来说明线程之间的通信** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab7ff494ac~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。 + +#### 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存? + +* 不会,在下一个垃圾回调周期中,这个对象将是被可回收的。 + +* 也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。 + +#### finalize()方法什么时候被调用?析构函数(finalization)的目的是什么? + +* 1.垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法; finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间 + +* 1. GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。 + * Finalizetion主要用来释放被对象占用的资源(不是指内存,而是指其他资源,比如文件(File Handle)、端口(ports)、数据库连接(DB Connection)等)。然而,它不能真正有效地工作。 + +#### 什么是重排序 + +* 程序执行的顺序按照代码的先后顺序执行。 +* 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 + +int a = 5; //语句1 +int r = 3; //语句2 +a = a + 2; //语句3 +r = a*a; //语句4 +复制代码 + +* 则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。 +* 显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。 + +#### 重排序实际执行的指令步骤 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab5818fe21~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 +2. 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 +3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 + +* 这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。 + +#### 重排序遵守的规则 + +* as-if-serial: + 1. 不管怎么排序,结果不能改变 + 2. 不存在数据依赖的可以被编译器和处理器重排序 + 3. 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序 + 4. 单线程根据此规则不会有问题,但是重排序后多线程会有问题 + +#### as-if-serial规则和happens-before规则的区别 + +* as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 + +* as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 + +* as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 + +#### 并发关键字 synchronized ? + +* 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。 + +* 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 + +#### 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 + +**synchronized关键字最主要的三种使用方式:** + +* 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 +* 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 +* 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 + +`总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!` + +#### 单例模式了解吗?给我解释一下双重检验锁方式实现单例模式!” + +**双重校验锁实现对象单例(线程安全)** + +**说明:** + +* 双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,而且也实现了懒加载 + +```java + public class Singleton { + private volatile static Singleton uniqueInstance; + private Singleton() {} + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } + } +``` +`另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。` + +* uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: + +1. 为 uniqueInstance 分配内存空间 +2. 初始化 uniqueInstance +3. 将 uniqueInstance 指向分配的内存地址 + +`但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。` + +`使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。` + +#### 说一下 synchronized 底层实现原理? + +* Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成, + +* 每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 ,过程: + + 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 + + 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. + + 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 + +`synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。` + +#### synchronized可重入的原理 + +* 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。 + +#### 什么是自旋 + +* 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。 +* 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。 + +#### 多线程中 synchronized 锁升级的原理是什么? + +* synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。 + +`锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。` + +* 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 + +* 轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁; + +* 重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。 + +#### 线程 B 怎么知道线程 A 修改了变量 + +* (1)volatile 修饰变量 + +* (2)synchronized 修饰修改变量的方法 + +* (3)wait/notify + +* (4)while 轮询 + +#### 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B? + +* 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。 + +#### synchronized、volatile、CAS 比较 + +* (1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。 + +* (2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。 + +* (3)CAS 是基于冲突检测的乐观锁(非阻塞) + +#### synchronized 和 Lock 有什么区别? + +* 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类; +* synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。 +* synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 +* 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 + +#### synchronized 和 ReentrantLock 区别是什么? + +* synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量 + +* synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。 + +* 相同点:两者都是可重入锁 + + 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 + +* 主要区别如下: + + * ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作; + * ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁; + * ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。 + * 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word +* Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: + + * 普通同步方法,锁是当前实例对象 + * 静态同步方法,锁是当前类的class对象 + * 同步方法块,锁是括号里面的对象 + +#### volatile 关键字的作用 + +* 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。 + +* 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。 + +* volatile 常用于多线程环境下的单次操作(单次读或者单次写)。 + +#### Java 中能创建 volatile 数组吗? + +* 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。 + +#### volatile 变量和 atomic 变量有什么不同? + +* volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。 + +* 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。 + +#### volatile 能使得一个非原子操作变成原子操作吗? + +* 关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。 + +* 虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。 + +**所以从Oracle Java Spec里面可以看到:** + +* 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。 +* 如果使用volatile修饰long和double,那么其读写都是原子操作 +* 对于64位的引用地址的读写,都是原子操作 +* 在实现JVM时,可以自由选择是否把读写long和double作为原子操作 +* 推荐JVM实现为原子操作 + +#### synchronized 和 volatile 的区别是什么? + +* synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。 + +* volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。 + +**区别** + +* volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。 + +* volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。 + +* volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 + +* volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 + +* volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。 + +#### final不可变对象,它对写并发应用有什么帮助? + +* 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。 + +* 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。 + +* 只有满足如下状态,一个对象才是不可变的; + + * 它的状态不能在创建后再被修改; + + * 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。 + +`不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。` + +#### Lock 接口和synchronized 对比同步它有什么优势? + +* Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。 + +* 它的优势有: + + * (1)可以使锁更公平 + + * (2)可以使线程在等待锁的时候响应中断 + + * (3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间 + + * (4)可以在不同的范围,以不同的顺序获取和释放锁 + +* 整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。 + +#### 乐观锁和悲观锁的理解及如何实现,有哪些实现方式? + +* 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。 + +* 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 + +#### 什么是 CAS + +* CAS 是 compare and swap 的缩写,即我们所说的比较交换。 + +* cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。 + +* CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。 + +`java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)` + +#### CAS 的会产生什么问题? + +* 1、ABA 问题: + + 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。 + +* 2、循环时间长开销大: + + 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。 + +* 3、只能保证一个共享变量的原子操作: + + 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。 + +#### 什么是原子类 + +* java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。 + +* 比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。 + +`简单来说就是原子类来实现CAS无锁模式的算法` + +#### 原子类的常用类 + +* AtomicBoolean +* AtomicInteger +* AtomicLong +* AtomicReference + +#### 说一下 Atomic的原理? + +* Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。 + +#### 死锁与活锁的区别,死锁与饥饿的区别? + +* 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 + +* 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 + +* 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。 + +* 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。 + + Java 中导致饥饿的原因: + + * 1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。 + + * 2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。 + + * 3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。 + +## 线程池 + +#### 什么是线程池? + +* Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。 + * 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 + * 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 + * 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用 + +#### 线程池作用? + +* 线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。 + +* 如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止。 + +#### 线程池有什么优点? + +* 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。 + +* 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 + +* 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +* 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。 + +#### 什么是ThreadPoolExecutor? + +* **ThreadPoolExecutor就是线程池** + + ThreadPoolExecutor其实也是JAVA的一个类,我们一般通过Executors工厂类的方法,通过传入不同的参数,就可以构造出适用于不同应用场景下的ThreadPoolExecutor(线程池) + +构造参数图: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172babf4c562f1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`构造参数参数介绍:`corePoolSize 核心线程数量 +maximumPoolSize 最大线程数量 +keepAliveTime 线程保持时间,N个时间单位 +unit 时间单位(比如秒,分) +workQueue 阻塞队列 +threadFactory 线程工厂 +handler 线程池拒绝策略 +复制代码 +#### 什么是Executors? + +* **Executors框架实现的就是线程池的功能。** + + Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池, + +Executor工厂类如何创建线程池图: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab8fc18fd3~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 线程池四种创建方式? + +* **Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:** + 1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 + 2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 + 3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 + 4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +#### 在 Java 中 Executor 和 Executors 的区别? + +* Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。 + +* Executor 接口对象能执行我们的线程任务。 + +* ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。 + +* 使用 ThreadPoolExecutor 可以创建自定义线程池。 + +#### 四种构建线程池的区别及特点? + +##### 1\. newCachedThreadPool + +* **特点**:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的长度作任何限制 + +* **缺点**:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值 + +* **总结**:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewCachedThreadPool { + public static void main(String[] args) { + // 创建无限大小线程池,由jvm自动回收 + ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); + for (int i = 0; i < 10; i++) { + final int temp = i; + newCachedThreadPool.execute(new Runnable() { + public void run() { + try { + Thread.sleep(100); + } catch (Exception e) { + } + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + +``` + +##### 2.newFixedThreadPool + +* **特点**:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。 + +* **缺点**:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间) + +* **总结**:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors() + +`Runtime.getRuntime().availableProcessors()方法是查看电脑CPU核心数量)` + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewFixedThreadPool { + public static void main(String[] args) { + ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newFixedThreadPool.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + +``` + +##### 3.newScheduledThreadPool + +* **特点**:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类) + +* **缺点**:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.Executors; + import java.util.concurrent.ScheduledExecutorService; + import java.util.concurrent.TimeUnit; + + public class TestNewScheduledThreadPool { + public static void main(String[] args) { + //定义线程池大小为3 + ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newScheduledThreadPool.schedule(new Runnable() { + public void run() { + System.out.println("i:" + temp); + } + }, 3, TimeUnit.SECONDS);//这里表示延迟3秒执行。 + } + } + } + +``` + +##### 4.newSingleThreadExecutor + +* **特点**:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +* **缺点**:缺点的话,很明显,他是单线程的,高并发业务下有点无力 + +* **总结**:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewSingleThreadExecutor { + public static void main(String[] args) { + ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); + for (int i = 0; i < 10; i++) { + final int index = i; + newSingleThreadExecutor.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + " index:" + index); + try { + Thread.sleep(200); + } catch (Exception e) { + } + } + }); + } + } + } + +``` + +#### 线程池都有哪些状态? + +* RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。 +* SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。 +* STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。 +* TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。 +* TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。 + +#### 线程池中 submit() 和 execute() 方法有什么区别? + +* 相同点: + * 相同点就是都可以开启线程执行池中的任务。 +* 不同点: + * 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。 + * 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有 + * 异常处理:submit()方便Exception处理 + +#### 什么是线程组,为什么在 Java 中不推荐使用? + +* ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。 + +* 线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。 + +* 为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。 + +#### ThreadPoolExecutor饱和策略有哪些? + +`如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:` + +* ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。 +* ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 +* ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。 +* ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。 + +#### 如何自定义线程线程池? + +* 先看ThreadPoolExecutor(线程池)这个类的构造参数 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab8f7d464b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)构造参数参数介绍: +```java + corePoolSize 核心线程数量 + maximumPoolSize 最大线程数量 + keepAliveTime 线程保持时间,N个时间单位 + unit 时间单位(比如秒,分) + workQueue 阻塞队列 + threadFactory 线程工厂 + handler 线程池拒绝策略 + +``` +* 代码示例: + +```java +package com.lijie; + + import java.util.concurrent.ArrayBlockingQueue; + import java.util.concurrent.ThreadPoolExecutor; + import java.util.concurrent.TimeUnit; + + public class Test001 { + public static void main(String[] args) { + //创建线程池 + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); + for (int i = 1; i <= 6; i++) { + TaskThred t1 = new TaskThred("任务" + i); + //executor.execute(t1);是执行线程方法 + executor.execute(t1); + } + //executor.shutdown()不再接受新的任务,并且等待之前提交的任务都执行完再关闭,阻塞队列中的任务不会再执行。 + executor.shutdown(); + } + } + + class TaskThred implements Runnable { + private String taskName; + + public TaskThred(String taskName) { + this.taskName = taskName; + } + public void run() { + System.out.println(Thread.currentThread().getName() + taskName); + } + } + +``` + +#### 线程池的执行原理? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac2446a113~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 提交一个任务到线程池中,线程池的处理流程如下: + + 1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。 + + 2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。 + + 3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。 + +#### 如何合理分配线程池大小? + +* 要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配 + +##### 什么是CPU密集 + +* CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 + +* CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。 + +##### 什么是IO密集 + +* IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 + +##### 分配CPU和IO密集: + +1. CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务 + +2. IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数 + +##### 精确来说的话的话: + +* 从以下几个角度分析任务的特性: + + * 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。 + + * 任务的优先级:高、中、低。 + + * 任务的执行时间:长、中、短。 + + * 任务的依赖性:是否依赖其他系统资源,如数据库连接等。 + +**可以得出一个结论:** + +* 线程等待时间比CPU执行时间比例越高,需要越多线程。 +* 线程CPU执行时间比等待时间比例越高,需要越少线程。 + +## 并发容器 + +#### 你经常使用什么并发容器,为什么? + +* 答:Vector、ConcurrentHashMap、HasTable + +* 一般软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等 + +* 但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。 + +#### 什么是Vector + +* Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它比访问ArrayList慢很多 + + (`ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。ArrayList的缺点是每个元素之间不能有间隔。`) + +#### ArrayList和Vector有什么不同之处? + +* Vector方法带上了synchronized关键字,是线程同步的 + +1. ArrayList添加方法源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac57d8b760~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +2. Vector添加源码(加锁了synchronized关键字)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac5bc35f22~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 为什么HashTable是线程安全的? + +* 因为HasTable的内部方法都被synchronized修饰了,所以是线程安全的。其他的都和HashMap一样 + +1. HashMap添加方法的源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac5c47d65f~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +2. HashTable添加方法的源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac599568da~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 用过ConcurrentHashMap,讲一下他和HashTable的不同之处? + +* ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。 + +* 看不懂???很正常,我也看不懂 + +* 总结: + + 1. HashTable就是实现了HashMap加上了synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全 + 2. ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。 + 3. 并且读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。 + 4. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术 + 5. 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容 + +#### Collections.synchronized * 是什么? + +`注意:* 号代表后面是还有内容的` + +* 此方法是干什么的呢,他完完全全的可以把List、Map、Set接口底下的集合变成线程安全的集合 + +* Collections.synchronized * :原理是什么,我猜的话是代理模式:[Java代理模式理解](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fweixin_43122090%2Farticle%2Fdetails%2F104883274 "https://blog.csdn.net/weixin_43122090/article/details/104883274") + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac6e2aff60~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### Java 中 ConcurrentHashMap 的并发度是什么? + +* ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。 + +* 在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。 + +#### 什么是并发容器的实现? + +* 何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。 + +* 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。 + +#### Java 中的同步集合与并发集合有什么区别? + +* 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 + +#### SynchronizedMap 和 ConcurrentHashMap 有什么区别? + +* SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。 + +* ConcurrentHashMap 使用分段锁来保证在多线程下的性能。 + +* ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。 + +* 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。 + +* 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。 + +#### CopyOnWriteArrayList 是什么? + +* CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。 + +* CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。 + +#### CopyOnWriteArrayList 的使用场景? + +* 合适读多写少的场景。 + +#### CopyOnWriteArrayList 的缺点? + +* 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。 +* 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。 +* 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。 + +#### CopyOnWriteArrayList 的设计思想? + +* 读写分离,读和写分开 +* 最终一致性 +* 使用另外开辟空间的思路,来解决并发冲突 + +## 并发队列 + +#### 什么是并发队列: + +* 消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信 + +* 并发队列是什么:并发队列多个线程以有次序共享数据的重要组件 + +#### 并发队列和并发集合的区别: + +`那就有可能要说了,我们并发集合不是也可以实现多线程之间的数据共享吗,其实也是有区别的:` + +* 队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的。 + +* 并发集合就是在多个线程中共享数据的 + +#### 怎么判断并发队列是阻塞队列还是非阻塞队列 + +* 在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列。 + +#### 阻塞队列和非阻塞队列区别 + +* 当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。 + +* 或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。 + +* 或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。 + +* 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来 + +#### 常用并发列队的介绍: + +1. **非堵塞队列:** + + 1. **ArrayDeque, (数组双端队列)** + + ArrayDeque (非堵塞队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。 + + 2. **PriorityQueue, (优先级队列)** + + PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象 + + 3. **ConcurrentLinkedQueue, (基于链表的并发队列)** + + ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。 + +2. **堵塞队列:** + + 1. **DelayQueue, (基于时间优先级的队列,延期阻塞队列)** + + DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。 + + 2. **ArrayBlockingQueue, (基于数组的并发阻塞队列)** + + ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据 + + 3. **LinkedBlockingQueue, (基于链表的FIFO阻塞队列)** + + LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。 + + 4. **LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)** + + LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 + + 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。 + + LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。 + + 5. **PriorityBlockingQueue, (带优先级的无界阻塞队列)** + + priorityBlockingQueue是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。 + + 6. **SynchronousQueue (并发同步阻塞队列)** + + SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。 + + 将这个类称为队列有点夸大其词。这更像是一个点。 + +#### 并发队列的常用方法 + +`不管是那种列队,是那个类,当是他们使用的方法都是差不多的` + +| 方法名 | 描述 | +| --- | --- | +| add() | 在不超出队列长度的情况下插入元素,可以立即执行,成功返回true,如果队列满了就抛出异常。 | +| offer() | 在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插入指定元素,成功时返回true,如果此队列已满,则返回false。 | +| put() | 插入元素的时候,如果队列满了就进行等待,直到队列可用。 | +| take() | 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。 | +| poll(long timeout, TimeUnit unit) | 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。 | +| remainingCapacity() | 获取队列中剩余的空间。 | +| remove(Object o) | 从队列中移除指定的值。 | +| contains(Object o) | 判断队列中是否拥有该值。 | +| drainTo(Collection c) | 将队列中值,全部移除,并发设置到给定的集合中。 | + +## 并发工具类 + +#### 常用的并发工具类有哪些? + +* CountDownLatch + + CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。 + +* CyclicBarrier (回环栅栏) CyclicBarrier它的作用就是会让所有线程都等待完成后才会继续下一步行动。 + + CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。 + + CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。 + +* Semaphore (信号量) Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。 + + Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。它的用法如下: + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125755293710 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" new file mode 100644 index 0000000..566eeaa --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" @@ -0,0 +1,739 @@ + + +## Java异常架构与异常关键字 + +### Java异常简介 + +* Java异常是Java提供的一种识别及响应错误的一致性机制。 + Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。 + +### Java异常架构 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de0260d11~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 1\. Throwable + +* Throwable 是 Java 语言中所有错误与异常的超类。 + +* Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。 + +* Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。 + +#### 2\. Error(错误) + +* **定义**:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 + +* **特点**:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。 + +* 这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的! + +#### 3\. Exception(异常) + +* 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。 + +##### 运行时异常 + +* **定义**:RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 + +* **特点**:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生! + +* RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(**就算我们没写异常捕获语句运行时也会抛出错误**!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。 + +##### 编译时异常 + +* **定义**: Exception 中除 RuntimeException 及其子类之外的异常。 + +* **特点**: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。**该异常我们必须手动在代码里添加捕获语句来处理该异常**。 + +#### 4\. 受检异常与非受检异常 + +* Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。 + +##### 受检异常 + +* 编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。**除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常**。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。 + +##### 非受检异常 + +* 编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。**该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。** + +### Java异常关键字 + +* **try** – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。 +* **catch** – 用于捕获异常。catch用来捕获try语句块中发生的异常。 +* **finally** – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。 +* **throw** – 用于抛出异常。 +* **throws** – 用在方法签名中,用于声明该方法可能抛出的异常。 + +## Java异常处理 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de2de5ccf~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。 + +* 在Java应用中,异常的处理机制分为声明异常,抛出异常和捕获异常。 + +### 声明异常 + +* 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 **throws** 关键字声明可能会抛出的异常。 + +**注意** + +* 非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。 +* 一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误。 + +### 抛出异常 + +* 如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。 + +* throw关键字作用是在方法内部抛出一个`Throwable`类型的异常。任何Java代码都可以通过throw语句抛出异常。 + +### 捕获异常 + +* 程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。 + +### 如何选择异常类型 + +* 可以根据下图来选择是捕获异常,声明异常还是抛出异常 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de32f26f7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 常见异常处理方式 + +#### 直接抛出异常 + +* 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 **throws** 关键字声明可能会抛出的异常。 + +private static void readFile(String filePath) throws IOException { + File file = new File(filePath); + String result; + BufferedReader reader = new BufferedReader(new FileReader(file)); + while((result = reader.readLine())!=null) { + System.out.println(result); + } + reader.close(); +} +复制代码 +#### 封装异常再抛出 + +* 有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。 + +private static void readFile(String filePath) throws MyException { + try { + // code + } catch (IOException e) { + MyException ex = new MyException("read file failed."); + ex.initCause(e); + throw ex; + } +} +复制代码 +#### 捕获异常 + +* 在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理 + +private static void readFile(String filePath) { + try { + // code + } catch (FileNotFoundException e) { + // handle FileNotFoundException + } catch (IOException e){ + // handle IOException + } +} +复制代码 + +* 同一个 catch 也可以捕获多种类型异常,用 | 隔开 + +private static void readFile(String filePath) { + try { + // code + } catch (FileNotFoundException | UnknownHostException e) { + // handle FileNotFoundException or UnknownHostException + } catch (IOException e){ + // handle IOException + } +} +复制代码 +#### 自定义异常 + +* 习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用) + +public class MyException extends Exception { + public MyException(){ } + public MyException(String msg){ + super(msg); + } + // ... +} +复制代码 +#### try-catch-finally + +* 当方法中发生异常,异常处之后的代码不会再执行,如果之前获取了一些本地资源需要释放,则需要在方法正常结束时和 catch 语句中都调用释放本地资源的代码,显得代码比较繁琐,finally 语句可以解决这个问题。 + +private static void readFile(String filePath) throws MyException { + File file = new File(filePath); + String result; + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(file)); + while((result = reader.readLine())!=null) { + System.out.println(result); + } + } catch (IOException e) { + System.out.println("readFile method catch block."); + MyException ex = new MyException("read file failed."); + ex.initCause(e); + throw ex; + } finally { + System.out.println("readFile method finally block."); + if (null != reader) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +复制代码 + +* 调用该方法时,读取文件时若发生异常,代码会进入 catch 代码块,之后进入 finally 代码块;若读取文件时未发生异常,则会跳过 catch 代码块直接进入 finally 代码块。所以无论代码中是否发生异常,fianlly 中的代码都会执行。 + +* 若 catch 代码块中包含 return 语句,finally 中的代码还会执行吗?将以上代码中的 catch 子句修改如下: + +catch (IOException e) { + System.out.println("readFile method catch block."); + return; +} +复制代码 + +* 调用 readFile 方法,观察当 catch 子句中调用 return 语句时,finally 子句是否执行 + +readFile method catch block. +readFile method finally block. +复制代码 + +* 可见,即使 catch 中包含了 return 语句,finally 子句依然会执行。若 finally 中也包含 return 语句,finally 中的 return 会覆盖前面的 return. + +#### try-with-resource + +* 上面例子中,finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。 + +private static void tryWithResourceTest(){ + try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){ + // code + } catch (IOException e){ + // handle exception + } +} +复制代码 + +* try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。 + +## Java异常常见面试题 + +### 1\. Error 和 Exception 区别是什么? + +* Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复; + +* Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。 + +### 2\. 运行时异常和一般异常(受检异常)区别是什么? + +* 运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。 + +* 受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。 + +* **RuntimeException异常和受检异常之间的区别**:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。 + +### 3\. JVM 是如何处理异常的? + +* 在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。 + +* JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。 + +### 4\. throw 和 throws 的区别是什么? + +* Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。 + +**throws 关键字和 throw 关键字在使用上的几点区别如下**: + +* throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。 +* throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。 + +### 5\. final、finally、finalize 有什么区别? + +* final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 +* finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 +* finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。 + +### 6\. NoClassDefFoundError 和 ClassNotFoundException 区别? + +* NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。 + +* 引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致; + +* ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。 + +### 7\. try-catch-finally 中哪个部分可以省略? + +* 答:catch 可以省略 + +**原因** + +* 更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。 + +* 理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。 + +* 至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。 + +### 8\. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗? + +* 答:会执行,在 return 前执行。 + +* **注意**:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。 + +**代码示例1:** + +public static int getInt() { + int a = 10; + try { + System.out.println(a / 0); + a = 20; + } catch (ArithmeticException e) { + a = 30; + return a; + /* + * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了 + * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40 + * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30 + */ + } finally { + a = 40; + } + return a; +} +复制代码 + +* 执行结果:30 + +**代码示例2:** + +public static int getInt() { + int a = 10; + try { + System.out.println(a / 0); + a = 20; + } catch (ArithmeticException e) { + a = 30; + return a; + } finally { + a = 40; + //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40 + return a; + } + +} +复制代码 + +* 执行结果:40 + +### 9\. 类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。 + +* 有如下代码片断: + +try { + throw new ExampleB("b") +} catch(ExampleA e){ + System.out.println("ExampleA"); +} catch(Exception e){ + System.out.println("Exception"); +} +复制代码 + +* 请问执行此段代码的输出是什么? + +* **答**:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常) + +* 面试题 - 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书) + +class Annoyance extends Exception { +} +class Sneeze extends Annoyance { +} +class Human { + public static void main(String[] args) + throws Exception { + try { + try { + throw new Sneeze(); + } catch ( Annoyance a ) { + System.out.println("Caught Annoyance"); + throw a; + } + } catch ( Sneeze s ) { + System.out.println("Caught Sneeze"); + return ; + } finally { + System.out.println("Hello World!"); + } + } +} +复制代码 + +* 结果 + +Caught Annoyance +Caught Sneeze +Hello World! +复制代码 +### 10\. 常见的 RuntimeException 有哪些? + +* ClassCastException(类转换异常) +* IndexOutOfBoundsException(数组越界) +* NullPointerException(空指针) +* ArrayStoreException(数据存储异常,操作数组时类型不一致) +* 还有IO操作的BufferOverflowException异常 + +### 11\. Java常见异常有哪些 + +* java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。 + +* java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常. + +* java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。 + +* java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。 + +* java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。 + +* java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。 + +* java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。 + +* java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。 + +* java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。 + +* java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。 + +* java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。 + +* java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。- + +* java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。 + +* java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。 + +* java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。 + +## Java异常处理最佳实践 + +* 在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。 + +* 本文给出几个被很多团队使用的异常处理最佳实践。 + +### 1\. 在 finally 块中清理资源或者使用 try-with-resource 语句 + +* 当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。 + +public void doNotCloseResourceInTry() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + // use the inputStream to read a file + // do NOT do this + inputStream.close(); + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +复制代码 + +* 问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是,你并没有关闭资源。 + +所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。 + +#### 1.1 使用 finally 代码块 + +* 与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资源。 + +public void closeResourceInFinally() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + // use the inputStream to read a file + } catch (FileNotFoundException e) { + log.error(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error(e); + } + } + } +} +复制代码 +#### 1.2 Java 7 的 try-with-resource 语法 + +* 如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。 + +public void automaticallyCloseResource() { + File file = new File("./tmp.txt"); + try (FileInputStream inputStream = new FileInputStream(file);) { + // use the inputStream to read a file + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} + +复制代码 + +​ + +### 2\. 优先明确的异常 + +* 你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。 + +* 因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的处理异常并且避免额外的检查。 + +* 因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。 + +public void doNotDoThis() throws Exception { + ... +} +public void doThis() throws NumberFormatException { + ... +} + +复制代码 + +​ + +### 3\. 对异常进行文档说明 + +* 当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。 + 在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。 + +public void doSomething(String input) throws MyBusinessException { + ... +} + +复制代码 +### 4\. 使用描述性消息抛出异常 + +* 在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。 + +* 但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。 + +* 如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。 + +try { + new Long("xyz"); +} catch (NumberFormatException e) { + log.error(e); +} + +复制代码 +### 5\. 优先捕获最具体的异常 + +* 大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。 + +* 但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch 块,因为它是 IllegalArgumentException 的子类。 + +* 总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。 + +* 你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个 catch 块处理所有 NumberFormatException 异常,第二个处理所有非 NumberFormatException 异常的IllegalArgumentException 异常。 + +public void catchMostSpecificExceptionFirst() { + try { + doSomething("A message"); + } catch (NumberFormatException e) { + log.error(e); + } catch (IllegalArgumentException e) { + log.error(e) + } +} + +复制代码 + +​ + +### 6\. 不要捕获 Throwable 类 + +* Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做! + +* 如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。 + +* 所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。 + +public void doNotCatchThrowable() { + try { + // do something + } catch (Throwable t) { + // don't do this! + } +} + +复制代码 +### 7\. 不要忽略异常 + +* 很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。 + +public void doNotIgnoreExceptions() { + try { + // do something + } catch (NumberFormatException e) { + // this will never happen + } +} + +复制代码 + +* 但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。 + +* 合理的做法是至少要记录异常的信息。 + +public void logAnException() { + try { + // do something + } catch (NumberFormatException e) { + log.error("This should never happen: " + e); + } +} + +复制代码 +### 8\. 不要记录并抛出异常 + +* 这可能是本文中最常被忽略的最佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下: + +try { + new Long("xyz"); +} catch (NumberFormatException e) { + log.error(e); + throw e; +} + +复制代码 + +* 这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下: + +17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz" +Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz" +at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) +at java.lang.Long.parseLong(Long.java:589) +at java.lang.Long.(Long.java:965) +at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63) +at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58) + +复制代码 + +* 如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。 + +public void wrapException(String input) throws MyBusinessException { + try { + // do something + } catch (NumberFormatException e) { + throw new MyBusinessException("A message that describes the error.", e); + } +} + +复制代码 + +* 因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。 + +### 9\. 包装异常时不要抛弃原始的异常 + +* 捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。 + 在你这样做时,请确保将原始异常设置为原因(注:参考下方代码 NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。 + +public void wrapException(String input) throws MyBusinessException { + try { + // do something + } catch (NumberFormatException e) { + throw new MyBusinessException("A message that describes the error.", e); + } +} + +复制代码 + +​ + +### 10\. 不要使用异常控制程序的流程 + +* 不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。 + +### 11\. 使用标准异常 + +* 如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。 + +### 12\. 异常会影响性能 + +* 异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。 + + * 仅在异常情况下使用异常; + * 在可恢复的异常情况下使用异常; +* 尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。 + +### 13\. 总结 + +* 综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。 + +* 异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。 + +### 异常处理-阿里巴巴Java开发手册 + +1. 【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等。 说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。 正例:if (obj != null) {…} 反例:try { obj.method(); } catch (NullPointerException e) {…} + +2. 【强制】异常不要用来做流程控制,条件控制。 说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。 + +3. 【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。 说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。 + +4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。 + +5. 【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。 + +6. 【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。 说明:如果JDK7及以上,可以使用try-with-resources方式。 + +7. 【强制】不要在finally块中使用return。 说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。 反例: + + private int x = 0; + public int checkReturn() { + try { + // x等于1,此处不返回 + return ++x; + } finally { + // 返回的结果是2 + return ++x; + } + } + +复制代码 + +1. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。 说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。 + +2. 【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。 说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。 + +3. 【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。 说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。 + +4. 【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景: 1) 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。 反例:public int f() { return Integer对象}, 如果为null,自动解箱抛NPE。 2) 数据库的查询结果可能为null。 3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。 4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。 5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。 6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。 + 正例:使用JDK8的Optional类来防止NPE问题。 + +5. 【推荐】定义时区分unchecked / checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。 + +6. 【参考】对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。 说明:关于RPC方法返回方式使用Result方式的理由: 1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。 2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。 + +7. 【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。 正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取: + private boolean checkParam(DTO dto) {…} + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904128959741965 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" new file mode 100644 index 0000000..382dac4 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" @@ -0,0 +1,627 @@ + + +## Java内存模型 + +### 我们开发人员编写的Java代码是怎么让电脑认识的 + +* 首先先了解电脑是二进制的系统,他只认识 01010101 + +* 比如我们经常要编写 HelloWord.java 电脑是怎么认识运行的 + +* HelloWord.java是我们程序员编写的,我们人可以认识,但是电脑不认识 + +**Java文件编译的过程** + +1. 程序员编写的.java文件 +2. 由javac编译成字节码文件.class:(为什么编译成class文件,因为JVM只认识.class文件) +3. 在由JVM编译成电脑认识的文件 (对于电脑系统来说 文件代表一切) + +`(这是一个大概的观念 抽象画的概念)` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc7554d11b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 为什么说java是跨平台语言 + +* 这个夸平台是中间语言(JVM)实现的夸平台 +* Java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统 + +`难道 C 和 C++ 不能夸平台吗 其实也可以` `C和C++需要在编译器层面去兼容不同操作系统的不同层面,写过C和C++的就知道不同操作系统的有些代码是不一样` + +### Jdk和Jre和JVM的区别 + +* Jdk包括了Jre和Jvm,Jre包括了Jvm + +* Jdk是我们编写代码使用的开发工具包 + +* Jre 是Java的运行时环境,他大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 + +* Jvm俗称Java虚拟机,他是java运行环境的一部分,它虚构出来的一台计算机,在通过在实际的计算机上仿真模拟各种计算机功能来实现Java应用程序 + +* 看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc8693d47b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 说一下 JVM由那些部分组成,运行流程是什么? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc868d44b7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。 + + * Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。 + + * Execution engine(执行引擎):执行classes中的指令。 + + * Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。 + + * Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。 + +* **流程** :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。 + +### 说一下 JVM 运行时数据区 + +* Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域: + +`简单的说就是我们java运行时的东西是放在那里的` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc8784541e~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成; + + `为什么要线程计数器?因为线程是不具备记忆功能` + +* Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息; + + `栈帧就是Java虚拟机栈中的下一个单位` + +* 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的; + + `Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码` + +* Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存; + +* 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。 + +`后面有详细的说明JVM 运行时数据区` + +### 详细的介绍下程序计数器?(重点理解) + +1. 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号) + +2. 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。 + + `总结:也可以把它叫做线程计数器` + +* **例子**:在java中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑,就是 CPU。在CPU上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是时基于时间片的,也就是当前的这一纳秒是分配给那个指令的。 + +* **假如**: + + * 线程A在看直播 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc9acf8957~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * 突然,线程B来了一个视频电话,就会抢夺线程A的时间片,就会打断了线程A,线程A就会挂起 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcc70da181~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * 然后,视频电话结束,这时线程A究竟该干什么? (线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:**程序计数器来记录**) ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcc90c8a88~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 详细介绍下Java虚拟机栈?(重点理解) + +1. Java虚拟机是线程私有的,它的生命周期和线程相同。 +2. 虚拟机栈描述的是Java方法执行的内存模型:`每个方法在执行的同时`都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 + +* **解释**:虚拟机栈中是有单位的,单位就是**栈帧**,一个方法一个**栈帧**。一个**栈帧**中他又要存储,局部变量,操作数栈,动态链接,出口等。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fccadd7f8b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**解析栈帧:** + +1. 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。) +2. 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去 +3. 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。 +4. 出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落 + +#### 一个方法调用另一个方法,会创建很多栈帧吗? + +* 答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面 + +#### 栈指向堆是什么意思? + +* 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址 + +#### 递归的调用自己会创建很多栈帧吗? + +* 答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去 + +### 你能给我详细的介绍Java堆吗?(重点理解) + +* java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。 +* 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。 +* java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。 +* 从内存回收角度来看java堆可分为:新生代和老生代。 +* 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。 +* 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。 +* 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。 + +### 能不能解释一下本地方法栈? + +1. 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字 +2. 它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务方法 +3. native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。 +4. 同理可得,本地方法栈中就是C和C++的代码 + +### 能不能解释一下方法区(重点理解) + +1. 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 +2. 它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。 + +### 什么是JVM字节码执行引擎 + +* 虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。 + +* “虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要JVM字节码执行引擎- 编译成机器码后才可在物理机上执行。 + +### 你听过直接内存吗? + +* 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。 +* 我的理解就是直接内存是基于物理内存和Java虚拟机内存的中间内存 + +### 知道垃圾收集系统吗? + +* 程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。 + +* 垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理 + +* 有一部分原因就是因为Java垃圾回收系统的强大导致Java领先市场 + +### 堆栈的区别是什么? + +> | 对比 | JVM堆 | JVM栈 | +> | --- | --- | --- | +> | 物理地址 | 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩) | 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。 | +> | 内存分别 | 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。 | 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。 | +> | 存放的内容 | 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储 | 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 | +> | 程序的可见度 | 堆对于整个应用程序都是共享、可见的。 | 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。 | + +* 注意: + * 静态变量放在方法区 + * 静态的对象还是放在堆。 + +### 深拷贝和浅拷贝 + +* 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址, +* 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存, +* 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。 +* 深复制:在计算机中开辟一块**新的内存地址**用于存放复制的对象。 + +### Java会存在内存泄漏吗?请说明为什么? + +* 内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。 + +* 但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,`尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收`,这就是java中内存泄露的发生场景。 + +## 垃圾回收机制及算法 + +### 简述Java垃圾回收机制 + +* 在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。 + +### GC是什么?为什么要GC + +* GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。 + +### 垃圾回收的优点和缺点 + +* 优点:JVM的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点 + +* 缺点:程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。 + +### 垃圾回收器的原理是什么?有什么办法手动进行垃圾回收? + +* 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。 + +* 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。 + +* 可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。 + +### JVM 中都有哪些引用类型? + +* 强引用:发生 gc 的时候不会被回收。 +* 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。 +* 弱引用:有用但不是必须的对象,在下一次GC时会被回收。 +* 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。 + +### 怎么判断对象是否可以被回收? + +* 垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死掉了,需要被回收。 + +* 一般有两种方法来判断: + + * 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;(这个已经淘汰了) + * 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。(市场上用的非常非常广泛) + +### Full GC是什么 + +* 清理整个堆空间—包括年轻代和老年代和永久代 +* 因为Full GC是清理整个堆空间所以Full GC执行速度非常慢,在Java开发中最好保证少触发Full GC + +### 对象什么时候可以被垃圾器回收 + +* 当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。 +* 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。 + +### JVM 垃圾回收算法有哪些? + +* 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。 +* 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。 +* 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。 +* 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。 + +#### 标记-清除算法 + +* 标记无用对象,然后进行清除回收。 + +* 标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段: + + * 标记阶段:标记出可以回收的对象。 + * 清除阶段:回收被标记的对象所占用的空间。 +* 标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。 + +* **优点**:实现简单,不需要对象进行移动。 + +* **缺点**:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。 + +* 标记-清除算法的执行的过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fce4d05e44~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 复制算法 + +* 为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。 + +* **优点**:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。 + +* **缺点**:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。 + +* 复制算法的执行过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcd22194ad~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 标记-整理算法 + +* 在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。 + +* **优点**:解决了标记-清理算法存在的内存碎片问题。 + +* **缺点**:仍需要进行局部对象移动,一定程度上降低了效率。 + +* 标记-整理算法的执行过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fce7bb60b1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 分代收集算法 + +* 当前商业虚拟机都采用 `分代收集`的垃圾收集算法。分代收集算法,顾名思义是根据对象的`存活周期`将内存划分为几块。一般包括`年轻代`、`老年代`和 `永久代`,如图所示:`(后面有重点讲解)` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcf6f94e92~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### JVM中的永久代中会发生垃圾回收吗 + +* 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 + (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区) + +## 垃圾收集器以及新生代、老年代、永久代 + +### 讲一下新生代、老年代、永久代的区别 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd069db773~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。 + +* 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了`复制算法`,只需要付出少量存活对象的复制成本就可以完成收集。 + +* 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用`“标记-清理”或者“标记-整理”`算法。 + +* 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。 + +### Minor GC、Major GC、Full GC是什么 + +1. Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾) +2. Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法) +3. Full GC是清理整个堆空间,包括年轻代和老年代 + +### Minor GC、Major GC、Full GC区别及触发条件 + +* **Minor GC 触发条件一般为:** + + 1. eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。 + 2. 新创建的对象大小 > Eden所剩空间时触发Minor GC +* **Major GC和Full GC 触发条件一般为:** `Major GC通常是跟full GC是等价的` + + 1. 每次晋升到老年代的对象平均大小>老年代剩余空间 + + 2. MinorGC后存活的对象超过了老年代剩余空间 + + 3. 永久代空间不足 + + 4. 执行System.gc() + + 5. CMS GC异常 + + 6. 堆内存分配很大的对象 + +### 为什么新生代要分Eden和两个 Survivor 区域? + +* 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。 +* Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。 +* 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生) + +### Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例? + +* 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。 + +* 其中,新生代 ( Young ) 被细分为 Eden 和 **两个 Survivor 区域**,Edem 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ), + +* 但是JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 + +### 为什么要这样分代: + +* 其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法: + + * 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 + + * 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。 + +* 新生代又分为Eden和Survivor (From与To,这里简称一个区)两个区。加上老年代就这三个区。数据会首先分配到Eden区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。当Eden没有足够空间的时候就会触发jvm发起一次Minor GC,。如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。 + +### 什么是垃圾回收器他和垃圾算法有什么区别 + +* 垃圾收集器是垃圾回收算法(标记清楚法、标记整理法、复制算法、分代算法)的具体实现,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能会有很在差别。 + +### 说一下 JVM 有哪些垃圾回收器? + +* 如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd022875b5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +> | 垃圾回收器 | 工作区域 | 回收算法 | 工作线程 | 用户线程并行 | 描述 | +> | --- | --- | --- | --- | --- | --- | +> | Serial | 新生带 | 复制算法 | 单线程 | 否 | Client模式下默认新生代收集器。简单高效 | +> | ParNew | 新生带 | 复制算法 | 多线程 | 否 | Serial的多线程版本,Server模式下首选, 可搭配CMS的新生代收集器 | +> | Parallel Scavenge | 新生带 | 复制算法 | 多线程 | 否 | 目标是达到可控制的吞吐量 | +> | Serial Old | 老年带 | 标记-整理 | 单线程 | 否 | Serial老年代版本,给Client模式下的虚拟机使用 | +> | Parallel Old | 老年带 | 标记-整理 | 多线程 | 否 | Parallel Scavenge老年代版本,吞吐量优先 | +> | | | | | | | +> | G1 | 新生带 + 老年带 | 标记-整理 + 复制算法 | 多线程 | 是 | JDK1.9默认垃圾收集器 | + +* Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效; +* ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现; +* Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景; +* Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本; +* Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本; +* CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。 +* G1(Garbage First)收集器 ( `标记整理 + 复制算法来回收垃圾` ): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。 + +### 收集器可以这么分配?(了解就好了) + +Serial / Serial Old +Serial / CMS +ParNew / Serial Old +ParNew / CMS +Parallel Scavenge / Serial Old +Parallel Scavenge / Parallel Old +G1 +复制代码 +### 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别? + +* 新生代回收器:Serial、ParNew、Parallel Scavenge +* 老年代回收器:Serial Old、Parallel Old、CMS +* 整堆回收器:G1 + +新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。 + +### 简述分代垃圾回收器是怎么工作的? + +* 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 + +* 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下: + + * 把 Eden + From Survivor 存活的对象放入 To Survivor 区; + * 清空 Eden 和 From Survivor 分区; + * From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。 +* 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。 + +* 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。 + +## 内存分配策略 + +### 简述java内存分配与回收策率以及Minor GC和Major GC + +* 所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面我们介绍了内存回收,这里我们再来聊聊内存分配。 + +* 对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则: + +#### 对象优先在 Eden 区分配 + +* 多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。 + * 这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。 + * **Minor GC** 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快; + * **Major GC/Full GC** 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。 + +#### 为什么大对象直接进入老年代 + +* 所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。 + +* 前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。 + +#### 长期存活对象将进入老年代 + +* 虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。 + +## 虚拟机类加载机制 + +### 简述java类加载机制? + +* 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。 + +### 类加载的机制及过程 + +* 程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd24770998~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 1、加载 + +* 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。 + +* Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。 + +* 类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。 + +* 类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口 + +##### 2、连接过程 + +* 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。 + +1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。 + +2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配 + +3. 解析:虚拟机常量池的符号引用替换为字节引用过程 + +##### 3、初始化 + +* 初始化阶段是执行类构造器``() 方法的过程。类构造器``()方法是由编译器自动收藏类中的`所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。` + +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 + +* 虚拟机会保证一个类的``() 方法在多线程环境中被正确加锁和同步 + +`初始化的总结就是:初始化是为类的静态变量赋予正确的初始值` + +### 描述一下JVM加载Class文件的原理机制 + +* Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。 + +* 类装载方式,有两种 : + + * 1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中, + + * 2.显式装载, 通过class.forname()等方法,显式加载需要的类 + +* Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。 + +### 什么是类加载器,类加载器有哪些? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd30195e5b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。 + +* 主要有一下四种类加载器: + + 1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。 + 2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 + 3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。 + 4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。 + +### 说一下类装载的执行过程? + +* 类装载分为以下 5 个步骤: + * 加载:根据查找路径找到相应的 class 文件然后导入; + * 验证:检查加载的 class 文件的正确性; + * 准备:给类中的静态变量分配内存空间; + * 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址; + * 初始化:对静态变量和静态代码块执行初始化工作。 + +### 什么是双亲委派模型? + +* 在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd30195e5b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 类加载器分类: + + * 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库; + * 其他类加载器: + * 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库; + * 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。 +* 双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。 + +* 总结就是:`当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。` + +## JVM调优 + +### JVM 调优的参数可以在那设置参数值 + +* 可以在IDEA,Eclipse,工具里设置 + +* 如果上线了是WAR包的话可以在Tomcat设置 + +* 如果是Jar包直接 :java -jar 是直接插入JVM命令就好了 + + java -Xms1024m -Xmx1024m ...等等等 JVM参数 -jar springboot_app.jar & + 复制代码 + +### 说一下 JVM 调优的工具? + +* JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。 + + * jconsole:用于对 JVM 中的内存、线程和类等进行监控; ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd4667d98b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd4d2a3018~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 常用的 JVM 调优的参数都有哪些? + +#常用的设置 +-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。 + +-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。 + +-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。 + +-XX:NewSize=n 设置年轻代初始化大小大小 + +-XX:MaxNewSize=n 设置年轻代最大值 + +-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4 + +-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8 + +-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。 + +-XX:ThreadStackSize=n 线程堆栈大小 + +-XX:PermSize=n 设置持久代初始值 + +-XX:MaxPermSize=n 设置持久代大小 + +-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。 + +#下面是一些不常用的 + +-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小 + +-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能 + +-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用 + +-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动 + +-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用 + +-Xnoclassgc 是否禁用垃圾回收 + +-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用 + +等等等...... +复制代码 +### JVM的GC收集器设置 + +* -xx:+Use xxx GC + * xxx 代表垃圾收集器名称 + +-XX:+UseSerialGC:设置串行收集器,年轻带收集器 + +-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。 + +-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量 + +-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。 + +-XX:+UseConcMarkSweepGC:设置年老代并发收集器 + +-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器 + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125696573448 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" new file mode 100644 index 0000000..077a179 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" @@ -0,0 +1,923 @@ + + +## 集合容器概述 + +### 什么是集合 + +* 集合就是一个放数据的容器,准确的说是放数据对象引用的容器 + +* 集合类存放的都是对象的引用,而不是对象的本身 + +* 集合类型主要有3种:set(集)、list(列表)和map(映射)。 + +### 集合的特点 + +* 集合的特点主要有如下两点: + * 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。 + + * 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小 + +### 集合和数组的区别 + +* 数组是固定长度的;集合可变长度的。 + +* 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。 + +* 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。 + +### 使用集合框架的好处 + +1. 容量自增长; +2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量; +3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。 +4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。 + +### 常用的集合类有哪些? + +* Map接口和Collection接口是所有集合框架的父接口: + +1. Collection接口的子接口包括:Set接口和List接口 +2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 +3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 +4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等 + +### List,Set,Map三者的区别? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e70de4bd~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。 + +* Collection集合主要有List和Set两大接口 + + * List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 + * Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 +* Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 + + * Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap + +### 集合框架底层数据结构 + +* Collection + + 1. List + + * Arraylist: Object数组 + + * Vector: Object数组 + + * LinkedList: 双向循环链表 + + 2. Set + + * HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 + * LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 + * TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) +* Map + + * HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 + * LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 + * HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 + * TreeMap: 红黑树(自平衡的排序二叉树) + +### 哪些集合类是线程安全的? + +* Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。 +* hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。 +* ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用) +* ... + +### Java集合的快速失败机制 “fail-fast”? + +* 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。 + +* 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 + +* 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 + +* 解决办法: + + 1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。 + + 2. 使用CopyOnWriteArrayList来替换ArrayList + +### 怎么确保一个集合不能被修改? + +* 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。 + +* 示例代码如下: + + List list = new ArrayList<>(); + list. add("x"); + Collection clist = Collections. unmodifiableCollection(list); + clist. add("y"); // 运行时此行报错 + System. out. println(list. size()); + 复制代码 + +## Collection接口 + +### List接口 + +#### 迭代器 Iterator 是什么? + +* Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。 +* 因为所有Collection接继承了Iterator迭代器 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e6f6342b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### Iterator 怎么使用?有什么特点? + +* Iterator 使用代码如下: + + List list = new ArrayList<>(); + Iterator it = list. iterator(); + while(it. hasNext()){ + String obj = it. next(); + System. out. println(obj); + } + 复制代码 +* Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。 + +#### 如何边遍历边移除 Collection 中的元素? + +* 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下: + + Iterator it = list.iterator(); + while(it.hasNext()){ + *// do something* + it.remove(); + } + 复制代码 + +一种最常见的**错误**代码如下: + +for(Integer i : list){ + list.remove(i) +} +复制代码 + +* 运行以上错误代码会报 **ConcurrentModificationException 异常**。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。 + +#### Iterator 和 ListIterator 有什么区别? + +* Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。 +* Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。 +* ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 + +#### 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? + +* 遍历方式有以下几种: + + 1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 + 2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 + 3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 +* 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 + + * 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。 + + * 如果没有实现该接口,表示不支持 Random Access,如LinkedList。 + + * 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 + +#### 说一下 ArrayList 的优缺点 + +* ArrayList的优点如下: + + * ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。 + * ArrayList 在顺序添加一个元素的时候非常方便。 +* ArrayList 的缺点如下: + + * 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。 + * 插入元素的时候,也需要做一次元素复制操作,缺点同上。 +* ArrayList 比较适合顺序添加、随机访问的场景。 + +#### 如何实现数组和 List 之间的转换? + +* 数组转 List:使用 Arrays. asList(array) 进行转换。 +* List 转数组:使用 List 自带的 toArray() 方法。 + +* 代码示例: + + // list to array + List list = new ArrayList(); + list.add("123"); + list.add("456"); + list.toArray(); + + // array to list + String[] array = new String[]{"123","456"}; + Arrays.asList(array); + 复制代码 + +#### ArrayList 和 LinkedList 的区别是什么? + +* 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。 +* 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 +* 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。 +* 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 +* 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; + +* 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 + +* LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 + +#### ArrayList 和 Vector 的区别是什么? + +* 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 + + * 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。 + * 性能:ArrayList 在性能方面要优于 Vector。 + * 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 +* Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 + +* Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 + +#### 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? + +* ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。 + +* Vector 中的方法由于加了 synchronized 修饰,因此 **Vector** **是线程安全容器,但性能上较ArrayList差**。 + +* LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 **LinkedList** **插入速度较快**。 + +#### 多线程场景下如何使用 ArrayList? + +* ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样: + + List synchronizedList = Collections.synchronizedList(list); + synchronizedList.add("aaa"); + synchronizedList.add("bbb"); + + for (int i = 0; i < synchronizedList.size(); i++) { + System.out.println(synchronizedList.get(i)); + } + 复制代码 + +#### 为什么 ArrayList 的 elementData 加上 transient 修饰? + +* ArrayList 中的数组定义如下: + + private transient Object[] elementData; + +* 再看一下 ArrayList 的定义: + + public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable + 复制代码 +* 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: + + private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ + *// Write out element count, and any hidden stuff* + int expectedModCount = modCount; + s.defaultWriteObject(); + *// Write out array length* + s.writeInt(elementData.length); + *// Write out all elements in the proper order.* + for (int i=0; i +* 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。 + +#### List 和 Set 的区别 + +* List , Set 都是继承自Collection 接口 + +* List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 + +* Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 + +* 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。 + +* Set和List对比 + + * Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 + * List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变 + +### Set接口 + +#### 说一下 HashSet 的实现原理? + +* HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。 + +#### HashSet如何检查重复?HashSet是如何保证数据不可重复的? + +* 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。 + +* HashSet 中的add ()方法会使用HashMap 的put()方法。 + +* HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。 + +* 以下是HashSet 部分源码: + + private static final Object PRESENT = new Object(); + private transient HashMap map; + + public HashSet() { + map = new HashMap<>(); + } + + public boolean add(E e) { + // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 + return map.put(e, PRESENT)==null; + } + 复制代码 + +**hashCode()与equals()的相关规定**: + +1. 如果两个对象相等,则hashcode一定也是相同的 + * hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值 +2. 两个对象相等,对两个equals方法返回true +3. 两个对象有相同的hashcode值,它们也不一定是相等的 +4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 +5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 + +**==与equals的区别** + +1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 +2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 + +#### HashSet与HashMap的区别 + +> | HashMap | HashSet | +> | --- | --- | +> | 实现了Map接口 | 实现Set接口 | +> | 存储键值对 | 仅存储对象 | +> | 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 | +> | HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false | +> | HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 | + +## Map接口 + +### 什么是Hash算法 + +* 哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + +### 什么是链表 + +* 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。 + +* 链表大致分为单链表和双向链表 + + 1. 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e72891e5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + 2. 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e73f80b0~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 链表的优点 + + * 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素) + * 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间) + * 大小没有固定,拓展很灵活。 +* 链表的缺点 + + * 不能随机查找,必须从第一个开始遍历,查找效率低 + +### 说一下HashMap的实现原理? + +* HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 + +* HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 + +* HashMap 基于 Hash 算法实现的 + + 1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 + + 2. 存储时,如果出现hash值相同的key,此时有两种情况。 + + ​ (1)如果key相同,则覆盖原始值; + + ​ (2)如果key不同(出现冲突),则将当前的key-value放入链表中 + + 3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 + + 4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 + +* 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) + +### HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。**数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法**的方式可以解决哈希冲突。 + +#### HashMap JDK1.8之前 + +* JDK1.8之前采用的是拉链法。**拉链法**:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e78f59a7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### HashMap JDK1.8之后 + +* 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e7c6af15~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### JDK1.7 VS JDK1.8 比较 + +* JDK1.8主要解决或优化了一下问题: + 1. resize 扩容优化 + 2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考 + 3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 + +> | 不同 | JDK 1.7 | JDK 1.8 | +> | --- | --- | --- | +> | 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 | +> | 初始化方式 | 单独函数:`inflateTable()` | 直接集成到了扩容函数`resize()`中 | +> | hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 | +> | 存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 | +> | 插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) | +> | 扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) | + +### 什么是红黑树 + +#### 说道红黑树先讲什么是二叉树 + +* 二叉树简单来说就是 每一个节上可以关联俩个子节点 + + * 大概就是这样子: + a + / \ + b c + / \ / \ + d e f g + / \ / \ / \ / \ + h i j k l m n o + 复制代码 + +#### 红黑树 + +* 红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。 ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173552173a8a0c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]。 + +* 如果一个结点是红色的,则它的子结点必须是黑色的。 + +* 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!] + +* 红黑树的基本操作是**添加、删除**。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。 + +### HashMap的put方法的具体流程? + +* 当我们put的时候,首先计算 `key`的`hash`值,这里调用了 `hash`方法,`hash`方法实际是让`key.hashCode()`与`key.hashCode()>>>16`进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:**高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞**。按照函数注释,因为bucket数组大小是2的幂,计算下标`index = (table.length - 1) & hash`,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 + +* putVal方法执行流程图 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717355218a84ee7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} + +//实现Map.put和相关方法 +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // 步骤①:tab为空则创建 + // table未初始化或者长度为0,进行扩容 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 步骤②:计算index,并对null做处理 + // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 桶中已经存在元素 + else { + Node e; K k; + // 步骤③:节点key存在,直接覆盖value + // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // 将第一个元素赋值给e,用e来记录 + e = p; + // 步骤④:判断该链为红黑树 + // hash值不相等,即key不相等;为红黑树结点 + // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null + else if (p instanceof TreeNode) + // 放入树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // 步骤⑤:该链为链表 + // 为链表结点 + else { + // 在链表最末插入结点 + for (int binCount = 0; ; ++binCount) { + // 到达链表的尾部 + + //判断该链表尾部指针是不是空的 + if ((e = p.next) == null) { + // 在尾部插入新结点 + p.next = newNode(hash, key, value, null); + //判断链表的长度是否达到转化红黑树的临界值,临界值为8 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + //链表结构转树形结构 + treeifyBin(tab, hash); + // 跳出循环 + break; + } + // 判断链表中结点的key值与插入的元素的key值是否相等 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + // 相等,跳出循环 + break; + // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 + p = e; + } + } + //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值 + if (e != null) { + // 记录e的value + V oldValue = e.value; + // onlyIfAbsent为false或者旧值为null + if (!onlyIfAbsent || oldValue == null) + //用新值替换旧值 + e.value = value; + // 访问后回调 + afterNodeAccess(e); + // 返回旧值 + return oldValue; + } + } + // 结构性修改 + ++modCount; + // 步骤⑥:超过最大容量就扩容 + // 实际大小大于阈值则扩容 + if (++size > threshold) + resize(); + // 插入后回调 + afterNodeInsertion(evict); + return null; +} +复制代码 + +1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; +2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; +3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; +4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5; +5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; +6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 + +### HashMap的扩容操作是怎么实现的? + +1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容; + +2. 每次扩展的时候,都是扩展2倍; + +3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。 + +* 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上 + + final Node[] resize() { + Node[] oldTab = table;//oldTab指向hash桶数组 + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空 + if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值 + threshold = Integer.MAX_VALUE; + return oldTab;//返回 + }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold + } + // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂 + // 直接将该值赋给新的容量 + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75 + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 新的threshold = 新的cap * 0.75 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + // 计算出新的数组长度后赋给当前成员变量table + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap];//新建hash桶数组 + table = newTab;//将新数组的值复制给旧的hash桶数组 + // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散 + if (oldTab != null) { + // 遍历新数组的所有桶下标 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收 + oldTab[j] = null; + // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树 + if (e.next == null) + // 用同样的hash映射算法把该元素加入新的数组 + newTab[e.hash & (newCap - 1)] = e; + // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + // e是链表的头并且e.next!=null,那么处理链表中元素重排 + else { // preserve order + // loHead,loTail 代表扩容后不用变换下标,见注1 + Node loHead = null, loTail = null; + // hiHead,hiTail 代表扩容后变换下标,见注1 + Node hiHead = null, hiTail = null; + Node next; + // 遍历链表 + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead + // 代表下标保持不变的链表的头元素 + loHead = e; + else + // loTail.next指向当前e + loTail.next = e; + // loTail指向当前的元素e + // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时, + // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next..... + // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。 + loTail = e; + } + else { + if (hiTail == null) + // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素 + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; + } + 复制代码 + +### HashMap是怎么解决哈希冲突的? + +* 答:在解决这个问题之前,我们首先需要知道**什么是哈希冲突**,而在了解哈希冲突之前我们还要知道**什么是哈希**才行; + +#### 什么是哈希? + +* Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + +#### 什么是哈希冲突? + +* **当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)**。 + +#### HashMap的数据结构 + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。 + * 数组的特点是:寻址容易,插入和删除困难; + * 链表的特点是:寻址困难,但插入和删除容易; +* 所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521c92dc84~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; +* 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 +* **但相比于hashCode返回的int类型,我们HashMap初始的容量大小`DEFAULT_INITIAL_CAPACITY = 1 << 4`(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表**,所以我们还需要对hashCode作一定的优化 + +#### hash()函数 + +* 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于**参与运算的只有hashCode的低位**,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为**扰动**,在**JDK 1.8**中的hash()函数如下: + + static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) + } + 复制代码 +* 这比在**JDK 1.7**中,更为简洁,**相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)**; + +#### 总结 + +* 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的: + * 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; + * 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 + +### 能否使用任何类作为 Map 的 key? + +可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点: + +* 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。 + +* 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 + +* 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。 + +* 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。 + +### 为什么HashMap中String、Integer这样的包装类适合作为K? + +* 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 + * 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 + * 内部已重写了`equals()`、`hashCode()`等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况; + +### 如果使用Object作为HashMap的Key,应该怎么办呢? + +* 答:重写`hashCode()`和`equals()`方法 + 1. **重写`hashCode()`是因为需要计算存储数据的存储位置**,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; + 2. **重写`equals()`方法**,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,**目的是为了保证key在哈希表中的唯一性**; + +### HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标? + +* 答:`hashCode()`方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过`hashCode()`计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; + +* **那怎么解决呢?** + + 1. HashMap自己实现了自己的`hash()`方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; + + 2. 在保证数组长度为2的幂次方的时候,使用`hash()`运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题; + +### HashMap 的长度为什么是2的幂次方 + +* 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 + +* **这个算法应该如何设计呢?** + + * 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 +* **那为什么是两次扰动呢?** + + * 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的; + +### HashMap 与 HashTable 有什么区别? + +1. **线程安全**: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +2. **效率**: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +3. **对Null key 和Null value的支持**: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 +4. **初始容量大小和每次扩充容量大小的不同** : + 1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。 + 2. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 +5. **底层数据结构**: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 +6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 + +### 什么是TreeMap 简介 + +* TreeMap 是一个**有序的key-value集合**,它是通过红黑树实现的。 +* TreeMap基于**红黑树(Red-Black tree)实现**。该映射根据**其键的自然顺序进行排序**,或者根据**创建映射时提供的 Comparator 进行排序**,具体取决于使用的构造方法。 +* TreeMap是线程**非同步**的。 + +### 如何决定使用 HashMap 还是 TreeMap? + +* 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 + +### HashMap 和 ConcurrentHashMap 的区别 + +1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。) +2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。 + +### ConcurrentHashMap 和 Hashtable 的区别? + +* ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 + + * **底层数据结构**: JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; + * **实现线程安全的方式**: + 1. **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本; + 2. ② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 +* **两者的对比图**: + +##### 1、HashTable: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521ca71b79~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 2、 JDK1.7的ConcurrentHashMap: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521de4886d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点): + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735522b19186a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 + +### ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? + +#### JDK1.7 + +* 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 + +* 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下: + +* 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735524c5089b8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色; +2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。 + +#### JDK1.8 + +* 在**JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现**,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 + +* 结构如下: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173552564c22be~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* **附加源码,有需要的可以看看** + +* 插入元素过程(建议去看看源码): + +* 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据; + + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + if (casTabAt(tab, i, null, new Node(hash, key, value, null))) + break; // no lock when adding to empty bin + } + 复制代码 +* 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点; + + if (fh >= 0) { + binCount = 1; + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, value, null); + break; + } + } + } + 复制代码 + +1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值; +2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount; + +## 辅助工具类 + +### Array 和 ArrayList 有何区别? + +* Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。 +* Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。 +* Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。 + +`对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。` + +### 如何实现 Array 和 List 之间的转换? + +* Array 转 List: Arrays. asList(array) ; +* List 转 Array:List 的 toArray() 方法。 + +### comparable 和 comparator的区别? + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +? + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +* 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。 + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125939843079 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" new file mode 100644 index 0000000..e799b7b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" @@ -0,0 +1,676 @@ + + +## Linux 概述 + +### 什么是Linux + +* Linux是一套免费使用和自由传播的类似Unix操作系统,一般的WEB项目都是部署都是放在Linux操作系统上面。 Linux是一个基于POSIX和Unix的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的Unix工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d148acc2~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### Windows和Linux的区别 + +* Windows是微软开发的操作系统,民用操作系统,可用于娱乐、影音、上网。 Windows操作系统具有强大的日志记录系统和强大的桌面应用。好处是它可以帮我们实现非常多绚丽多彩的效果,可以非常方便去进行娱乐、影音、上网。 +* Linux的应用相对单纯很多,没有什么绚丽多彩的效果,因此Linux的性能是非常出色的,可以完全针对机器的配置有针对性的优化, +* 简单来说Windows适合普通用户进行娱乐办公使用,Linux适合软件开发部署 + +### Unix和Linux有什么区别? + +* Linux和Unix都是功能强大的操作系统,都是应用广泛的服务器操作系统,有很多相似之处,甚至有一部分人错误地认为Unix和Linux操作系统是一样的,然而,事实并非如此,以下是两者的区别。 + 1. 开源性 + Linux是一款开源操作系统,不需要付费,即可使用;Unix是一款对源码实行知识产权保护的传统商业软件,使用需要付费授权使用。 + 2. 跨平台性 + Linux操作系统具有良好的跨平台性能,可运行在多种硬件平台上;Unix操作系统跨平台性能较弱,大多需与硬件配套使用。 + 3. 可视化界面 + Linux除了进行命令行操作,还有窗体管理系统;Unix只是命令行下的系统。 + 4. 硬件环境 + Linux操作系统对硬件的要求较低,安装方法更易掌握;Unix对硬件要求比较苛刻,按照难度较大。 + 5. 用户群体 + Linux的用户群体很广泛,个人和企业均可使用;Unix的用户群体比较窄,多是安全性要求高的大型企业使用,如银行、电信部门等,或者Unix硬件厂商使用,如Sun等。 + 相比于Unix操作系统,Linux操作系统更受广大计算机爱好者的喜爱,主要原因是Linux操作系统具有Unix操作系统的全部功能,并且能够在普通PC计算机上实现全部的Unix特性,开源免费的特性,更容易普及使用! + +### 什么是 Linux 内核? + +* Linux 系统的核心是内核。内核控制着计算机系统上的所有硬件和软件,在必要时分配硬件,并根据需要执行软件。 + 1. 系统内存管理 + 2. 应用程序管理 + 3. 硬件设备管理 + 4. 文件系统管理 + +### Linux的基本组件是什么? + +* 就像任何其他典型的操作系统一样,Linux拥有所有这些组件:内核,shell和GUI,系统实用程序和应用程序。Linux比其他操作系统更具优势的是每个方面都附带其他功能,所有代码都可以免费下载。 + +### Linux 的体系结构 + +* 从大的方面讲,Linux 体系结构可以分为两块: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d1cc127a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 用户空间(User Space) :用户空间又包括用户的应用程序(User Applications)、C 库(C Library) 。 +* 内核空间(Kernel Space) :内核空间又包括系统调用接口(System Call Interface)、内核(Kernel)、平台架构相关的代码(Architecture-Dependent Kernel Code) 。 + +**为什么 Linux 体系结构要分为用户空间和内核空间的原因?** + +* 1、现代 CPU 实现了不同的工作模式,不同模式下 CPU 可以执行的指令和访问的寄存器不同。 +* 2、Linux 从 CPU 的角度出发,为了保护内核的安全,把系统分成了两部分。 + +* 用户空间和内核空间是程序执行的**两种不同的状态**,我们可以通过两种方式完成用户空间到内核空间的转移: + * 系统调用; + * 硬件中断。 + +### BASH和DOS之间的基本区别是什么? + +* BASH和DOS控制台之间的主要区别在于3个方面: + * BASH命令区分大小写,而DOS命令则不区分; + * 在BASH下,/ character是目录分隔符,\作为转义字符。在DOS下,/用作命令参数分隔符,\是目录分隔符 + * DOS遵循命名文件中的约定,即8个字符的文件名后跟一个点,扩展名为3个字符。BASH没有遵循这样的惯例。 + +### Linux 开机启动过程? + +> 了解即可。 + +* 1、主机加电自检,加载 BIOS 硬件信息。 + +* 2、读取 MBR 的引导文件(GRUB、LILO)。 + +* 3、引导 Linux 内核。 + +* 4、运行第一个进程 init (进程号永远为 1 )。 + +* 5、进入相应的运行级别。 + +* 6、运行终端,输入用户名和密码。 + +### Linux系统缺省的运行级别? + +* 关机。 +* 单机用户模式。 +* 字符界面的多用户模式(不支持网络)。 +* 字符界面的多用户模式。 +* 未分配使用。 +* 图形界面的多用户模式。 +* 重启。 + +### Linux 使用的进程间通信方式? + +> 了解即可,不需要太深入。 + +* 1、管道(pipe)、流管道(s_pipe)、有名管道(FIFO)。 +* 2、信号(signal) 。 +* 3、消息队列。 +* 4、共享内存。 +* 5、信号量。 +* 6、套接字(socket) 。 + +### Linux 有哪些系统日志文件? + +* 比较重要的是 `/var/log/messages` 日志文件。 + +> 该日志文件是许多进程日志文件的汇总,从该文件可以看出任何入侵企图或成功的入侵。 +> +> 另外,如果胖友的系统里有 ELK 日志集中收集,它也会被收集进去。 + +### Linux系统安装多个桌面环境有帮助吗? + +* 通常,一个桌面环境,如KDE或Gnome,足以在没有问题的情况下运行。尽管系统允许从一个环境切换到另一个环境,但这对用户来说都是优先考虑的问题。有些程序在一个环境中工作而在另一个环境中无法工作,因此它也可以被视为选择使用哪个环境的一个因素。 + +### 什么是交换空间? + +* 交换空间是Linux使用的一定空间,用于临时保存一些并发运行的程序。当RAM没有足够的内存来容纳正在执行的所有程序时,就会发生这种情况。 + +### 什么是root帐户 + +* root帐户就像一个系统管理员帐户,允许你完全控制系统。你可以在此处创建和维护用户帐户,为每个帐户分配不同的权限。每次安装Linux时都是默认帐户。 + +### 什么是LILO? + +* LILO是Linux的引导加载程序。它主要用于将Linux操作系统加载到主内存中,以便它可以开始运行。 + +### 什么是BASH? + +* BASH是Bourne Again SHell的缩写。它由Steve Bourne编写,作为原始Bourne Shell(由/ bin / sh表示)的替代品。它结合了原始版本的Bourne Shell的所有功能,以及其他功能,使其更容易使用。从那以后,它已被改编为运行Linux的大多数系统的默认shell。 + +### 什么是CLI? + +* **命令行界面**(英语**:command-line interface**,缩写]**:CLI**)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为**字符用户界面**(CUI)。 + +* 通常认为,命令行界面(CLI)没有图形用户界面(GUI)那么方便用户操作。因为,命令行界面的软件通常需要用户记忆操作的命令,但是,由于其本身的特点,命令行界面要较图形用户界面节约计算机系统的资源。在熟记命令的前提下,使用命令行界面往往要较使用图形用户界面的操作速度要快。所以,图形用户界面的操作系统中,都保留着可选的命令行界面。 + +### 什么是GUI? + +* 图形用户界面(Graphical User Interface,简称 GUI,又称图形用户接口)是指采用图形方式显示的计算机操作用户界面。 + +* 图形用户界面是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其它一些日常任务。与通过键盘输入文本或字符命令来完成例行任务的字符界面相比,图形用户界面有许多优点。 + +### 开源的优势是什么? + +* 开源允许你将软件(包括源代码)免费分发给任何感兴趣的人。然后,人们可以添加功能,甚至可以调试和更正源代码中的错误。它们甚至可以让它运行得更好,然后再次自由地重新分配这些增强的源代码。这最终使社区中的每个人受益。 + +### GNU项目的重要性是什么? + +* 这种所谓的自由软件运动具有多种优势,例如可以自由地运行程序以及根据你的需要自由学习和修改程序。它还允许你将软件副本重新分发给其他人,以及自由改进软件并将其发布给公众。 + +## 磁盘、目录、文件 + +### 简单 Linux 文件系统? + +**在 Linux 操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。** + +* 也就是说在 Linux 系统中有一个重要的概念**:一切都是文件**。其实这是 Unix 哲学的一个体现,而 Linux 是重写 Unix 而来,所以这个概念也就传承了下来。在 Unix 系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。 + +* Linux 支持 5 种文件类型,如下图所示: + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d70c1faf~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### Linux 的目录结构是怎样的? + +> 这个问题,一般不会问。更多是实际使用时,需要知道。 + +* Linux 文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录: + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d6e0c867~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**常见目录说明**: + +> | 目录 | 介绍 | +> | --- | --- | +> | /bin | 存放二进制可执行文件(ls,cat,mkdir等),常用命令一般都在这里; | +> | /etc | 存放系统管理和配置文件; | +> | /home | 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是/home/user,可以用~user表示; | +> | /usr | 用于存放系统应用程序; | +> | /opt | 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把tomcat等都安装到这里; | +> | /proc | 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息; | +> | /root | 超级用户(系统管理员)的主目录(特权阶级); | +> | /sbin | 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如ifconfig等; | +> | /dev | 用于存放设备文件; | +> | /mnt | 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统; | +> | /boot | 存放用于系统引导时使用的各种文件; | +> | /lib | 存放着和系统运行相关的库文件 ; | +> | /tmp | 用于存放各种临时文件,是公用的临时文件存储点; | +> | /var | 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等; | +> | /lost+found | 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里 | + +### 什么是 inode ? + +> 一般来说,面试不会问 inode 。但是 inode 是一个重要概念,是理解 Unix/Linux 文件系统和硬盘储存的基础。 + +* 理解inode,要从文件储存说起。 + +* 文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。 + +* 操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。 + +* 文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。 + +* 每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。 + +**简述 Linux 文件系统通过 i 节点把文件的逻辑结构和物理结构转换的工作过程?** + +> 如果看的一脸懵逼,也没关系。一般来说,面试官不太会问这个题目。 + +* Linux 通过 inode 节点表将文件的逻辑结构和物理结构进行转换。 + * inode 节点是一个 64 字节长的表,表中包含了文件的相关信息,其中有文件的大小、文件所有者、文件的存取许可方式以及文件的类型等重要信息。在 inode 节点表中最重要的内容是磁盘地址表。在磁盘地址表中有 13 个块号,文件将以块号在磁盘地址表中出现的顺序依次读取相应的块。 + * Linux 文件系统通过把 inode 节点和文件名进行连接,当需要读取该文件时,文件系统在当前目录表中查找该文件名对应的项,由此得到该文件相对应的 inode 节点号,通过该 inode 节点的磁盘地址表把分散存放的文件物理块连接成文件的逻辑结构。 + +### 什么是硬链接和软链接? + +* **硬链接**:由于 Linux 下的文件是通过索引节点(inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配 inode 。每添加一个一个硬链接,文件的链接数就加 1 。 + + * 不足: + 1. 不可以在不同文件系统的文件间建立链接; + 2. 只有超级用户才可以为目录创建硬链接。 +* **软链接**:软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。 + + * 不足:因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;还有它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。 +* **实际场景下,基本是使用软链接**。总结区别如下: + + * 硬链接不可以跨分区,软件链可以跨分区。 + * 硬链接指向一个 inode 节点,而软链接则是创建一个新的 inode 节点。 + * 删除硬链接文件,不会删除原文件,删除软链接文件,会把原文件删除。 + +### RAID 是什么? + +> RAID 全称为独立磁盘冗余阵列(Redundant Array of Independent Disks),基本思想就是把多个相对便宜的硬盘组合起来,成为一个硬盘阵列组,使性能达到甚至超过一个价格昂贵、 容量巨大的硬盘。RAID 通常被用在服务器电脑上,使用完全相同的硬盘组成一个逻辑扇区,因此操作系统只会把它当做一个硬盘。 +> +> RAID 分为不同的等级,各个不同的等级均在数据可靠性及读写性能上做了不同的权衡。在实际应用中,可以依据自己的实际需求选择不同的 RAID 方案。 + +* 当然,因为很多公司都使用云服务,大家很难接触到 RAID 这个概念,更多的可能是普通云盘、SSD 云盘酱紫的概念。 + +## 安全 + +### 一台 Linux 系统初始化环境后需要做一些什么安全工作? + +* 1、添加普通用户登陆,禁止 root 用户登陆,更改 SSH 端口号。 + + > 修改 SSH 端口不一定绝对哈。当然,如果要暴露在外网,建议改下。l + +* 2、服务器使用密钥登陆,禁止密码登陆。 + +* 3、开启防火墙,关闭 SElinux ,根据业务需求设置相应的防火墙规则。 + +* 4、装 fail2ban 这种防止 SSH 暴力破击的软件。 + +* 5、设置只允许公司办公网出口 IP 能登陆服务器(看公司实际需要) + + > 也可以安装 VPN 等软件,只允许连接 VPN 到服务器上。 + +* 6、修改历史命令记录的条数为 10 条。 + +* 7、只允许有需要的服务器可以访问外网,其它全部禁止。 + +* 8、做好软件层面的防护。 + + * 8.1 设置 nginx_waf 模块防止 SQL 注入。 + * 8.2 把 Web 服务使用 www 用户启动,更改网站目录的所有者和所属组为 www 。 + +### 什么叫 CC 攻击?什么叫 DDOS 攻击? + +* CC 攻击,主要是用来攻击页面的,模拟多个用户不停的对你的页面进行访问,从而使你的系统资源消耗殆尽。 + +* DDOS 攻击,中文名叫分布式拒绝服务攻击,指借助服务器技术将多个计算机联合起来作为攻击平台,来对一个或多个目标发动 DDOS 攻击。 + + > 攻击,即是通过大量合法的请求占用大量网络资源,以达到瘫痪网络的目的。 + +**怎么预防 CC 攻击和 DDOS 攻击?** + +* 防 CC、DDOS 攻击,这些只能是用硬件防火墙做流量清洗,将攻击流量引入黑洞。 + +> 流量清洗这一块,主要是买 ISP 服务商的防攻击的服务就可以,机房一般有空余流量,我们一般是买服务,毕竟攻击不会是持续长时间。 + +### 什么是网站数据库注入? + +* 由于程序员的水平及经验参差不齐,大部分程序员在编写代码的时候,没有对用户输入数据的合法性进行判断。 +* 应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的 SQL 注入。 +* SQL注入,是从正常的 WWW 端口访问,而且表面看起来跟一般的 Web 页面访问没什么区别,如果管理员没查看日志的习惯,可能被入侵很长时间都不会发觉。 + +**如何过滤与预防?** + +* 数据库网页端注入这种,可以考虑使用 nginx_waf 做过滤与预防。 + +### Shell 脚本是什么? + +* 一个 Shell 脚本是一个文本文件,包含一个或多个命令。作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以添加这些所有命令在一个文本文件(Shell 脚本)来完成这些日常工作任务。 + +## 实战 + +### 如何选择 Linux 操作系统版本? + +**一般来讲,桌面用户首选 Ubuntu ;服务器首选 RHEL 或 CentOS ,两者中首选 CentOS 。** + +* 根据具体要求: + + * 安全性要求较高,则选择 Debian 或者 FreeBSD 。 + + * 需要使用数据库高级服务和电子邮件网络应用的用户可以选择 SUSE 。 + + * 想要新技术新功能可以选择 Feddora ,Feddora 是 RHEL 和 CentOS 的一个测试版和预发布版本。 + + * 【重点】**根据现有状况,绝大多数互联网公司选择 CentOS 。现在比较常用的是 6 系列,现在市场占有大概一半左右。另外的原因是 CentOS 更侧重服务器领域,并且无版权约束**。 + + > CentOS 7 系列,也慢慢使用的会比较多了。 + +### 如何规划一台 Linux 主机,步骤是怎样? + +* 1、确定机器是做什么用的,比如是做 WEB 、DB、还是游戏服务器。 + + > 不同的用途,机器的配置会有所不同。 + +* 2、确定好之后,就要定系统需要怎么安装,默认安装哪些系统、分区怎么做。 + +* 3、需要优化系统的哪些参数,需要创建哪些用户等等的。 + +### 请问当用户反馈网站访问慢,你会如何处理? + +**有哪些方面的因素会导致网站网站访问慢?** + +* 1、服务器出口带宽不够用 + + > * 本身服务器购买的出口带宽比较小。一旦并发量大的话,就会造成分给每个用户的出口带宽就小,访问速度自然就会慢。 + > * 跨运营商网络导致带宽缩减。例如,公司网站放在电信的网络上,那么客户这边对接是长城宽带或联通,这也可能导致带宽的缩减。 + +* 2、服务器负载过大,导致响应不过来 + + > 可以从两个方面入手分析: + > + > * 分析系统负载,使用 w 命令或者 uptime 命令查看系统负载。如果负载很高,则使用 top 命令查看 CPU ,MEM 等占用情况,要么是 CPU 繁忙,要么是内存不够。 + > * 如果这二者都正常,再去使用 sar 命令分析网卡流量,分析是不是遭到了攻击。一旦分析出问题的原因,采取对应的措施解决,如决定要不要杀死一些进程,或者禁止一些访问等。 + +* 3、数据库瓶颈 + + > * 如果慢查询比较多。那么就要开发人员或 DBA 协助进行 SQL 语句的优化。 + > * 如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等。然后,也可以搭建 MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。 + +* 4、网站开发代码没有优化好 + + > * 例如 SQL 语句没有优化,导致数据库读写相当耗时。 + +**针对网站访问慢,怎么去排查?** + +* 1、首先要确定是用户端还是服务端的问题。当接到用户反馈访问慢,那边自己立即访问网站看看,如果自己这边访问快,基本断定是用户端问题,就需要耐心跟客户解释,协助客户解决问题。 + + > 不要上来就看服务端的问题。一定要从源头开始,逐步逐步往下。 + +* 2、如果访问也慢,那么可以利用浏览器的调试功能,看看加载那一项数据消耗时间过多,是图片加载慢,还是某些数据加载慢。 + +* 3、针对服务器负载情况。查看服务器硬件(网络、CPU、内存)的消耗情况。如果是购买的云主机,比如阿里云,可以登录阿里云平台提供各方面的监控,比如 CPU、内存、带宽的使用情况。 + +* 4、如果发现硬件资源消耗都不高,那么就需要通过查日志,比如看看 MySQL慢查询的日志,看看是不是某条 SQL 语句查询慢,导致网站访问慢。 + +**怎么去解决?** + +* 1、如果是出口带宽问题,那么久申请加大出口带宽。 +* 2、如果慢查询比较多,那么就要开发人员或 DBA 协助进行 SQL 语句的优化。 +* 3、如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等等。然后也可以搭建MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。 +* 4、申请购买 CDN 服务,加载用户的访问。 +* 5、如果访问还比较慢,那就需要从整体架构上进行优化咯。做到专角色专用,多台服务器提供同一个服务。 + +### Linux 性能调优都有哪几种方法? + +* 1、Disabling daemons (关闭 daemons)。 +* 2、Shutting down the GUI (关闭 GUI)。 +* 3、Changing kernel parameters (改变内核参数)。 +* 4、Kernel parameters (内核参数)。 +* 5、Tuning the processor subsystem (处理器子系统调优)。 +* 6、Tuning the memory subsystem (内存子系统调优)。 +* 7、Tuning the file system (文件系统子系统调优)。 +* 8、Tuning the network subsystem(网络子系统调优)。 + +## 基本命令 + +###### cd (change directory:英文释义是改变目录)切换目录 + +cd ../ ;跳到上级目录 +cd /opt ;不管现在到那直接跳到指定的opt文件夹中 +cd ~ ;切换当前用户的家目录。root用户的家目录就是root目录。 +复制代码 + +###### pwd (print working directory:显示当前工作目录的绝对路径) + +pwd +显示当前的绝对路劲 +复制代码 + +###### ls (ls:list的缩写,查看列表)查看当前目录下的所有文件夹(ls 只列出文件名或目录名) + +ls -a ;显示所有文件夹,隐藏文件也显示出来 +ls -R ;连同子目录一起列出来 +复制代码 + +###### ll (ll:list的缩写,查看列表详情)查看当前目录下的所有详细信息和文件夹(ll 结果是详细,有时间,是否可读写等信息) + +ll -a ;显示所有文件,隐藏文件也显示出来 +ll -R ;连同子目录内容一起列出来 +ll -h ;友好展示详情信息,可以看大小 +ll -al ;即能显示隐藏文件又能显示详细列表。 +复制代码 + +###### touch (touch:创建文件)创建文件 + +touch test.txt ;创建test.txt文件 +touch /opt/java/test.java ;在指定目录创建test.java文件 +复制代码 + +###### mkdir (mkdir:创建目录) 创建目录 + +mkdir 文件夹名称 ;在此目录创建文件夹 +mkdir /opt/java/jdk ;在指定目录创建文件夹 +复制代码 + +###### cat (concatenate:显示或把多个文本文件连接起来)查看文件命令(可以快捷查看当前文件的内容)(不能快速定位到最后一页) + +cat lj.log ;快捷查看文件命令 +Ctrl + c ;暂停显示文件 +Ctrl + d ;退出查看文件命令 +复制代码 + +###### more (more:更多的意思)分页查看文件命令(不能快速定位到最后一页) + +回车:向下n行,需要定义,默认为1行。 +空格键:向下滚动一屏或Ctrl+F +B:返回上一层或Ctrl+B +q:退出more +复制代码 + +###### less (lese:较少的意思)分页查看文件命令(可以快速定位到最后一页) + +less -m 显示类似于more命令的百分比。 +less -N 显示每行的行号。(大写的N) +两参数一起使用如:less -mN 文件名,如此可分页并显示行号。 + +空格键:前下一页或page down。 +回车:向下一行。 +b:后退一页 或 page up。 +q:退出。 +d:前进半页。 +u:后退半页 +复制代码 + +###### tail(尾巴) 查看文件命令(看最后多少行) + +tail -10 ;文件名 看最后10行 +复制代码 + +###### cp(copy单词缩写,复制功能) + +cp /opt/java/java.log /opt/logs/ ;把java.log 复制到/opt/logs/下 +cp /opt/java/java.log /opt/logs/aaa.log ;把java.log 复制到/opt/logs/下并且改名为aaa.log +cp -r /opt/java /opt/logs ;把文件夹及内容复制到logs文件中 +复制代码 + +###### mv(move单词缩写,移动功能,该文件名称功能) + +mv /opt/java/java.log /opt/mysql/ ;移动文件到mysql目录下 +mv java.log mysql.log ;把java.log改名为mysql.log +复制代码 + +###### rm(remove:移除的意思)删除文件,或文件夹 + +-f或--force 强制删除文件或目录。删除文件不包括文件夹的文件 +-r或-R或--recursive 递归处理,将指定目录下的所有文件及子目录一并删除。 +-rf 强制删除文件夹及内容 + +rm 文件名 ;安全删除命令 (yes删除 no取消) +rm -rf 强制删除文件夹及内容 +rm -rf * 删除当前目录下的所有内容。 +rm -rf /* 删除Linux系统根目录下所有的内容。系统将完蛋。 +复制代码 + +###### find (find:找到的意思)查找指定文件或目录 + +* 表示0~多个任意字符。 + +find -name 文件名;按照指定名称查找在当前目录下查找文件 +find / -name 文件名按照指定名称全局查找文件 +find -name '*文件名' ;任意前缀加上文件名在当前目录下查找文件 +find / -name '*文件名*' ;全局进行模糊查询带文件名的文件 +复制代码 + +###### vi (VIsual:视觉)文本编辑器 类似win的记事本 (操作类似于地下的vim命令,看底下vim 的操作) + +###### vim (VI IMproved:改进版视觉)改进版文本编辑器 (不管是文件查看还是文件编辑 按 Shift + 上或者下可以上下移动查看视角) + +输入”vim 文件名” 打开文件,刚刚时是”一般模式”。 + +一般模式:可以浏览文件内容,可以进行文本快捷操作。如单行复制,多行复制,单行删除,多行删除,(退出)等。 +插入模式:可以编辑文件内容。 +底行模式:可以进行强制退出操作,不保存 :q! + 可以进行保存并退出操作 :wq + +按下”i”或”a”或”o”键,从”一般模式”,进入”插入模式(编辑模式)”。 +在编辑模式下按”Esc” 即可到一般模式 +在一般模式下按”:”,冒号进入底行模式。 + +在一般模式下的快捷键 + dd ;删除一整行 + X ;向前删除 等同于windowns系统中的删除键 + x ;向后删除和大写x相反方向 + Ctrl + f ;向后看一页 + Ctrl + b ;向前看一页 + u ;撤销上一步操作 + /word ;向下查找word关键字 输入:n查找下一个,N查找上一个(不管是哪个查找都是全局查找 只不过n的方向相反) + ?log ;向上查找log关键字 输入:n查找上一个,N查找下一个 + :1,90s/redis/Redis/g ;把1-90行的redis替换为Redis。语法n1,n2s/原关键字/新关键字/g,n1代表其实行,n2代表结尾行,g是必须要的 + :0 ;光标移动到第一行 + :$ ;光标移动到最后一行 + :300 ;光标移动到300行,输入多少数字移动到多少行 + :w ;保存 + :w! ;强制保存 + :q ;退出 + :q! ;强制退出 + 5dd ;删除后面5行,打一个参数为自己填写 + 5x ;删除此光标后面5个字符 + d1G ;删除此光标之前的所有 + d0 ;从光标当前位置删除到此行的第一个位置 + yy ;复制 + p ;在光标的下面进行粘贴 + P ;在光标的上门进行粘贴 +复制代码 + +###### | 管道命令(把多个命令组合起来使用) + +管道命令的语法:命令1 | 命令2 | 命令3。 +复制代码 + +###### grep (grep :正则表达式)正则表达式,用于字符串的搜索工作(模糊查询)。不懂可以先过 + +单独使用: +grep String test.java ;在test.java文件中查找String的位置,返回整行 +一般此命令不会单独使用下面列几个常用的命令(地下通过管道命令组合起来使用) + +ps aux|grep java ;查找带java关键字的进程 +ll |grep java ;查找带java关键字的文件夹及文件 +复制代码 + +###### yum install -y lrzsz 命令(实现win到Linux文件互相简单上传文件) + +#(实际上就是在Linux系统中下载了一个插件)下了了此安装包后就可以实现win系统到linux之间拉文件拉文件 +#等待下载完了就可以输入: + +rz 从win系统中选择文件上传到Linux系统中 + +sz 文件名 选择Linux系统的文件复制到win系统中 + +复制代码 + +###### tar (解压 压缩 命令) + +常用的组合命令: +-z 是否需要用gzip压缩。 +-c 建立一个压缩文件的参数指令(create) –压缩 + -x 解开一个压缩文件的参数指令(extract) –解压 + -v 压缩的过程中显示文件(verbose) + -f 使用档名,在f之后要立即接档中(file) + 常用解压参数组合:zxvf + 常用压缩参数组合:zcvf + +解压命令: +tar -zxvf redis-3.2.8.tar.gz ;解压到当前文件夹 +tar -zxvf redis-3.2.8.tar.gz -C /opt/java/ ;解压到指定目录 + +压缩命令:(注意 语法有点反了,我反正每次都搞反) +tar -zcvf redis-3.2.8.tar.gz redis-3.2.8/ ;语法 tar -zcvf 压缩后的名称 要压缩的文件 +tar -zcvf 压缩后的文件(可指定目录) 要压缩的文件(可指定目录) +复制代码 + +###### ps (process status:进程状态,类似于windows的任务管理器) + +常用组合:ps -ef 标准的格式查看系统进程 + ps -aux BSD格式查看系统进程 + ps -aux|grep redis BSD格式查看进程名称带有redis的系统进程(常用技巧) +//显示进程的一些属性,需要了解(ps aux) +USER //用户名 +PID //进程ID号,用来杀死进程的 +%CPU //进程占用的CPU的百分比 +%MEM //占用内存的的百分比 +VSZ //该进程使用的虚拟內存量(KB) +RSS //该进程占用的固定內存量(KB) +STAT //进程的状态 +START //该进程被触发启动时间 +TIME //该进程实际使用CPU运行的时间 +复制代码 + +###### clear 清屏命令。(强迫症患者使用) + +kill 命令用来中止一个进程。(要配合ps命令使用,配合pid关闭进程) +(ps类似于打开任务管理器,kill类似于关闭进程) + kill -5 进程的PID ;推荐,和平关闭进程 + kill -9 PID ;不推荐,强制杀死进程 +复制代码 + +###### ifconfig命令 + +用于查看和更改网络接口的地址和参数,包括IP地址、网络掩码、广播地址,使用权限是超级用户。(一般是用来查看的,很少更改) +如果此命令输入无效,先输入yum -y install net-tools +ifconfig +复制代码 + +###### ping (用于检测与目标的连通性)语法:ping ip地址 + +测试: +1、在Windows操作系统中cmdipconfig,查看本机IP地址: +2、再到LInux系统中输入 ping ip地址 +(公司电脑,我就不暴露Ip了,没图片 自己去试) +按Ctrl + C 可以停止测试。 +复制代码 + +###### free 命令 (显示系统内存) + +#显示系统内存使用情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。 +-b 以Byte显示内存使用情况 +-k 以kb为单位显示内存使用情况 +-m 以mb为单位显示内存使用情况 +-g 以gb为单位显示内存使用情况 +-s<间隔秒数> 持续显示内存 +-t 显示内存使用总合 +复制代码 + +###### top 命令 + +#显示当前系统正在执行的进程的相关信息,包括进程 ID、内存占用率、CPU 占用率等 +-c 显示完整的进程命令 +-s 保密模式 +-p <进程号> 指定进程显示 +-n <次数>循环显示次数 +复制代码 + +###### netstat 命令 + +#Linux netstat命令用于显示网络状态。 +#利用netstat指令可让你得知整个Linux系统的网络情况。 +#语法: +netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] +复制代码 + +###### file (可查看文件类型) + +file 文件名 +复制代码 + +###### 重启linux + +Linux centos 重启命令:reboot +复制代码 + +###### 关机linux + +Linux centos 关机命令:halt + +复制代码 + +###### 同步时间命令 + +ntpdate ntp1.aliyun.com +复制代码 + +###### 更改为北京时间命令 + +rm -rf /etc/localtime +ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +复制代码 + +###### 查看时间命令: + +date + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127059738637 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" new file mode 100644 index 0000000..a1d00e1 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" @@ -0,0 +1,516 @@ + + +### 什么是Nginx? + +* Nginx是一个 轻量级/高性能的反向代理Web服务器,他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发,现在中国使用nginx网站用户有很多,例如:新浪、网易、 腾讯等。 + +### 为什么要用Nginx? + +* 跨平台、配置简单、方向代理、高并发连接:处理2-3万并发连接数,官方监测能支持5万并发,内存消耗小:开启10个nginx才占150M内存 ,nginx处理静态文件好,耗费内存少, + +* 而且Nginx内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。 + +* 使用Nginx的话还能: + + 1. 节省宽带:支持GZIP压缩,可以添加浏览器本地缓存 + 2. 稳定性高:宕机的概率非常小 + 3. 接收用户请求是异步的 + +### 为什么Nginx性能这么高? + +* 因为他的事件处理机制:异步非阻塞事件处理机制:运用了epoll模型,提供了一个队列,排队解决 + +### Nginx怎么处理请求的? + +* nginx接收一个请求后,首先由listen和server_name指令匹配server模块,再匹配server模块里的location,location就是实际地址 + +```java +server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.htm; # 默认的首页文件,多个用空格分开 + } # 第一个location区块结果 + } +``` +### 什么是正向代理和反向代理? + +1. 正向代理就是一个人发送一个请求直接就到达了目标的服务器 +2. 反方代理就是请求统一被Nginx接收,nginx反向代理服务器接收到之后,按照一定的规 则分发给了后端的业务处理服务器进行处理了 + +### 使用“反向代理服务器的优点是什么? + +* 反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和web服务器之间的中间层。这对于安全方面来说是很好的,特别是当您使用web托管服务时。 + +### Nginx的优缺点? + +* 优点: + + 1. 占内存小,可实现高并发连接,处理响应快 + 2. 可实现http服务器、虚拟主机、方向代理、负载均衡 + 3. Nginx配置简单 + 4. 可以不暴露正式的服务器IP地址 +* 缺点: 动态处理差:nginx处理静态文件好,耗费内存少,但是处理动态页面则很鸡肋,现在一般前端用nginx作为反向代理抗住压力, + +### Nginx应用场景? + +1. http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。 +2. 虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。 +3. 反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。 +4. nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。 + +### Nginx目录结构有哪些? + +```java +[root@localhost ~]# tree /usr/local/nginx +/usr/local/nginx +├── client_body_temp +├── conf # Nginx所有配置文件的目录 +│ ├── fastcgi.conf # fastcgi相关参数的配置文件 +│ ├── fastcgi.conf.default # fastcgi.conf的原始备份文件 +│ ├── fastcgi_params # fastcgi的参数文件 +│ ├── fastcgi_params.default +│ ├── koi-utf +│ ├── koi-win +│ ├── mime.types # 媒体类型 +│ ├── mime.types.default +│ ├── nginx.conf # Nginx主配置文件 +│ ├── nginx.conf.default +│ ├── scgi_params # scgi相关参数文件 +│ ├── scgi_params.default +│ ├── uwsgi_params # uwsgi相关参数文件 +│ ├── uwsgi_params.default +│ └── win-utf +├── fastcgi_temp # fastcgi临时数据目录 +├── html # Nginx默认站点目录 +│ ├── 50x.html # 错误页面优雅替代显示文件,例如当出现502错误时会调用此页面 +│ └── index.html # 默认的首页文件 +├── logs # Nginx日志目录 +│ ├── access.log # 访问日志文件 +│ ├── error.log # 错误日志文件 +│ └── nginx.pid # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件 +├── proxy_temp # 临时目录 +├── sbin # Nginx命令目录 +│ └── nginx # Nginx的启动命令 +├── scgi_temp # 临时目录 +└── uwsgi_temp # 临时目录 +``` +### Nginx配置文件nginx.conf有哪些属性模块? + +```java +worker_processes 1; # worker进程的数量 +events { # 事件区块开始 + worker_connections 1024; # 每个worker进程支持的最大连接数 +} # 事件区块结束 +http { # HTTP区块开始 + include mime.types; # Nginx支持的媒体类型库文件 + default_type application/octet-stream; # 默认的媒体类型 + sendfile on; # 开启高效传输模式 + keepalive_timeout 65; # 连接超时 + server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.htm; # 默认的首页文件,多个用空格分开 + } # 第一个location区块结果 + error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户 + location = /50x.html { # location区块开始,访问50x.html + root html; # 指定对应的站点目录为html + } + } + ...... +``` +### Nginx静态资源? + +* 静态资源访问,就是存放在nginx的html页面,我们可以自己编写 + +### 如何用Nginx解决前端跨域问题? + +* 使用Nginx转发请求。把跨域的接口写成调本域的接口,然后将这些接口转发到真正的请求地址。 + +### Nginx虚拟主机怎么配置? + +* 1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 + +* 2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台 + +* 3、基于ip的虚拟主机。 + +#### 基于虚拟主机配置域名 + +* 需要建立/data/www /data/bbs目录,windows本地hosts添加虚拟机ip地址对应的域名解析;对应域名网站目录下新增index.html文件; + +```java +#当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/www目录下文件 + server { + listen 80; + server_name www.lijie.com; + location / { + root data/www; + index index.html index.htm; + } + } + + #当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/bbs目录下文件 + server { + listen 80; + server_name bbs.lijie.com; + location / { + root data/bbs; + index index.html index.htm; + } + } +``` +#### 基于端口的虚拟主机 + +* 使用端口来区分,浏览器使用域名或ip地址:端口号 访问 + +```java +#当客户端访问www.lijie.com,监听端口号为8080,直接跳转到data/www目录下文件 + server { + listen 8080; + server_name 8080.lijie.com; + location / { + root data/www; + index index.html index.htm; + } + } + + #当客户端访问www.lijie.com,监听端口号为80直接跳转到真实ip服务器地址 127.0.0.1:8080 + server { + listen 80; + server_name www.lijie.com; + location / { + proxy_pass http://127.0.0.1:8080; + index index.html index.htm; + } + } +``` +### location的作用是什么? + +* location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。 + +#### location的语法能说出来吗? + +> 注意:~ 代表自己输入的英文字母 +> +> | 匹配符 | 匹配规则 | 优先级 | +> | --- | --- | --- | +> | = | 精确匹配 | 1 | +> | ^~ | 以某个字符串开头 | 2 | +> | ~ | 区分大小写的正则匹配 | 3 | +> | ~* | 不区分大小写的正则匹配 | 4 | +> | !~ | 区分大小写不匹配的正则 | 5 | +> | !~* | 不区分大小写不匹配的正则 | 6 | +> | / | 通用匹配,任何请求都会匹配到 | 7 | + +#### Location正则案例 + +* 示例: + +```java +#优先级1,精确匹配,根路径 + location =/ { + return 400; + } + + #优先级2,以某个字符串开头,以av开头的,优先匹配这里,区分大小写 + location ^~ /av { + root /data/av/; + } + + #优先级3,区分大小写的正则匹配,匹配/media*****路径 + location ~ /media { + alias /data/static/; + } + + #优先级4 ,不区分大小写的正则匹配,所有的****.jpg|gif|png 都走这里 + location ~* .*\.(jpg|gif|png|js|css)$ { + root /data/av/; + } + + #优先7,通用匹配 + location / { + return 403; + } +``` +### 限流怎么做的? + +* Nginx限流就是限制用户请求速度,防止服务器受不了 + +* 限流有3种 + + 1. 正常限制访问频率(正常流量) + 2. 突发限制访问频率(突发流量) + 3. 限制并发连接数 +* Nginx的限流都是基于漏桶流算法,底下会说道什么是桶铜流 + +**实现三种限流算法** + +##### 1、正常限制访问频率(正常流量): + +* 限制一个用户发送的请求,我Nginx多久接收一个请求。 + +* Nginx中使用ngx_http_limit_req_module模块来限制的访问频率,限制的原理实质是基于漏桶算法原理来实现的。在nginx.conf配置文件中可以使用limit_req_zone命令及limit_req命令限制单个IP的请求处理频率。 + +```java +#定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + + #绑定限流维度 + server{ + + location/seckill.html{ + limit_req zone=zone; + proxy_pass http://lj_seckill; + } + + } +``` + +* 1r/s代表1秒一个请求,1r/m一分钟接收一个请求, 如果Nginx这时还有别人的请求没有处理完,Nginx就会拒绝处理该用户请求。 + +##### 2、突发限制访问频率(突发流量): + +* 限制一个用户发送的请求,我Nginx多久接收一个。 + +* 上面的配置一定程度可以限制访问频率,但是也存在着一个问题:如果突发流量超出请求被拒绝处理,无法处理活动时候的突发流量,这时候应该如何进一步处理呢?Nginx提供burst参数结合nodelay参数可以解决流量突发的问题,可以设置能处理的超过设置的请求数外能额外处理的请求数。我们可以将之前的例子添加burst参数以及nodelay参数: + +```java +#定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + + #绑定限流维度 + server{ + + location/seckill.html{ + limit_req zone=zone burst=5 nodelay; + proxy_pass http://lj_seckill; + } + + } +``` + +* 为什么就多了一个 burst=5 nodelay; 呢,多了这个可以代表Nginx对于一个用户的请求会立即处理前五个,多余的就慢慢来落,没有其他用户的请求我就处理你的,有其他的请求的话我Nginx就漏掉不接受你的请求 + +##### 3、 限制并发连接数 + +* Nginx中的ngx_http_limit_conn_module模块提供了限制并发连接数的功能,可以使用limit_conn_zone指令以及limit_conn执行进行配置。接下来我们可以通过一个简单的例子来看下: + +```java +http { + limit_conn_zone $binary_remote_addr zone=myip:10m; + limit_conn_zone $server_name zone=myServerName:10m; + } + + server { + location / { + limit_conn myip 10; + limit_conn myServerName 100; + rewrite / http://www.lijie.net permanent; + } + } +``` + +* 上面配置了单个IP同时并发连接数最多只能10个连接,并且设置了整个虚拟服务器同时最大并发数最多只能100个链接。当然,只有当请求的header被服务器处理后,虚拟服务器的连接数才会计数。刚才有提到过Nginx是基于漏桶算法原理实现的,实际上限流一般都是基于漏桶算法和令牌桶算法实现的。接下来我们来看看两个算法的介绍: + +### 漏桶流算法和令牌桶算法知道? + +#### 漏桶算法 + +* 漏桶算法是网络世界中流量整形或速率限制时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。也就是我们刚才所讲的情况。漏桶算法提供的机制实际上就是刚才的案例:**突发流量会进入到一个漏桶,漏桶会按照我们定义的速率依次处理请求,如果水流过大也就是突发流量过大就会直接溢出,则多余的请求会被拒绝。所以漏桶算法能控制数据的传输速率。**![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172646dbb8b696~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 令牌桶算法 + +* 令牌桶算法是网络流量整形和速率限制中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。**令牌桶算法的机制如下:存在一个大小固定的令牌桶,会以恒定的速率源源不断产生令牌。如果令牌消耗速率小于生产令牌的速度,令牌就会一直产生直至装满整个令牌桶。** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172646dbc20c88~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 为什么要做动静分离? + +* Nginx是当下最热的Web容器,网站优化的重要点在于静态化网站,网站静态化的关键点则是是动静分离,动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们则根据静态资源的特点将其做缓存操作。 + +* 让静态的资源只走静态资源服务器,动态的走动态的服务器 + +* Nginx的静态处理能力很强,但是动态处理能力不足,因此,在企业中常用动静分离技术。 + +* 对于静态资源比如图片,js,css等文件,我们则在反向代理服务器nginx中进行缓存。这样浏览器在请求一个静态资源时,代理服务器nginx就可以直接处理,无需将请求转发给后端服务器tomcat。 若用户请求的动态文件,比如servlet,jsp则转发给Tomcat服务器处理,从而实现动静分离。这也是反向代理服务器的一个重要的作用。 + +### Nginx怎么做的动静分离? + +* 只需要指定路径对应的目录。location/可以使用正则表达式匹配。并指定对应的硬盘中的目录。如下:(操作都是在Linux上) + +```java +location /image/ { + root /usr/local/static/; + autoindex on; + } +``` + +1. 创建目录 + +```java +mkdir /usr/local/static/image +``` +1. 进入目录 + +```java +cd /usr/local/static/image +``` +1. 放一张照片上去# + +```java +1.jpg +``` +1. 重启 nginx + +```java +sudo nginx -s reload +``` +1. 打开浏览器 输入 server_name/image/1.jpg 就可以访问该静态图片了 + +### Nginx负载均衡的算法怎么实现的?策略有哪些? + +* 为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。 + +* Nginx负载均衡实现的策略有以下五种: + +#### 1 轮询(默认) + +* 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。 + +```java +upstream backserver { + server 192.168.0.12; + server 192.168.0.13; +} +``` +#### 2 权重 weight + +* weight的值越大分配 + +* 到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。 + +```java +upstream backserver { + server 192.168.0.12 weight=2; + server 192.168.0.13 weight=8; +} +``` + +* 权重越高,在被访问的概率越大,如上例,分别是20%,80%。 + +#### 3 ip_hash( IP绑定) + +* 每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,`并且可以有效解决动态网页存在的session共享问题` + +```java +upstream backserver { + ip_hash; + server 192.168.0.12:88; + server 192.168.0.13:80; +} +``` +#### 4 fair(第三方插件) + +* 必须安装upstream_fair模块。 + +* 对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。 + +```java +upstream backserver { + server server1; + server server2; + fair; +} + +``` + +* 哪个服务器的响应速度快,就将请求分配到那个服务器上。 + +#### 5、url_hash(第三方插件) + +* 必须安装Nginx的hash软件包 + +* 按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。 + +```java +upstream backserver { + server squid1:3128; + server squid2:3128; + hash $request_uri; + hash_method crc32; +} + +``` +### Nginx配置高可用性怎么配置? + +* 当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用 + +* Nginx配置代码: + +```java +server { + listen 80; + server_name www.lijie.com; + location / { + ### 指定上游服务器负载均衡服务器 + proxy_pass http://backServer; + ###nginx与上游服务器(真实访问的服务器)超时时间 后端服务器连接的超时时间_发起握手等候响应超时时间 + proxy_connect_timeout 1s; + ###nginx发送给上游服务器(真实访问的服务器)超时时间 + proxy_send_timeout 1s; + ### nginx接受上游服务器(真实访问的服务器)超时时间 + proxy_read_timeout 1s; + index index.html index.htm; + } + } + +``` +### Nginx怎么判断别IP不可访问? + +```java +# 如果访问的ip地址为192.168.9.115,则返回403 +if ($remote_addr = 192.168.9.115) { + return 403; +} +``` +### 怎么限制浏览器访问? + +```java +## 不允许谷歌浏览器访问 如果是谷歌浏览器返回500 +if ($http_user_agent ~ Chrome) { + return 500; +} +``` +### Rewrite全局变量是什么? + +> | 变量 | 含义 | +> | --- | --- | +> | $args | 这个变量等于请求行中的参数,同$query_string | +> | $content length | 请求头中的Content-length字段。 | +> | $content_type | 请求头中的Content-Type字段。 | +> | $document_root | 当前请求在root指令中指定的值。 | +> | $host | 请求主机头字段,否则为服务器名称。 | +> | $http_user_agent | 客户端agent信息 | +> | $http_cookie | 客户端cookie信息 | +> | $limit_rate | 这个变量可以限制连接速率。 | +> | $request_method | 客户端请求的动作,通常为GET或POST。 | +> | $remote_addr | 客户端的IP地址。 | +> | $remote_port | 客户端的端口。 | +> | $remote_user | 已经经过Auth Basic Module验证的用户名。 | +> | $request_filename | 当前请求的文件路径,由root或alias指令与URI请求生成。 | +> | $scheme | HTTP方法(如http,https)。 | +> | $server_protocol | 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。 | +> | $server_addr | 服务器地址,在完成一次系统调用后可以确定这个值。 | +> | $server_name | 服务器名称。 | +> | $server_port | 请求到达服务器的端口号。 | +> | $request_uri | 包含请求参数的原始URI,不包含主机名,如”/foo/bar.php?arg=baz”。 | +> | $uri | 不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。 | +> | $document_uri | 与$uri相同。 | + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125784653837 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" new file mode 100644 index 0000000..8c213a4 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" @@ -0,0 +1,296 @@ +https://juejin.cn/post/6844904032083902471 + +## 前言 + + ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。ElasticSearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47cdfa683a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## Elasticsearch 面试题 + + 1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。 + + 2、elasticsearch 的倒排索引是什么 + + 3、elasticsearch 索引数据多了怎么办,如何调优,部署 + + 4、elasticsearch 是如何实现 master 选举的 + + 5、详细描述一下 Elasticsearch 索引文档的过程 + + 6、详细描述一下 Elasticsearch 搜索的过程? + + 7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 + + 8、lucence 内部结构是什么? + + 9、Elasticsearch 是如何实现 Master 选举的? + + 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个选了一个master,另外 10 个选了另一个 master,怎么办? + + 11、客户端在和集群连接时,如何选择特定的节点执行请求的? + + 12、详细描述一下 Elasticsearch 索引文档的过程。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47cb162405~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## 1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。 + + 面试官:想了解应聘者之前公司接触的 ES 使用场景、规模,有没有做过比较大规模的索引设计、规划、调优。 + + 解答:如实结合自己的实践场景回答即可。 + + 比如:ES 集群架构 13 个节点,索引根据通道不同共 20+索引,根据日期,每日递增 20+,索引:10 分片,每日递增 1 亿+数据,每个通道每天索引大小控制:150GB 之内。 + + 仅索引层面调优手段: + +### 1.1、设计阶段调优 + + (1)根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引; + + (2)使用别名进行索引管理; + + (3)每天凌晨定时对索引做 force_merge 操作,以释放空间; + + (4)采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink操作,以缩减存储; + + (5)采取 curator 进行索引的生命周期管理; + + (6)仅针对需要分词的字段,合理的设置分词器; + + (7)Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。…….. + +### 1.2、写入调优 + + (1)写入前副本数设置为 0; + + (2)写入前关闭 refresh_interval 设置为-1,禁用刷新机制; + + (3)写入过程中:采取 bulk 批量写入; + + (4)写入后恢复副本数和刷新间隔; + + (5)尽量使用自动生成的 id。 + +### 1.3、查询调优 + + (1)禁用 wildcard; + + (2)禁用批量 terms(成百上千的场景); + + (3)充分利用倒排索引机制,能 keyword 类型尽量 keyword; + + (4)数据量大时候,可以先基于时间敲定索引再检索; + + (5)设置合理的路由机制。 + +### 1.4、其他调优 + + 部署调优,业务调优等。 + + 上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。 + +## 2、elasticsearch 的倒排索引是什么 + + 面试官:想了解你对基础概念的认知。 + + 解答:通俗解释一下就可以。 + + 传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。 + + 而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d11e0d11~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 学术的解答方式: + + 倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。 + + 加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。 + + lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点: + + (1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间; + + (2)查询速度快。O(len(str))的查询时间复杂度。 + +## 3、elasticsearch 索引数据多了怎么办,如何调优,部署 + + 面试官:想了解大数据量的运维能力。 + + 解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。 + + 如何调优,正如问题 1 所说,这里细化一下: + +### 3.1 动态索引层面 + + 基于模板+时间+rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为:blog_index_时间戳的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线 2 的32 次幂-1,索引存储达到了 TB+甚至更大。 + + 一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。 + +### 3.2 存储层面 + + 冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。 + + 对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索效率。 + +### 3.3 部署层面 + + 一旦之前没有规划,这里就属于应急策略。 + + 结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。 + +## 4、elasticsearch 是如何实现 master 选举的 + + 面试官:想了解 ES 集群的底层原理,不再只关注业务层面了。 + + 解答: + + 前置前提: + + (1)只有候选主节点(master:true)的节点才能成为主节点。 + + (2)最小主节点数(min_master_nodes)的目的是防止脑裂。 + + 核对了一下代码,核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。选举流程大致描述如下: + + 第一步:确认候选主节点数达标,elasticsearch.yml 设置的值 + + discovery.zen.minimum_master_nodes; + + 第二步:比较:先判定是否具备 master 资格,具备候选主节点资格的优先返回; + + 若两节点都为候选主节点,则 id 小的值会主节点。注意这里的 id 为 string 类型。 + + 题外话:获取节点 id 的方法。 + +1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name + +2ip port heapPercent heapMax id name复制代码 +## 5、详细描述一下 Elasticsearch 索引文档的过程 + + 面试官:想了解 ES 的底层原理,不再只关注业务层面了。 + + 解答: + + 这里的索引文档应该理解为文档写入 ES,创建索引的过程。 + + 文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程。 + + 记住官方文档中的这个图。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d2b0df73~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。) + + 第二步:节点 1 接受到请求后,使用文档_id 来确定文档属于分片 0。请求会被转到另外的节点,假定节点 3。因此分片 0 的主分片分配到节点 3 上。 + + 第三步:节点 3 在主分片上执行写操作,如果成功,则将请求并行转发到节点 1和节点 2 的副本分片上,等待结果返回。所有的副本分片都报告成功,节点 3 将向协调节点(节点 1)报告成功,节点 1 向请求客户端报告写入成功。 + + 如果面试官再问:第二步中的文档获取分片的过程? + + 回答:借助路由算法获取,路由算法就是根据路由和文档 id 计算目标的分片 id 的过程。 + +1shard = hash(_routing) % (num_of_primary_shards)复制代码 +## 6、详细描述一下 Elasticsearch 搜索的过程? + + 面试官:想了解 ES 搜索的底层原理,不再只关注业务层面了。 + + 解答: + + 搜索拆解为“query then fetch” 两个阶段。 + + query 阶段的目的:定位到位置,但不取。 + + 步骤拆解如下: + + (1)假设一个索引数据有 5 主+1 副本 共 10 分片,一次请求会命中(主或者副本分片中)的一个。 + + (2)每个分片在本地进行查询,结果返回到本地有序的优先队列中。 + + (3)第 2)步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。 + + fetch 阶段的目的:取数据。 + + 路由节点获取所有文档,返回给客户端。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d2edca5f~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## 7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 + + 面试官:想了解对 ES 集群的运维能力。 + + 解答: + + (1)关闭缓存 swap; + + (2)堆内存设置为:Min(节点内存/2, 32GB); + + (3)设置最大文件句柄数; + + (4)线程池+队列大小根据业务需要做调整; + + (5)磁盘存储 raid 方式——存储有条件使用 RAID10,增加单节点性能以及避免单节点存储故障。 + +## 8、lucence 内部结构是什么? + + 面试官:想了解你的知识面的广度和深度。 + + 解答: + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d3590ee5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + Lucene 是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。可以基于这个脉络展开一些。 + +## 9、Elasticsearch 是如何实现 Master 选举的? + + (1)Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分; + + (2)对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。 + + (3)如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。 + + (4)补充:master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http 功能*。 + +## 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个 + + 选了一个 master,另外 10 个选了另一个 master,怎么办? + + (1)当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题; + + (3)当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data节点,避免脑裂问题。 + +## 11、客户端在和集群连接时,如何选择特定的节点执行请求的? + + TransportClient 利用 transport 模块远程连接一个 elasticsearch 集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的 transport 地址,并以 轮询 的方式与这些地址进行通信。 + +## 12、详细描述一下 Elasticsearch 索引文档的过程。 + + 协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片。 + +shard = hash(document_id) % (num_of_primary_shards)复制代码 + + (1)当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffer 到 Filesystem Cache 的过程就叫做 refresh; + + (2)当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中 ,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush; + + (3)在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。 + + (4)flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时; + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd48799304d6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 补充:关于 Lucene 的 Segement: + + (1)Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。 + + (2)段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。 + + (3)对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。 + + (4)为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。 + +作者:程序员追风 +链接:https://juejin.cn/post/6844904031555420167 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" new file mode 100644 index 0000000..677057b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" @@ -0,0 +1 @@ +https://juejin.cn/post/6844904025805029383 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" new file mode 100644 index 0000000..b648d26 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" @@ -0,0 +1,565 @@ + + +## MyBatis简介 + +### MyBatis是什么? + +* Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。 + +* MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 + +### Mybatis优缺点 + +**优点** + +`与传统的数据库访问技术相比,ORM有以下优点:` + +* 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用 +* 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接 +* 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持) +* 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护 +* 能够与Spring很好的集成 + +**缺点** + +* SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求 +* SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库 + +### Hibernate 和 MyBatis 的区别 + +**相同点** + +* 都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。 + +**不同点** + +* 映射关系 + * MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单 + * Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂 + +**SQL优化和移植性** + +* Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。 +* MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。 + +### ORM是什么 + +* ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。 + +### 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? + +* Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。 + +* 而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 + +### 传统JDBC开发存在什么问题? + +* 频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。可以使用连接池解决这个问题。但是使用jdbc需要自己实现连接池。 +* sql语句定义、参数设置、结果集处理存在硬编码。实际项目中sql语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译,重新发布。不好维护。 +* 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。 +* 结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便。 + +### JDBC编程有哪些不足之处,MyBatis是如何解决的? + +* 1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。 + + * 解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。 +* 2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。- + + * 解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 +* 3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 + + * 解决: Mybatis自动将java对象映射至sql语句。 +* 4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 + + * 解决:Mybatis自动将sql执行结果映射至java对象。 + +### MyBatis和Hibernate的适用场景? + +* MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。 +* 对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。 + +**开发难易程度和学习成本** + +* Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统 + +* MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统 + +**总结** + +* MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架, + +* Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。 + +## MyBatis的架构 + +### MyBatis编程步骤是什么样的? + +* 1、 创建SqlSessionFactory + +* 2、 通过SqlSessionFactory创建SqlSession + +* 3、 通过sqlsession执行数据库操作 + +* 4、 调用session.commit()提交事务 + +* 5、 调用session.close()关闭会话 + +### 请说说MyBatis的工作原理 + +* 在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图 ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a66d9566c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。 +2. 加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。 +3. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。 +4. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。 +5. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。 +6. MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。 +7. 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。 +8. 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。 + +### MyBatis的功能架构是怎样的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5a426a4d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 我们把Mybatis的功能架构分为三层: + * API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。 + * 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。 + * 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。 + +### MyBatis的框架架构设计是怎么样的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5ab904a6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 这张图从上往下看。MyBatis的初始化,会从mybatis-config.xml配置文件,解析构造成Configuration这个类,就是图中的红框。 + +1. 加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。 + +2. SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。 + +3. SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。 + +4. 结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。 + +### 什么是DBMS + +* DBMS:数据库管理系统(database management system)是一种操纵和管理数据库的大型软件,用于建立、使用和维护数zd据库,简称dbms。它对数据库进行统一的管理和控制,以保证数据库的安全性和完整性。用户通过dbms访问数据库中的数据,数据库管理员也通过dbms进行数据库的维护工作。它可使多个应用程序和用户用不同的方法在同时版或不同时刻去建立,修改和询问数据库。DBMS提供数据定义语言[DDL](https://link.juejin.cn?target=https%3A%2F%2Fwww.baidu.com%2Fs%3Fwd%3DDDL%26tn%3DSE_PcZhidaonwhc_ngpagmjz%26rsv_dl%3Dgh_pc_zhidao "https://www.baidu.com/s?wd=DDL&tn=SE_PcZhidaonwhc_ngpagmjz&rsv_dl=gh_pc_zhidao")(Data Definition Language)与数据操作语言[DML](https://link.juejin.cn?target=https%3A%2F%2Fwww.baidu.com%2Fs%3Fwd%3DDML%26tn%3DSE_PcZhidaonwhc_ngpagmjz%26rsv_dl%3Dgh_pc_zhidao "https://www.baidu.com/s?wd=DML&tn=SE_PcZhidaonwhc_ngpagmjz&rsv_dl=gh_pc_zhidao")(Data Manipulation Language),供用户定义数据库的模式结构与权限约束,实现对数据的追加权、删除等操作。 + +### 为什么需要预编译 + +* 定义: + + * SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译,这样 DBMS 执行 SQL 时,就不需要重新编译。 +* 为什么需要预编译 + + * JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下,将对所有的 SQL 进行预编译。 + * 还有一个重要的原因,复制SQL注入 + +### Mybatis都有哪些Executor执行器?它们之间的区别是什么? + +* Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。 + +* **SimpleExecutor**:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。 + +* **ReuseExecutor**:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。 + +* **BatchExecutor**:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。 + +`作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。` + +### Mybatis中如何指定使用哪一种Executor执行器? + +* 在Mybatis配置文件中,在设置(settings)可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数,如SqlSession openSession(ExecutorType execType)。 + +* 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 + +### Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? + +* Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 + +* 它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。 + +* 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 + +## 映射器 + +### #{}和${}的区别 + +* #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。 + +* Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。 + +* #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入 + +* #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外 + +### 模糊查询like语句该怎么写 + +* 1 ’%${question}%’ 可能引起SQL注入,不推荐 +* 2 "%"#{question}"%" 注意:因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ',所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果。 +* 3 CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,(推荐) +* 4 使用bind标签(不推荐) + + +复制代码 +### 在mapper中如何传递多个参数 + +**方法1:顺序传参法** + +public User selectUser(String name, int deptId); + + +复制代码 + +* #{}里面的数字代表传入参数的顺序。 + +* 这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错。 + +**方法2:@Param注解传参法** + +public User selectUser(@Param("userName") String name, int @Param("deptId") deptId); + + +复制代码 + +* #{}里面的名称对应的是注解@Param括号里面修饰的名称。 + +* 这种方法在参数不多的情况还是比较直观的,(推荐使用)。 + +**方法3:Map传参法** + +public User selectUser(Map params); + + +复制代码 + +* #{}里面的名称对应的是Map里面的key名称。 + +* 这种方法适合传递多个参数,且参数易变能灵活传递的情况。(推荐使用)。 + +**方法4:Java Bean传参法** + +public User selectUser(User user); + + +复制代码 + +* #{}里面的名称对应的是User类里面的成员属性。 + +* 这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。(推荐使用)。 + +### Mybatis如何执行批量操作 + +* **使用foreach标签** +* foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。 + * item   表示集合中每一个元素进行迭代时的别名,随便起的变量名; + * index   指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用; + * open   表示该语句以什么开始,常用“(”; + * separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”; + * close   表示以什么结束,常用“)”。 +* 在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况: + 1. 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list + 2. 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array + 3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的, + map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key +* 具体用法如下: + + + //推荐使用 + + + INSERT INTO emp(ename,gender,email,did) + VALUES + + (#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) + + +复制代码 + + + + INSERT INTO emp(ename,gender,email,did) + VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) + + +复制代码 + +* **使用ExecutorType.BATCH** + + * Mybatis内置的ExecutorType有3种,默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优; 但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的 + + * 具体用法如下: + + //批量保存方法测试 + @Test + public void testBatch() throws IOException{ + SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); + //可以执行批量操作的sqlSession + SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + + //批量保存执行前时间 + long start = System.currentTimeMillis(); + try { + EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class); + for (int i = 0; i < 1000; i++) { + mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1")); + } + + openSession.commit(); + long end = System.currentTimeMillis(); + //批量保存执行后的时间 + System.out.println("执行时长" + (end - start)); + //批量 预编译sql一次==》设置参数==》10000次==》执行1次 677 + //非批量 (预编译=设置参数=执行 )==》10000次 1121 + + } finally { + openSession.close(); + } + } + 复制代码 + * mapper和mapper.xml如下 + + public interface EmployeeMapper { + //批量保存员工 + Long addEmp(Employee employee); + } + + 复制代码 + + insert into employee(lastName,email,gender) + values(#{lastName},#{email},#{gender}) + + + + 复制代码 + +### 如何获取生成的主键 + +* 新增标签中添加:keyProperty=" ID " 即可 + + + insert into user( + user_name, user_password, create_time) + values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP}) + + + 复制代码 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5d83efe4~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 当实体类中的属性名和表中的字段名不一样 ,怎么办 + +* 第1种: 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 + + + + 复制代码 +* 第2种: 通过``来映射字段名和实体类属性名的一一对应的关系。 + + + + + + + + + + + + + 复制代码 + +### Mapper 编写有哪几种方式? + +* 第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。 + + 1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置 + + + + + + + 复制代码 + 2. 定义 mapper 接口 + + 3. 实现类集成 SqlSessionDaoSupport + + mapper 方法中可以 this.getSqlSession()进行数据增删改查。 + + 4. spring 配置 + + + + + + 复制代码 +* 第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean: + + 1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和mappre 接口的名称相同且在同一个目录,这里可以不用配置 + + 2. 定义 mapper 接口: + + + + + + + 复制代码 + 3. mapper.xml 中的 namespace 为 mapper 接口的地址 + + 4. mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致 + + 5. Spring 中定义 + + + + + + + 复制代码 +* 第三种:使用 mapper 扫描器: + + 1. mapper.xml 文件编写: + + mapper.xml 中的 namespace 为 mapper 接口的地址; + + mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致; + + 如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。 + + 2. 定义 mapper 接口: + + 注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录 + + 3. 配置 mapper 扫描器: + + + + + + + 复制代码 + 4. 使用扫描器后从 spring 容器中获取 mapper 的实现对象。 + +### 什么是MyBatis的接口绑定?有哪些实现方式? + +* 接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。 + +* 接口绑定有两种实现方式 + + 1. 通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定; + + 2. 通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。 + +### 使用MyBatis的mapper接口调用时有哪些要求? + +1. Mapper接口方法名和mapper.xml中定义的每个sql的id相同。 +2. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。 +3. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。 +4. Mapper.xml文件中的namespace即是mapper接口的类路径。 + +### 这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗 + +* Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 + +* Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 + +### Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复? + +* 不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。 + +* 原因就是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 + +### 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? + +* 答:Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。在Xml映射文件中,``标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。``标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。每一个`