JVM 核心技术 22 讲

专栏亮点

  1. 从 0 掌握 JVM 关键技术,了解核心知识;
  2. 全面了解各类 GC 算法的原理和特性,洞悉相关原理;
  3. 全面学习常见的 JVM 分析调优工具,上手十八般武艺;
  4. 一线大厂 JVM 面试题全面剖析,助力拿到心仪 Offer。

专栏介绍

近些年来,无论是使用规模、开发者人数,还是技术生态成熟度、相关工具的丰富程度,Java 都当之无愧是后端开发语言中不可撼动的王者,也是开发各类业务系统的首选语言。

时至今日,整个 IT 招聘市场上,Java 开发工程师依然是缺口最大,需求最多的热门职位。另外,从整个市场环境看,传统企业的信息化,传统 IT 系统的互联网化,都还有非常大的发展空间,由此推断未来 Java 开发的市场前景广阔,从业人员的行业红利还可以持续很长时间。

从权威的 TIOBE 编程语言排行榜 2019 年 11 月数据来看,Java 的流行程度也是稳居第一:

1c91b731-e86d-4b59-85b6-8b7ec53e87d6.jpg

4fead80b-dc2e-4c40-852f-c4bd22bab207.jpg

拉勾网 2019 年 9 月统计的招聘岗位比例,也可以看到 Java 和 JavaScript 是最高的,不过 Java 的求职难度只有 JavaScript 的 1/7

f5b072d7-2235-4814-ac63-3e90f0633629.jpg

Java 平均一个岗位有 4 个人竞争,而 JavaScript 则是 28 个,Perl 最夸张,超过 30 个。

d70b22b6-177c-443e-8ef1-957531028c60.jpg

而通过职友网的数据统计,北京、上海、杭州、深圳的 Java 程序员平均薪酬在 16-21K 之间,在广州、成都、苏州、南京等城市也有 11K-13K 的平均收入,远超一般行业的收入水平。

fd19dbb9-87e6-40bd-9d67-4455f1ee2513.jpg

所以学习 Java 目前还是一个非常有优势的职业发展选择。

而了解 JVM 则是深入学习 Java 必不可少的一环,也是 Java 开发人员迈向更高水平的一个阶梯。

我们不仅要会用 Java 写代码做系统,更要懂得如何理解和分析 Java 程序运行起来以后内部发生了什么,然后可以怎么让它运行的更好。就像我们要想多年开车的老司机,仅仅会开车肯定不能当一个好司机。车开多了,总会有一些多多少少大大小小的故障毛病。老司机需要知道什么现象说明有了什么毛病,需要怎么处理,不然就会导致经常抛锚,影响我们的行程。本课程就是用来教会我们怎么能够去了解 JVM 这辆优秀跑车的一些原理和怎么去用各种工具分析修理它。

市面上各类 JVM 相关的资料虽多如牛毛,但是明显都很难让大家系统性地学明白,同时一线大厂技术面试现在 JVM 知识也是必考科目。

在这个背景下,我们全面梳理了系统化学习 JVM 的知识和经验,包括 JVM 的技术和内存模型、JVM 参数和内置工具、GC 算法、GC 日志、内存和线程等相关问题排查分析,以及常见的面试问题深度剖析等高级的进阶方法与实战,既满足大家快速系统化学习和全面掌握知识的需求,又兼顾大家的面试经验辅导。

  • 通过体系化的学习,了解一般原理,知其然知其所以然;
  • 熟悉工具和方案,知道从何下手,工作中如何分析和解决问题;
  • 随着课程的演示和练习,加深理解,不管大家之前的基础如何,都能够融会贯通;
  • 面试题的解析部分,会根据大家的反馈进行持续更新,长期助力于大家的学习和进步。

本课程的特点可以总结为 16 个字:

深入浅出、实践为要、体系完整、层次分明

本课程分为两部分,基础知识篇主要介绍 JVM 的基础知识、JDK 相关的各种工具用法,深入分析篇讲解各种 GC 算法、如何进行 JVM 的 GC 日志、线程、内存等各类指标进行分析和问题诊断,再结合作者的实际分析调优经验,以及对于常见的 JVM 面试问题进行分析和解答,为学习者梳理清楚 JVM 的整体知识脉络,带来最全面的 JVM 一线经验和实用技巧。

专栏大纲

本次分享您将了解以下内容(22 课时):

avatar

基础知识篇

  1. 环境准备:千里之行,始于足下
  2. 常用性能指标:没有量化,就没有改进
  3. JVM 基础知识:不积跬步,无以至千里
  4. Java 字节码技术:不积细流,无以成江河
  5. JVM 类加载器:山不辞土,故能成其高
  6. JVM 内存模型:海不辞水,故能成其深
  7. JVM 启动参数详解:博观而约取、厚积而薄发
  8. JDK 内置命令行工具:工欲善其事,必先利其器
  9. JDK 内置图形界面工具:海阔凭鱼跃,天高任鸟飞
  10. JDWP 简介:十步杀一人,千里不留行
  11. JMX 与相关工具:山高月小,水落石出

深入分析篇

  1. 常见的 GC 算法介绍(Parallel/CMS/G1):温故而知新
  2. Java11 ZGC 和 Java12 Shenandoah 介绍:苟日新、日日新、又日新
  3. Oracle Graalvm 介绍:会当凌绝顶、一览众山小
  4. GC 日志解读与分析:千淘万漉虽辛苦,吹尽狂沙始到金
  5. JVM 的线程堆栈等数据分析:操千曲而后晓声、观千剑而后识器
  6. 内存 dump 和内存分析工具:万里赴戎机、关山度若飞
  7. fastthread 相关的工具介绍:欲穷千里目,更上一层楼
  8. 面临复杂问题时的几个高级工具:它山之石,可以攻玉
  9. JVM 问题排查分析调优经验:操千曲而后晓声,观千剑而后识器
  10. JVM 相关的常见面试问题汇总:运筹策帷帐之中,决胜于千里之外
  11. 应对容器时代面临的挑战:长风破浪会有时、直挂云帆济沧海

你能收获什么

  1. 夯实基础:掌握 JVM 的基础知识和常用工具,了解一般原理,知道从什么地方着手分析问题;
  2. 深入学习:掌握各类 GC 算法的一般原理,知道如何根据实际需要选择使用合适的 GC 策略;
  3. 分析问题:掌握 GC 日志、线程、内存等维度的分析技巧,知道排查问题和优化系统的套路;
  4. 积累经验:了解常见的分析调优经验,熟悉常见的面试问题和技巧,彻底学会 JVM 知识。

适宜人群

  1. 希望提高技术能力的 Java 开发者、运维工程师
  2. 对 JVM 技术和性能、问题分析调优感兴趣的技术人员
  3. 广大高校计算机和软件专业的师生

前置知识

  • 熟练掌握 Java 开发基础知识

作者介绍

avatar

avatar

购买须知

  • 本专栏为图文内容,共计 22 篇。
  • 每周二、四更新,预计于 2020 年 1 月 25 日更新完毕。
  • 付费用户可享受文章永久阅读权限。
  • 本专栏为虚拟产品,一经付费概不退款,敬请谅解。
  • 本专栏可在 GitChat 服务号、App 及网页端 gitbook.cn 上购买,一端购买,多端阅读。

订阅福利

  • 本专栏限时特价 49 元,12 月 26 日恢复至原价 69 元。
  • 订购本专栏可获得专属海报(在 GitChat 服务号领取),分享专属海报每成功邀请一位好友购买,即可获得 25% 的返现奖励,多邀多得,上不封顶,立即提现。
  • 提现流程:在 GitChat 服务号中点击「我-我的邀请-提现」。
  • 购买本专栏后,可加入读者群交流(入群方式可查看第 3 篇文末说明)。

课程内容

阅读此专栏的正确姿势

课程背景

近些年来,无论是使用规模、开发者人数,还是技术生态成熟度、相关工具的丰富程度,Java 都当之无愧是后端开发语言中不可撼动的王者,也是开发各类业务系统的首选语言。

时至今日,整个 IT 招聘市场上,Java 开发工程师依然是缺口最大,需求最多的热门职位。另外,从整个市场环境看,传统企业的信息化,传统 IT 系统的互联网化,都还有非常大的发展空间,由此推断未来 Java 开发的市场前景广阔,从业人员的行业红利还可以持续很长时间。

从权威的 TIOBE 编程语言排行榜 2019 年 11 月数据来看,Java 的流行程度也是稳居第一。

1c91b731-e86d-4b59-85b6-8b7ec53e87d6.jpg

4fead80b-dc2e-4c40-852f-c4bd22bab207.jpg

拉勾网 2019 年 9 月统计的招聘岗位比例,也可以看到 Java 和 JavaScript 是最高的,不过 Java 的求职难度只有 JavaScript 的 1/7。

f5b072d7-2235-4814-ac63-3e90f0633629.jpg

Java 平均一个岗位有 4 个人竞争,而 JavaScript 则是 28 个,Perl 最夸张,超过 30 个。

d70b22b6-177c-443e-8ef1-957531028c60.jpg

而通过职友网的数据统计,北京、上海、杭州、深圳的 Java 程序员平均薪酬在 16-21K 之间,在广州、成都、苏州、南京等城市也有 11K-13K 的平均收入,远超一般行业的收入水平。

fd19dbb9-87e6-40bd-9d67-4455f1ee2513.jpg

所以学习 Java 目前还是一个非常有优势的职业发展选择。

而了解 JVM 则是深入学习 Java 必不可少的一环,也是 Java 开发人员迈向更高水平的一个阶梯。我们不仅要会用 Java 写代码做系统,更要懂得如何理解和分析 Java 程序运行起来以后内部发生了什么,然后可以怎么让它运行的更好。

就像我们要想多年开车的老司机,仅仅会开车肯定不能当一个好司机。车开多了,总会有一些多多少少大大小小的故障毛病。老司机需要知道什么现象说明有了什么毛病,需要怎么处理,不然就会导致经常抛锚,影响我们的行程。

本课程就是用来教会我们怎么能够去了解 JVM 这辆优秀跑车的一些原理和怎么去用各种工具分析修理它。

课程特点

市面上各类 JVM 相关的资料虽多,但是明显存在两个极端:过于生涩难懂,或者流于某个技巧点而不系统化。同时各大公司也都越来越重视推动和发展 JVM 相关技术,一线大厂技术面试现在 JVM 知识也是必考科目。

在这个背景下,我们全面梳理了系统化学习 JVM 的知识和经验,包括 JVM 的技术和内存模型,JVM 参数和内置工具,GC 算法,GC 日志、内存和线程等相关问题排查分析,以及常见的面试问题深度剖析等高级的进阶方法与实战,既满足大家快速系统化学习和全面掌握知识的需求,又兼顾大家的面试经验辅导。

  • 通过体系化的学习,了解一般原理,知其然知其所以然;
  • 熟悉工具和方案,知道从何下手,工作中如何分析和解决问题;
  • 随着课程的演示和练习,加深理解,不管大家之前的基础如何,都能够融会贯通;
  • 面试题的解析部分,会根据大家的反馈进行持续更新,长期助力于大家的学习和进步。

本课程的特点可以总结为 16 个字:

体系完整、层次分明、深入浅出、实践为要

为什么做这门课

最近有人问我,程序员多以高深技术为尊,为什么你要做 JVM 的一个偏向于基础和实际应用的专栏,而不是一个讲 JVM 内部实现的各种底层原理,或者是高深的各种算法原理之类的内容。

我在此想说一下我对这个问题的想法:

我个人一直认为,技术应该有两方面,有一小部分人去做高精尖的,以理论为主,更多的人以把技术应用到实际工作、改进效率、提高生产力,以实用为主。这也契合了技术大牛史海峰老师经常说的一句话,架构师应该是一个胸怀理想的实用主义者。

所以,我们再这个课程里,只给大家呈现那些对大家的工作和其他方面,应该会有用的东西,脚踏实地的东西,不管是技术点,还是经验之谈,虽有少量的前瞻性介绍和展望,但是主线一定是偏向于基础和实际应用的。

前一阵在网上听樊登老师的演讲,他提到的一个东西方教学的差异。国人教学、传授知识,喜欢按孔子、老子的这一套,讲究悟性,说一句话就很高深,让人摸不着头脑,然后你要是有悟性,就能悟到真理,悟不到就说明还需要加倍努力。

而西方从苏格拉底、柏拉图、亚里士多德起,就喜欢用逻辑,第一步是这样,第二步是那样,第三步要是发现第一步不完善,那么 OK,我们就可以去改善第一步,然后继续第二步,第三步......这样我们的知识体系就会慢慢的越来越完善,厚实,接近真理,并且这个方法是可以复制的。

所以我们公司技术委员会就组织了一些一线的技术人员,在我们的研发团队实验了几期 4~6 课时,每次 2 小时的“知识+实践”课程,并且受到了良好的效果和积极的反馈。

恰好当时内部培训的时候,《JVM 基础入门》这门课是我和富飞一起组织的,富飞在以往的工作经历中,翻译和撰写了不少 JVM 相关的技术文章和博客,在 JVM 方面积累了大量的一手经验和技巧。

知识这种东西,独乐乐不如众乐乐,一个人会了它的价值就有限,我们在公司内部做了培训也还是只影响了参加培训的百八十个人。如果把 JVM 的内容进行更加完整的整理加工,再融合目前行业里大家最关心的各类问题,变成一个公开的课程,那么就可以影响到更多的人,产生更大的价值,对大家都有益,这是一个多赢的事情(这也是史老师那句话的前半句里的“胸怀理想"吧)。

基于这些原因,大家一拍即合,于是就有了这个课程跟大家见面。我们相信这门课程,一定不会让大家失望。

课程内容

本课程分为两部分,基础知识篇主要介绍 JVM 的基础知识、JDK 相关的各种工具用法,深入分析篇讲解各种 GC 算法、如何进行 JVM 的 GC 日志、线程、内存等各类指标进行分析和问题诊断,再结合作者的实际分析调优经验,以及对于常见的 JVM 面试问题进行分析和解答,为学习者梳理清楚 JVM 的整体知识脉络,带来最全面的 JVM 一线经验和实用技巧。

本次分享您将了解以下内容(22 课时):

avatar

基础知识篇

  1. 环境准备:千里之行,始于足下
  2. 常用性能指标:没有量化,就没有改进
  3. JVM 基础知识:不积跬步,无以至千里
  4. Java 字节码技术:不积细流,无以成江河
  5. JVM 类加载器:山不辞土,故能成其高
  6. JVM 内存模型:海不辞水,故能成其深
  7. JVM 启动参数详解:博观而约取、厚积而薄发
  8. JDK 内置命令行工具介绍:工欲善其事,必先利其器
  9. JDK 内置图形界面工具介绍:海阔凭鱼跃,天高任鸟飞
  10. JDWP 简介:十步杀一人,千里不留行
  11. JMX 与相关工具:山高月小,水落石出

深入分析篇

  1. 常见的 GC 算法介绍(Parallel/CMS/G1):温故而知新
  2. Java11 ZGC 和 Java12 Shenandoah 介绍:苟日新、日日新、又日新
  3. Oracle Graalvm 介绍:会当凌绝顶、一览众山小
  4. GC 日志解读与分析:千淘万漉虽辛苦,吹尽狂沙始到金
  5. JVM 的线程堆栈等数据分析:操千曲而后晓声、观千剑而后识器
  6. 内存 dump 和内存分析工具:万里赴戎机、关山度若飞
  7. fastthread 相关的工具介绍:欲穷千里目,更上一层楼
  8. 面临复杂问题时的几个高级工具:它山之石,可以攻玉
  9. JVM 问题排查分析调优经验:操千曲而后晓声,观千剑而后识器
  10. JVM 相关的常见面试问题汇总:运筹策帷帐之中,决胜于千里之外
  11. 应对容器时代面临的挑战:长风破浪会有时、直挂云帆济沧海

送给大家的话

俗话说,“活到老、学到老”。IT 行业的技术发展和创新速度太快,新的知识很快成为老知识,新的技巧很快成为旧把式,只有终身学习才能适应技术本身的发展。同时现在随着网络的发展,特别是各类新的内容平台和媒体的涌现,信息不是太少了,而是太多了。

信息爆炸带来了甄别有用信息的过程成本增加,这时候选择好的学习途径、学习内容就跟学习方法一样重要,为大家系统化的总结经验和传播知识也同样变得很重要。

让我们一起在 GitChat 平台不断学习,跟志同道合的同学们一起努力,共同进步。

环境准备:千里之行,始于足下

Java 语言编写代码非常简单,也很容易入门,非常适合开发各种企业级应用和业务系统。一个众所周知的事实是: 用起来越简单的系统, 其背后的原理和实现就越复杂。道理很容易理解, 系统的内部实现考虑了各种极端的情况,对用户屏蔽了各种复杂性。作为支撑庞大的 Java 生态系统的基石, JVM 内部实现是非常复杂的。据统计,OpenJDK 的实现代码已经超过 1000 万行。

JVM 难不难? 自然是 “难者不会,会者不难”。万丈高楼平地起, 没有掌握一定的基础知识, 学过的各种原理,了解相关技巧,也就会出现转眼即忘,书到用时方恨少的情况。

掌握好基础知识,学而时习之,经常使用各种工具并熟练运用,自然就能深入掌握一门技能。理论结合实践,掌握 JVM 相关知识,熟练各种工具的使用,是 Java 工程师职业进阶中不可或缺的。学就要学会理论,掌握实现原理。 理解了 Java 标准平台的 JVM,举一反三,稍微变通一下,碰到 Android 的 ART, Go 的虚拟机,以及各种语言的垃圾收集实现,都会很容易理解。

1.1 JDK、JRE、JVM 的关系

JDK

JDK(Java Development Kit) 是用于开发 Java 应用程序的软件开发工具集合,包括了 Java 运行时的环境(JRE)、解释器(Java)、编译器(javac)、Java 归档(jar)、文档生成器(Javadoc)等工具。简单的说我们要开发 Java 程序,就需要安装某个版本的 JDK 工具包。

JRE

JRE(Java Runtime Enviroment )提供 Java 应用程序执行时所需的环境,由 Java 虚拟机(JVM)、核心类、支持文件等组成。简单的说,我们要是想在某个机器上运行 Java 程序,可以安装 JDK,也可以只安装 JRE,后者体积比较小。

JVM

Java Virtual Machine(Java 虚拟机)有三层含义,分别是:

  • JVM规范要求;
  • 满足 JVM 规范要求的一种具体实现(一种计算机程序);
  • 一个 JVM 运行实例,在命令提示符下编写 Java 命令以运行 Java 类时,都会创建一个 JVM 实例,我们下面如果只记到 JVM 则指的是这个含义;如果我们带上了某种 JVM 的名称,比如说是 Zing JVM,则表示上面第二种含义。

JDK 与 JRE、JVM 之间的关系

就范围来说,JDK > JRE > JVM:

  • JDK = JRE + 开发工具
  • JRE = JVM + 类库

0.18346271077222331.png

三者在开发运行 Java 程序时的交互关系:

简单的说,就是通过 JDK 开发的程序,编译以后,可以打包分发给其他装有 JRE 的机器上去运行。而运行的程序,则是通过 Java 命令启动的一个 JVM 实例,代码逻辑的执行都运行在这个 JVM 实例上。

0.9484384203409852.png

Java 程序的开发运行过程为:

我们利用 JDK (调用 Java API)开发 Java 程序,编译成字节码或者打包程序。然后可以用 JRE 则启动一个 JVM 实例,加载、验证、执行 Java 字节码以及依赖库,运行 Java 程序。而 JVM 将程序和依赖库的 Java 字节码解析并变成本地代码执行,产生结果。

1.2 JDK 的发展过程与版本变迁

说了这么多 JDK 相关的概念,我们再来看一下 JDK 的发展过程。JDK 版本列表

JDK版本发布时间代号备注
11996年1月23日Oak(橡树)初代版本,伟大的一个里程碑,但是是纯解释运行,使用JIT,性能比较差,速度慢
1.11997年2月19日Sparkler(宝石)JDBC、支持内部类、RMI、反射等等
1.21998年12月8日Playground(操场)集合框架、JIT等等
1.32000年5月8日Kestrel(红隼)对Java的各个方面都做了大量优化和增强
1.42004年2月6日Merlin(隼)XML处理、支持IPV6、正则表达式,引入nio和CMS垃圾回收器
52004年9月30日Tiger(老虎)泛型、增强for语句、自动拆装箱、可变参数、静态导入、注解
62006年12月11日Mustang(野马)支持脚本语言、JDBC4.0
72011年7月28日Dolphin(海豚)switch支持String类型、泛型推断、nio 2.0开发包、数值类型可以用二进制字符串表示
82014年3月18日Spider(蜘蛛)Lambda 表达式、接口默认方法、Stream API、新的日期API、Nashorn引擎 jjs,引入G1垃圾回收器
92017年9月22日Modularity (模块化)模块系统、HTTP 2 客户端、多版本兼容 JAR 包、私有接口方法、改进Stream API、响应式流(Reactive Streams) API
102018年3月21日引入关键字 var 局部变量类型推断、统一的垃圾回收接口
112018年9月25日HTTP客户端(标准)、无操作垃圾收集器,支持ZGC垃圾回收器,首个LTS版本
122019年3月19日新增一个名为 Shenandoah 的垃圾回收器、扩展switch语句的功能、改进 G1 垃圾回收器
132019年9月17日改进了CDS内存共享,ZGC归还系统内存,SocketAPI和switch语句以及文本块表示
14开发中继续对ZGC、G1改进,标记 ParallelScavenge + SerialOld组合为过时的 ,移除CMS垃圾回收器

Java 大事记

  1. 1995 年 5 月 23 日,Java 语言诞生
  2. 1996 年 1 月,第一个 JDK-JDK1.0 诞生
  3. 1997 年 2 月 18 日,JDK1.1 发布
  4. 1997 年 4 月 2 日,JavaOne 会议召开,参与者逾一万人,创当时全球同类会议规模之纪录
  5. 1997 年 9 月,Java 开发者社区成员超过十万
  6. 1998 年 2 月,JDK1.1 被下载超过 200 万次
  7. 1998 年 12 月 8 日,JAVA2 企业平台 J2EE 发布
  8. 1999 年 6 月,Sun 公司发布 Java 的三个版本:标准版、企业版和微型版(J2SE、J2EE、J2ME)
  9. 2000 年 5 月 8 日,JDK1.3 发布
  10. 2000 年 5 月 29 日,JDK1.4 发布
  11. 2002 年 2 月 26 日,J2SE1.4 发布,自此 Java 的计算能力有了大幅提升
  12. 2004 年 9 月 30 日,J2SE1.5 发布,是 Java 语言的发展史上的又一里程碑事件,Java 并发包 JUC 也是这个版本引入的。为了表示这个版本的重要性,J2SE1.5 更名为 J2SE5.0
  13. 2005 年 6 月,发布 Java SE 6,这也是一个比较长期使用的版本
  14. 2006 年 11 月 13 日,Sun 公司宣布 Java 全线采纳 GNU General Public License Version 2,从而公开了 Java 的源代码
  15. 2009 年 04 月 20 日,Oracle 公司 74 亿美元收购 Sun。取得 Java 的版权
  16. 2011 年 7 月 28 日,Oracle 公司发布 Java SE7.0 的正式版
  17. 2014 年 3 月 18 日,Oracle 公司发布 Java SE 8,这个版本是目前最广泛使用的版本
  18. 2017 年 9 月 22 日,JDK9 发布,API 有了较大的调整,添加了对 WebSocket 和 HTTP/2 的支持,此后每半年发布一个大版本
  19. 2018 年 3 月 21 日,JDK10 发布,最大的变化就是引入了 var,如果你熟悉 C# 或 JavaScript/NodeJS 就会知道它的作用
  20. 2018 年 9 月 25 日,JDK11 发布,引入 ZGC,这个也是第一个公布的长期维护版本 LTS
  21. 2019 年 3 月 19 日,JDK12 发布,引入毫秒级停顿的 Shenandoah GC
  22. 2019 年 9 月 17 日,JDK13 发布,改进了 CDS 内存共享,ZGC 归还系统内

我们可以看到 JDK 发展的越来越多,越来越复杂,特别是被 Oracle 收购以后,近 2 年以来版本号快速膨胀,GC 算法也有了更快速的发展。目前最新的 JDK 是 JDK13,同时 JDK14 正在开发中,预计 2020 年 3 月份发布。很多朋友直呼,“不要再升级了,还在用 JDK8,已经学不过来了”。但是正是由于 Java 不断的发展和改进,才会持续具有生命力。

常规的 JDK,一般指 OpenJDK 或者 Oracle JDK,当然 Oracle 还有一个新的 JVM 叫 GraalVM,也非常有意思。除了 Sun/Oracle 的 JDK 以外,原 BEA 公司(已被 Oracle 收购)的 JRockit,IBM 公司的 J9,Azul 公司的 Zing JVM,阿里巴巴公司的分支版本 DragonWell 等等。

1.3 安装 JDK

JDK 通常是从 Oracle 官网下载, 打开页面翻到底部,找 Java for Developers 或者 Developers, 进入 Java 相应的页面 或者 Java SE 相应的页面, 查找 Download, 接受许可协议,下载对应的 x64 版本即可。891e2fe6-e872-4aa9-b00d-d176e947f11f.jpg

建议安装比较新的 JDK8 版本, 如 JDK8u2313bbdc5e9-149c-407d-b757-69a061581aae.png

注意:从 Oracle 官方安装 JDK 需要注册和登录 Oracle 账号。现在流行将下载链接放到页面底部,很多工具都这样。当前推荐下载 JDK8。 今后 JDK11 可能成为主流版本,因为 Java11 是 LTS 长期支持版本,但可能还需要一些时间才会普及,而且 JDK11 的文件目录结构与之前不同, 很多工具可能不兼容其 JDK 文件的目录结构。

有的操作系统提供了自动安装工具,直接使用也可以,比如 yum, brew, apt 等等。例如在 MacBook 上,执行:

brew cask install java8

而使用如下命令,会默认安装最新的 JDK13:

brew cask install java

如果电脑上有 360 软件管家或者腾讯软件管家,也可以直接搜索和下载安装 JDK(版本不是最新的,但不用注册登录 Oracle 账号):035a0b3e-de33-4e97-946c-c9adb8b68ae7.png

如果网络不好,可以从我的百度网盘共享获取:https://pan.baidu.com/s/14ukd1wZgXFw1ShnUJ8z6aA

1.4 设置环境变量

如果找不到命令,需要设置环境变量: JAVA_HOMEPATH

JAVA_HOME 环境变量表示 JDK 的安装目录,通过修改 JAVA_HOME ,可以快速切换 JDK 版本 。很多工具依赖此环境变量。

另外, 建议不要设置 CLASS_PATH 环境变量,新手没必要设置,容易造成一些困扰。

Windows 系统, 系统属性 - 高级 - 设置系统环境变量。 如果没权限也可以只设置用户环境变量。

Linux 和 MacOSX 系统, 需要配置脚本。 例如:

$ cat ~/.bash_profile

# JAVA ENVexport JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Homeexport PATH=$PATH:$JAVA_HOME/bin

让环境配置立即生效:

$ source ~/.bash_profile

查看环境变量:

echo $PATHecho $JAVA_HOME

一般来说,.bash_profile 之类的脚本只用于设置环境变量。 不设置随机器自启动的程序。

如果不知道自动安装/别人安装的 JDK 在哪个目录怎么办?

最简单/最麻烦的查询方式是询问相关人员。

查找的方式很多,比如,可以使用 whichwhereisls -l 跟踪软连接, 或者 find 命令全局查找(可能需要 sudo 权限), 例如:

jps -vwhereis javacls -l /usr/bin/javacfind / -name javac

找到满足 $JAVA_HOME/bin/javac 的路径即可。

Windows 系统,安装在哪就是哪,默认在C:\Program Files (x86)\Java下。通过任务管理器也可以查看某个程序的路径,注意 JAVA_HOME 不可能是 C:\Windows\System32 目录。

然后我们就可以在 JDK 安装路径下看到很多 JVM 工具,例如在 Mac 上:

54940291.png在后面的章节里,我们会详细解决其中一些工具的用法,以及怎么用它们来分析 JVM 情况。

1.4 验证 JDK 安装完成

安装完成后,Java 环境一般来说就可以使用了。 验证的脚本命令为:

$ java -version

可以看到输出类似于以下内容,既证明成功完成安装:

java version "1.8.065"Java(TM) SE Runtime Environment (build 1.8.065-b17) Java HotSpot(TM) 64-Bit Server VM (build 25.65-b01, mixed mode)

然后我们就可以写个最简单的 Java 程序了,新建一个文本文件,输入以下内容:

public class Hello {    public static void main(String[] args){        System.out.println("Hello, JVM!");    }}

然后把文件名改成Hello.java,在命令行下执行:

$ javac Hello.java

然后使用如下命令运行它:

$ java Hello Hello, JVM!

即证明运行成功,我们的 JDK 环境可以用来开发了。

参考材料

  1. https://www.jianshu.com/p/7b99bd132470
  2. https://blog.csdn.net/Phoenix_smf/article/details/79709592
  3. https://www.iteye.com/blog/dasheng-727156
  4. https://blog.csdn.net/lc11535/article/details/99776597
  5. https://blog.csdn.net/damin112/article/details/84634041
  6. https://blog.csdn.net/KamRoseLee/article/details/79440425
  7. https://blog.csdn.net/j3T9Z7H/article/details/94592958
  8. http://openjdk.java.net/projects/jdk/
  9. http://openjdk.java.net/projects/jdk/13/
JVM 启动参数详解:博观而约取、厚积而薄发

JVM 作为一个通用的虚拟机,我们可以通过启动 Java 命令时指定不同的 JVM 参数,让 JVM 调整自己的运行状态和行为,内存管理和垃圾回收的 GC 算法,添加和处理调试和诊断信息等等。本节概括的讲讲 JVM 参数,对于 GC 相关的详细参与将在后续的 GC 章节说明和分析。

直接通过命令行启动 Java 程序的格式为:

java [options] classname [args]java [options] -jar filename [args]

其中:

  • [options] 部分称为 "JVM 选项",对应 IDE 中的 VM options, 可用 jps -v 查看。
  • [args] 部分是指 "传给main函数的参数", 对应 IDE 中的 Program arguments, 可用 jps -m 查看。

如果是使用 Tomcat 之类自带 startup.sh 等启动脚本的程序,我们一般把相关参数都放到一个脚本定义的 JAVAOPTS 环境变量中,最后脚本启动 JVM 时会把 JAVAOPTS 变量里的所有参数都加到命令的合适位置。

如果是在 IDEA 之类的 IDE 里运行的话,则可以在“Run/Debug Configurations”里看到 VM 选项和程序参数两个可以输入参数的地方,直接输入即可。

73146375.png

上图输入了两个 VM 参数,都是环境变量,一个是指定文件编码使用 UTF-8,一个是设置了环境变量 a 的值为 1。

Java 和 JDK 内置的工具,指定参数时都是一个 -,不管是长参数还是短参数。有时候,JVM 启动参数和 Java 程序启动参数,并没必要严格区分,大致知道都是一个概念即可。

JVM 的启动参数, 从形式上可以简单分为:

  • -开头为标准参数,所有的 JVM 都要实现这些参数,并且向后兼容。
  • -X开头为非标准参数, 基本都是传给 JVM 的,默认 JVM 实现这些参数的功能,但是并不保证所有 JVM 实现都满足,且不保证向后兼容。
  • -XX:开头为非稳定参数, 专门用于控制 JVM 的行为,跟具体的 JVM 实现有关,随时可能会在下个版本取消。
  • -XX:+-Flags 形式, +- 是对布尔值进行开关。
  • -XX:key=value 形式, 指定某个选项的值。

实际上,直接在命令行输入 java,然后回车,就会看到 java 命令可以其使用的参数列表说明:

$ java用法: java [-options] class [args...]           (执行类)   或 java [-options] -jar jarfile [args...]           (执行 jar 文件)其中选项包括:    -d32 使用 32 位数据模型 (如果可用)    -d64 使用 64 位数据模型 (如果可用)    -server 选择 "server" VM                  默认 VM 是 server,                  因为您是在服务器类计算机上运行。    -cp <目录和 zip/jar 文件的类搜索路径>    -classpath <目录和 zip/jar 文件的类搜索路径>                  用 : 分隔的目录, JAR 档案                  和 ZIP 档案列表, 用于搜索类文件。    -D<名称>=<值>                  设置系统属性    -verbose:[class|gc|jni]                  启用详细输出    -version 输出产品版本并退出    -version:<值>                  警告: 此功能已过时, 将在                  未来发行版中删除。                  需要指定的版本才能运行    -showversion 输出产品版本并继续    -jre-restrict-search | -no-jre-restrict-search                  警告: 此功能已过时, 将在                  未来发行版中删除。                  在版本搜索中包括/排除用户专用 JRE    -? -help 输出此帮助消息    -X 输出非标准选项的帮助    -ea[:<packagename>...|:<classname>]    -enableassertions[:<packagename>...|:<classname>]                  按指定的粒度启用断言    -da[:<packagename>...|:<classname>]    -disableassertions[:<packagename>...|:<classname>]                  禁用具有指定粒度的断言    -esa | -enablesystemassertions                  启用系统断言    -dsa | -disablesystemassertions                  禁用系统断言    -agentlib:<libname>[=<选项>]                  加载本机代理库 <libname>, 例如 -agentlib:hprof                  另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help    -agentpath:<pathname>[=<选项>]                  按完整路径名加载本机代理库    -javaagent:<jarpath>[=<选项>]                  加载 Java 编程语言代理, 请参阅 java.lang.instrument    -splash:<imagepath>                  使用指定的图像显示启动屏幕有关详细信息, 请参阅 http://www.oracle.com/technetwork/java/javase/documentation/index.html。

7.1 设置系统属性

当我们给一个 Java 程序传递参数,最常用的方法有两种:

  • 系统属性,有时候也叫环境变量,例如直接给 JVM 传递指定的系统属性参数,需要使用 -Dkey=value 这种形式,此时如果系统的环境变量里不管有没有指定这个参数,都会以这里的为准。

  • 命令行参数,直接通过命令后面添加的参数,比如运行 Hello 类,同时传递 2 个参数 kimm、king:java Hello kimm king,然后在Hello类的 main 方法的参数里可以拿到一个字符串的参数数组,有两个字符串,kimm 和 king。

比如我们常见的设置 $JAVA_HOME 就是一个环境变量,只要在当前命令执行的上下文里有这个环境变量,就可以在启动的任意程序里,通过相关 API 拿到这个参数,比如 Java 里:

System.getProperty("key")来获取这个变量的值,这样就可以做到多个不同的应用进程可以共享这些变量,不用每个都重复设置,也可以实现简化 Java 命令行的长度(想想要是配置了 50 个参数多恐怖,放到环境变量里,可以简化启动输入的字符)。此外,由于环境变量的 key-value 的形式,所以不管是环境上下文里配置的,还是通过运行时-D来指定,都可以不在意参数的顺序,而命令行参数就必须要注意顺序,顺序错误就会导致程序错误。

例如指定随机数熵源(Entropy Source),示例:

JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom"

此外还有一些常见设置:

 -Duser.timezone=GMT+08  // 设置用户的时区为东八区 -Dfile.encoding=UTF-8      // 设置默认的文件编码为UTF-8

查看默认的所有系统属性,可以使用命令:

$ java -XshowSettings:properties -versionProperty settings:    awt.toolkit = sun.lwawt.macosx.LWCToolkit    file.encoding = UTF-8    file.encoding.pkg = sun.io    file.separator = /    gopherProxySet = false    java.awt.graphicsenv = sun.awt.CGraphicsEnvironment    java.awt.printerjob = sun.lwawt.macosx.CPrinterJob    java.class.path = .    java.class.version = 52.0...... 省略了几十行

同样可以查看 VM 设置:

$ java -XshowSettings:vm -versionVM settings:    Max. Heap Size (Estimated): 1.78G    Ergonomics Machine Class: server    Using VM: Java HotSpot(TM) 64-Bit Server VM......

查看当前 JDK/JRE 的默认显示语言设置:

java -XshowSettings:locale -versionLocale settings:    default locale = 中文    default display locale = 中文 (中国)    default format locale = 英文 (中国)    available locales = , ar, ar_AE, ar_BH, ar_DZ, ar_EG, ar_IQ, ar_JO,        ar_KW, ar_LB, ar_LY, ar_MA, ar_OM, ar_QA, ar_SA, ar_SD, ......

还有常见的,我们使用 mvn 脚本去执行编译的同时,如果不想编译和执行单元测试代码:

$ mvn package -Djava.test.skip=true

或者

$ mvn package -DskipTests

等等,很多地方会用设置系统属性的方式去传递数据给Java程序,而不是直接用程序参数的方式。

7.2 Agent 相关的选项

Agent 是 JVM 中的一项黑科技, 可以通过无侵入方式来做很多事情,比如注入 AOP 代码,执行统计等等,权限非常大。这里简单介绍一下配置选项,详细功能在后续章节会详细讲。

设置 agent 的语法如下:

  • -agentlib:libname[=options] 启用native方式的agent, 参考 LD_LIBRARY_PATH 路径。
  • -agentpath:pathname[=options] 启用native方式的agent。
  • -javaagent:jarpath[=options] 启用外部的agent库, 比如 pinpoint.jar 等等。
  • -Xnoagent 则是禁用所有 agent。

以下示例开启 CPU 使用时间抽样分析:

JAVA_OPTS="-agentlib:hprof=cpu=samples,file=cpu.samples.log"

其中 hprof 是 JDK 内置的一个性能分析器。cpu=samples 会抽样在各个方法消耗的时间占比, Java 进程退出后会将分析结果输出到文件。

7.3 JVM 运行模式

JVM 有两种运行模式:

  • -server:设置 jvm 使 server 模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有 64 位能力的 jdk 环境下将默认启用该模式,而忽略 -client 参数。

  • -client :JDK1.7 之前在 32 位的 x86 机器上的默认值是 -client 选项。设置 jvm 使用 client 模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或者PC应用开发和调试。

此外,我们知道 JVM 加载字节码后,可以解释执行,也可以编译成本地代码再执行,所以可以配置 JVM 对字节码的处理模式:

  • -Xint:在解释模式(interpreted mode)下,-Xint 标记会强制 JVM 解释执行所有的字节码,这当然会降低运行速度,通常低 10 倍或更多。
  • -Xcomp:-Xcomp 参数与 -Xint 正好相反,JVM 在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。
  • -Xmixed:-Xmixed 是混合模式,将解释模式和变异模式进行混合使用,有 JVM 自己决定,这是 JVM 的默认模式,也是推荐模式。 我们使用 java -version 可以看到 mixed mode 等信息。

示例:

JAVA_OPTS="-server"

7.4 设置堆内存

JVM 的内存设置是最重要的参数设置,也是 GC 分析和调优的重点。

JVM 总内存=堆+栈+非堆+堆外内存。

相关的参数:

  • -Xmx, 指定最大堆内存。 如 -Xmx4g. 这只是限制了 Heap 部分的最大值为 4g。这个内存不包括栈内存,也不包括堆外使用的内存。

  • -Xms, 指定堆内存空间的初始大小。 如 -Xms4g。 而且指定的内存大小,并不是操作系统实际分配的初始值,而是 GC 先规划好,用到才分配。 专用服务器上需要保持 -Xms-Xmx一致,否则应用刚启动可能就有好几个 FullGC。当两者配置不一致时,堆内存扩容可能会导致性能抖动。

  • -Xmn, 等价于 -XX:NewSize,使用 G1 垃圾收集器 不应该 设置该选项,在其他的某些业务场景下可以设置。官方建议设置为 -Xmx1/2 ~ 1/4

  • -XX:MaxPermSize=size, 这是 JDK1.7 之前使用的。Java8 默认允许的 Meta 空间无限大,此参数无效。

  • -XX:MaxMetaspaceSize=size, Java8 默认不限制 Meta 空间, 一般不允许设置该选项。

  • XX:MaxDirectMemorySize=size,系统可以使用的最大堆外内存,这个参数跟-Dsun.nio.MaxDirectMemorySize效果相同。

  • -Xss, 设置每个线程栈的字节数。 例如 -Xss1m 指定线程栈为 1MB,与-XX:ThreadStackSize=1m等价

这里要特别说一下堆外内存,也就是说不在堆上的内存,我们可以通过jconsole,jvisualvm 等工具查看。

RednaxelaFX 提到:

一个 Java 进程里面,可以分配 native memory 的东西有很多,特别是使用第三方 native 库的程序更是如此。

但在这里面除了

  • GC heap = Java heap + Perm Gen(JDK <= 7)
  • Java thread stack = Java thread count * Xss
  • other thread stack = other thread count * stack size
  • CodeCache 等东西之外

还有诸如 HotSpot VM 自己的 StringTable、SymbolTable、SystemDictionary、CardTable、HandleArea、JNIHandleBlock 等许多数据结构是常驻内存的,外加诸如 JIT 编译器、GC 等在工作的时候都会额外临时分配一些 native memory,这些都是 HotSpot VM自己所分配的 native memory;在 JDK 类库实现中也有可能有些功能分配长期存活或者临时的 native memory。

然后就是各种第三方库的 native 部分分配的 native memory。

“Direct Memory”,一般来说是 Java NIO 使用的 Direct-X-Buffer(例如 DirectByteBuffer)所分配的 native memory,这个地方如果我们使用 netty 之类的框架,会产生大量的堆外内存。

示例:

JAVA_OPTS="-Xms28g -Xmx28g"

最佳实践

配置多少 xmx 合适

从上面的分析可以看到,系统有大量的地方使用堆外内存,远比我们常说的 xmx 和 xms 包括的范围要广。所以我们需要在设置内存的时候留有余地。

实际上,我个人比较推荐配置系统或容器里可用内存的 70-80% 最好。比如说系统有 8G 物理内存,系统自己可能会用掉一点,大概还有 7.5G 可以用,那么建议配置

-Xmx6g 说明:xmx : 7.5G*0.8 = 6G,如果知道系统里有明确使用堆外内存的地方,还需要进一步降低这个值。

举个具体例子,我在过去的几个不同规模,不同发展时期,不同研发成熟度的公司研发团队,都发现过一个共同的 JVM 问题,就是线上经常有JVM实例突然崩溃,这个过程也许是三天,也可能是 2 周,异常信息也很明确,就是内存溢出 OOM。

运维人员不断加大堆内存或者云主机的物理内存,也无济于事,顶多让这个过程延缓。

大家怀疑内存泄露,但是看 GC 日志其实一直还挺正常,系统在性能测试环境也没什么问题,开发和运维还因此不断地发生矛盾和冲突。

其中有个运维同事为了缓解问题,通过一个多月的观察,持续地把一个没什么压力的服务器从 2 台逐渐扩展了 15 台,因为每天都有几台随机崩溃,他需要在系统通知到他去处理的这段时间,保证其他机器可以持续提供服务。

大家付出了很多努力,做了一些技术上的探索,还想了不少的歪招,但是没有解决问题,也就是说没有创造价值。

后来我去深入了解一下,几分钟就解决了问题,创造了技术的价值,把服务器又压缩回 2 台就可以保证系统稳定运行,业务持续可用了,降低成本带来的价值,也得到业务方和客户认可。

那么实际问题出在哪儿呢?一台云主机 4G 或 8G 内存,为了让 JVM 最大化的使用内存,服务部署的同事直接配置了xmx4g 或 xmx8g。因为他不知道 xmx 配置的内存和 JVM 可能使用的最大内存是不相等的。我让他把 8G 内存的云主机,设置 xmx6g,再也没出过问题,而且让他观察看到在 Java 进程最多的时候 JVM 进程使用了 7G 出头的内存(堆最多用 6g, java 进程自身、堆外空间都需要使用内存,这些内存不在 xmx 的范围内),而不包含 xmx 设置的 6g 内存内。

xmx 和 xms 是不是要配置成一致的

一般情况下,我们的服务器是专用的,就是一个机器(也可能是云主机或 docker 容器)只部署一个 Java 应用,这样的时候建议配置成一样的,好处是不会再动态去分配,如果内存不足(像上面的情况)上来就知道。

7.5 GC 日志相关的参数

在生产环境或性能压测环境里,我们用来分析和判断问题的重要数据来源之一就是 GC 日志,JVM 启动参数为我们提供了一些用于控制 GC 日志输出的选项。

  • -verbose:gc :和其他 GC 参数组合使用, 在 GC 日志中输出详细的GC信息。 包括每次 GC 前后各个内存池的大小,堆内存的大小,提升到老年代的大小,以及消耗的时间。此参数支持在运行过程中动态开关。比如使用 jcmd, jinfo, 以及使用 JMX 技术的其他客户端。

  • -XX:+PrintGCDetails-XX:+PrintGCTimeStamps:打印 GC 细节与发生时间。请关注我们后续的 GC 课程章节。

  • -Xloggc:file:与-verbose:gc功能类似,只是将每次 GC 事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。若与 verbose:gc 命令同时出现在命令行中,则以 -Xloggc 为准。

示例:

export JAVA_OPTS="-Xms28g -Xmx28g -Xss1m \-verbosegc -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/"

7.6 指定垃圾收集器相关参数

垃圾回收器是 JVM 性能分析和调优的核心内容之一,也是近几个 JDK 版本大力发展和改进的地方。通过不同的 GC 算法和参数组合,配合其他调优手段,我们可以把系统精确校验到性能最佳状态。

以下参数指定具体的垃圾收集器,详细情况会在第二部分讲解:

  • -XX:+UseG1GC:使用 G1 垃圾回收器
  • -XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器
  • -XX:+UseSerialGC:使用串行垃圾回收器
  • -XX:+UseParallelGC:使用并行垃圾回收器

7.7 特殊情况执行脚本的参数

除了上面介绍的一些 JVM 参数,还有一些用于出现问题时提供诊断信息之类的参数。

  • -XX:+-HeapDumpOnOutOfMemoryError 选项, 当 OutOfMemoryError 产生,即内存溢出(堆内存或持久代)时,自动 Dump 堆内存。因为在运行时并没有什么开销, 所以在生产机器上是可以使用的。示例用法: java -XX:+HeapDumpOnOutOfMemoryError -Xmx256m ConsumeHeap
java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid2262.hprof .........
  • -XX:HeapDumpPath 选项, 与HeapDumpOnOutOfMemoryError搭配使用, 指定内存溢出时 Dump 文件的目录。如果没有指定则默认为启动 Java 程序的工作目录。示例用法: java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/ ConsumeHeap自动 Dump 的 hprof 文件会存储到 /usr/local/ 目录下。

  • -XX:OnError 选项, 发生致命错误时(fatal error)执行的脚本。例如, 写一个脚本来记录出错时间, 执行一些命令, 或者 curl 一下某个在线报警的url.示例用法: java -XX:OnError="gdb - %p" MyApp可以发现有一个 %p 的格式化字符串,表示进程 PID。

  • -XX:OnOutOfMemoryError 选项, 抛出 OutOfMemoryError 错误时执行的脚本。

  • -XX:ErrorFile=filename 选项, 致命错误的日志文件名,绝对路径或者相对路径。

本节只简要的介绍一下 JVM 参数,其实还有大量的参数跟 GC 垃圾收集器有关系,将会在第二部分进行详细的解释和分析。

参考资料

  • 如何比较准确地估算一个Java进程到底申请了多大的Direct Memory?:https://www.zhihu.com/question/55033583/answer/142577881
  • 最全的官方JVM参数清单:https://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
常用性能指标:没有量化,就没有改进
JVM 基础知识:不积跬步,无以至千里
Java 字节码技术:不积细流,无以成江河
Java 类加载器:山不辞土,故能成其高
Java 内存模型:海不辞水,故能成其深
JDK 内置命令行工具:工欲善其事,必先利其器
JDK 内置图形界面工具:海阔凭鱼跃,天高任鸟飞
JDWP 简介:十步杀一人,千里不留行
JMX 与相关工具:山高月小,水落石出
常见的 GC 算法介绍(Parallel/CMS/G1):温故而知新
Java11 ZGC 和 Java12 Shenandoah 介绍:苟日新、日日新、又日新
Oracle Graalvm 介绍:会当凌绝顶、一览众山小
GC 日志解读与分析:千淘万漉虽辛苦,吹尽狂沙始到金
JVM 的线程堆栈等数据分析:操千曲而后晓声、观千剑而后识器
内存 dump 和内存分析工具:万里赴戎机、关山度若飞
fastthread 相关的工具介绍:欲穷千里目,更上一层楼
面临复杂问题时的几个高级工具:它山之石,可以攻玉
JVM 问题排查分析调优经验:操千曲而后晓声,观千剑而后识器
JVM 相关的常见面试问题汇总:运筹策帷帐之中,决胜于千里之外
应对容器时代面临的挑战:长风破浪会有时、直挂云帆济沧海

阅读全文: http://gitbook.cn/gitchat/column/5de76cc38d374b7721a15cec

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页