JVM 对 Java 有多重要,对程序员面试有多重要,这些不用多说。

如果你还没意识到学 JVM 的必要性,或者不知道怎么学 JVM,那么看完这篇文章,你就能知道答案了。

曾经的我很不屑于学 JVM,但是后来发现不学不行。这就像和媳妇吵架之后我不想道歉一样,不道歉那是不可能的,道歉是早晚的事儿,逃不掉。

后来我明白了:

认怂越晚,结局越惨。

但是我学的时候才知道:JVM,你太过分了,太难学了!

我的学习过程可以说非常坎坷,不过经历坎坷之后,我倒是发现学 JVM 的门道很多。

以我的经验加上和同行们的交流,我认为学 JVM 最好的方法是:

在程序员不同的水平段,做精准的学习。

所谓的精准学习,就是学习对自己工作有巨大帮助的知识点。以工作内容带动学习,等到积累多了,再一举攻克所有 JVM 知识点,最终熟练掌握 JVM 底层原理。

下面我来说说初级、高级、资深程序员,如何循序渐进、分步学习。

初级程序员怎么学

对刚入行的新手程序员,工作一般是修复简单 bug、开发简单功能。如何编码少出 bug,是这个阶段的核心问题。

对于这个核心问题,JVM 原理必须深入掌握两个知识点。

1. 类的初始化

类的初始化,要了解的非常深入才可以。否则,一不留神就会往项目里引入一些有关初始化的 bug。

比如看看下面这段代码:

public class ParentClass {
private int parentX;
public ParentClass() {
setX(100);
}
public void setX(int x) {
parentX = x;
}
} public class ChildClass extends ParentClass{
private int childX = 1;
public ChildClass() {}
@Override
public void setX(int x) {
super.setX(x);
childX = x;
System.out.println("ChildX 被赋值为 " + x);
}
public void printX() {
System.out.println("ChildX = " + childX);
} } public class TryInitMain {
public static void main(String[] args) {
ChildClass cc = new ChildClass();
cc.printX();
}
}

有兴趣可以运行看看结果,一旦把这种代码放到了生产环境里,排查非常困难。

2. Java 内存结构和对象分配

第二个知识点,就是 Java 内存结构和对象分配的基础知识,尤其是 JVM 内存中堆的布局和对象分配的关系。

比如,堆内存的布局

当然,Java7 后,新布局变了

知道布局了,就得知道java对象分配的基本原则:

  • 对象优先在Eden区分配
  • 对象太大直接会分配到老年代

只有知道这些知识,才不会经常写下底下这种 bug:

// 将全部行数读取的内存中
List<String> lines = FileUtils.readLines(new File("temp/test.txt"), Charset.defaultCharset());
for (String line : lines) {
// pass
}

上面这段代码,一旦读取到了大文件,很可能把生产环境搞崩。

所以,把上述两个知识点深入理解了,对新手提升自己的代码质量非常非常有用。只有代码质量上去了,你才能得到更好的发展。

对于这两个知识点,我认为通过网络的文章去学习最好。如果直接看书,有两个最大的缺点:

  • 知识积累不足导致学不懂
  • 书中冗余知识点太多,互相交杂,精力耗费过大,性价比不高

故这里学习推荐根据知识点去搜文章读,而不是找原理性的书籍看。

高级程序员怎么学

对处于这个阶段的朋友,他们已经可以熟练编写健壮的代码了,经常会独立开发出一个大的功能模块,有的可能还能独立开发出一个完整的小型项目。

这时候,他们可能会面临两种情况:

1. 需要写一些工具类给全团队使用

在这种情况下,你很可能就需要 Java 中的语法糖,因为语法糖能让你写出非常灵活简单的代码。这包括泛型,自动拆装箱,可变参数还有遍历循环。

但是,使用这些语法糖的时候,如果你不熟悉他们在 JVM 中的实现原理,就非常容易栽个大跟头,

比如:

public class GenericPitfall {
public static void main(String[] args) {
List list = new ArrayList();
list.add("123");
List<Integer> list2 = list;
System.out.println(list2.get(0).intValue());
}
}

2. 编写性能优越的代码

什么时候需要性能优越的代码?最常见的就是把以前性能不好的同步实现,转化成异步实现。

而这种要求,就需要开发对 Java 的多线程开发非常熟悉,并且一定要深入理解多线程在 JVM 中的原理实现。

不然,可以看看下面这段代码:

class IncompletedSynchronization {
int x; public int getX() {
return x;
} public synchronized void setX(int x) {
this.x = x;
}
}

再看看这段:

Object lock = new Object();
synchronized (lock) {
lock = new Object();
}

如果把上面这些代码上了生产环境,熬通宵排查问题的命运就注定了……

这里的知识点,我推荐通过网上的文章看,又因为涉及到了并发知识,我建议就着《Java Performance》第二版的“Chapter 9. Threading and Synchronization Performance”这章一起看。

还有余力,建议再继续看周志明的那本《深入理解 JAVA 虚拟机》第三版中的 12-13 章。周志明这本书讲的十分深入,也带来个缺点:门槛高。此时,如果没看懂可以放一放。

注意,我这里说的是并发的原理,不是并发实践,读者想学并发编程,《JAVA 并发编程实践》我认为是前提条件,故不会赘述。

资深程序员怎么学

这时候的你,已经开始承担项目开发中很重要的职责了,有些出色的朋友都开始带团队了。那这时候,你可能会做下面的事:

1. 合理规划项目使用资源

合理规划项目使用资源,前提是对垃圾回收有非常深入的了解。

如果说在新手期,已经对 Java 对象的内存分配和内存使用有了大致的概念,那么,这个垃圾回收,则是这类知识的进一步拓展。

只有理解了各种垃圾回收的原理,再配合着 Java 内存布局的基础知识,才能更好地规划出项目用什么回收算法,才能在合适的资源利用度上得到最佳性能。

比如,新生代和老年代之间的合适比例。比如,新生代中 Eden 和 Survivor 区域间的比例。

2. 排查各种线上问题

要排查各种问题,就需要对 JVM 提供的各种故障排查工具非常了解。

这些工具又分为两类:

  • 基础的命令行形式的故障处理工具,比如 jps、jstack 等等
  • 第二类是可视化的故障处理工具,比如 VisualVM

但是,掌握工具的使用还不够。因为有关垃圾回收的问题,还必须得通过解析 GC 日志后,再通过工具的使用,才可能能定位到问题的根源。

所以,最好对使用故障排查工具和 GC 日志都非常熟练。

比如:

2021-05-26T14:45:37.987-0200: 151.126:
[GC (Allocation Failure) 151.126: [DefNew: 629119K->69888K(629120K), 0.0584157 secs] 1619346K->1273247K(2027264K), 0.0585007 secs]
[Times: user=0.06 sys=0.00, real=0.06 secs] 2021-05-26T14:45:59.690-0200: 172.829:
[GC (Allocation Failure) 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs]172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs] 1832479K->755802K(2027264K), [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs]
[Times: user=0.18 sys=0.00, real=0.18 secs]

上面这条,应该一眼看出来,垃圾算法用的是 Serial 收集器,并且年轻代分配出现了问题,大小可能需要调整。

这里的知识点,强烈反对看网上的文章,网上说的很多细节有问题,疏漏很多。所以,推荐看书。

《Java Performance》第二版里,“Chapter 5. An Introduction to Garbage Collection”,“Chapter 6. Garbage Collection Algorithms”的知识已经足够。

有人去看《深入理解 JAVA 虚拟机》第三版中的第 3 章,讲垃圾收集器与内存分配策略的。这里还是老问题,讲的太细,我建议绕过 3.4 节,讲 HotSpot 算法细节的那块儿。

这里安全点这个知识点挺重要,但是现在这个阶段想理解挺难的。我觉得将来做一些底层框架,接触到崩溃恢复的 checkpoint 相关思想了,再回头来学习,那才能真正理解和掌握。

技术专家怎么学

达到这个级别了,那就需要对整套 JVM 要有非常深入的了解了,因为你是解决技术问题的最后保障了。有些时候,甚至还需要因为某些问题开发出各种各样的工具。

曾经,有个项目时不时总是会报错:

java.lang.OutOfMemoryError: GC overhead limit exceeded

这个问题几个同事都没搞定,就来找我。我看了看,突然想起来,以前在官方调优指南《HotSpot Virtual Machine Garbage Collection Tuning Guide》看到过相关介绍。

JVM 本身内存不足就会运行 GC,但是如果每次 GC 回收的内存不够,那么很快就会开始下一次 GC。

JVM 有个默认的保护机制,如果发现在一个统计周期内,98% 的时间都是在运行 GC,内存回收却少于 2% 的时候,就会报这个错。

怎么引起的呢?这个问题如果去排查代码,那真的是难如登天,首先,没有任何堆栈错误去帮助定位问题。其次,项目代码量大了去了,而且是年头久远。

这时,就需要通过对 JVM 总体的深入理解,去反推问题了。我当时是这样推理的:

内存溢出,GC 无法回收问题,说明了两个问题:

  1. 堆内的内存不够用了
  2. 占用内存的对象要么就是该关闭的资源没有关闭,要么被大量的暂时放在一起了

那如果我 dump 出内存文件出来,再分析下就知道是哪些对象在占用内存了。

一查发现是大量的字符串在占用内存。

综合我前面的推测,字符串不是数据库连接,肯定没有该关闭未关闭的问题。那就剩一个可能了,就是被大量的暂时放起来了,导致 GC 回收不了。

那么新问题来了,能大量放字符串的,会是什么?

首先就去猜缓存。根据这条线索,直接去源码搜 Cache 关键词,把所有关于 Cache 的代码都看了下。一下子就找到问题了。

原来,我们有个功能是解析一个非常大的文件。文件的格式如下:

需要把这个文件的每一行内容按照列去一起存到数据库里。

由于写代码的人偷懒,想一次解析完毕后一股脑全塞到数据库里。所以,他弄了个 Map,Map 的 Key 是相同的列名,Value是每一行解析过的内容。

而这样写代码的结果就是,一行对应了一个有三个条目的 HashMap。如果文件有十几万行,就有十几万的 HashMap。然后,这些 HashMap 再存到一个列表里,再把这个列表放到一个叫做 xxxCache 的 HashMap 中。

示意代码如下:

public class ParseFile4OOM {
public static void main(String[] args) {
List<Map<String, String>> lst = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
Map<String, String> map = new HashMap<>();
map.put("Column1", "Content1");
map.put("Column2", "Content2");
map.put("Column3", "Content3");
lst.add(map);
} Map<String, List<Map<String, String>>> contentCache = new HashMap<>();
contentCache.put("contents", lst);
}
}

那对这种情况怎么办呢?代码还不能大动,只能优化。

那时,我们已经用了 JDK8 了,引入了 String 常量池。同时,Hashmap 在这个业务场景下,容积是固定的,所以,就不应该给它多分配空间,就固定死为 3。

优化后,代码如下:

public class ParseFile4OOM {
public static void main(String[] args) {
List<Map<String, String>> lst = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
Map<String, String> map = new HashMap<>(3);
map.put("Column1".intern(), "Content1".intern());
map.put("Column2".intern(), "Content2".intern());
map.put("Column3".intern(), "Content3".intern());
lst.add(map);
} Map<String, List<Map<String, String>>> contentCache = new HashMap<>();
contentCache.put("contents".intern(), lst);
}
}

把优化后的代码上线,错误搞定了!

所以,在这个阶段就非得把 JVM 吃透不可了。吃透原理就必须靠看书了。

周志明的《深入理解 JAVA 虚拟机》是必须的了,但是还不够。

《Oracle JRockit: The Definitive Guide》这本书我也建议读一读,虽然老了,但是里面的很多内容,尤其前四章,对 JVM 原理真的快讲透了。对 JVM 是如何弹性伸缩去平衡资源和性能关系的,娓娓道来,让我醍醐灌顶,编程视野一下子打开了很多。

至此,不同阶段的学习方法讲完了。

总的来说,JVM 知识广博复杂,如果想要掌握,不能一蹴而就。而且咱们程序员不容易,需要学的知识太多,然而咱们的精力却是有限的。

所以,对于 JVM 原理来说,假设有些知识点眼前看不懂,用不上,可以先暂时放一放,做到精准学习,把省下来的精力用在别的知识甚至自己的生活上,更有意义。

看完如果觉得有收获,希望能随手点个赞。


你好,我是四猿外。

一家上市公司的技术总监,管理的技术团队一百余人。

我从一名非计算机专业的毕业生,转行到程序员,一路打拼,一路成长。

我会把自己的成长故事写成文章,把枯燥的技术文章写成故事。

欢迎关注我的公众号。

JVM,我就不信学不会你了的更多相关文章

  1. RocketMQ在面试中那些常见问题及答案+汇总

    0.汇总 RocketMQ入门到入土(一)新手也能看懂的原理和实战! RocketMQ入门到入土(二)事务消息&顺序消息 从入门到入土(三)RocketMQ 怎么保证的消息不丢失? Rocke ...

  2. 关于课堂测试ATM系统的总结

    第一节课就是考试,是要求用Java语言编写模仿ATM的系统操作,说实话真的好难,Java语言,王主任是让我们自学的,然后就让我们写一个这比较大的程序,好难,也可能是我太笨了吧... 不过话说回来,说到 ...

  3. 索引很难么?带你从头到尾捋一遍MySQL索引结构,不信你学不会!

    前言 Hello我又来了,快年底了,作为一个有抱负的码农,我想给自己攒一个年终总结.自上上篇写了手动搭建Redis集群和MySQL主从同步(非Docker)和上篇写了动手实现MySQL读写分离and故 ...

  4. 蚂蚁金服寒泉子:JVM源码分析之临门一脚的OutOfMemoryError完全解读

    ➠更多技术干货请戳:听云博客 概述 OutOfMemoryError,说的是java.lang.OutOfMemoryError,是JDK里自带的异常,顾名思义,说的就是内存溢出,当我们的系统内存严重 ...

  5. JVM基本结构

    以下是JVM的一个基本架构图,在这个基本架构图中,栈有两部份,Java线程栈以及本地方法栈,栈的概念与C/C++程序基本上都是一个概念,里面存放的都是栈帧,一个栈帧代表的就是一个函数的调用,在栈帧里面 ...

  6. [转]JVM内存堆布局图解分析

    JAVA能够实现跨平台的一个根本原因,是定义了class文件的格式标准,凡是实现该标准的JVM都能够加载并解释该class文件,据此也可以知道,为啥Java语言的执行速度比C/C++语言执行的速度要慢 ...

  7. jdk、jre、jvm的关系

    JDK里面的工具也是用JAVA编写的,它们本身运行的时候也需要一套JRE,如C:\Program Files\Java\jdk1.5.x\目录下的JRE.而C:\Program Files\Java\ ...

  8. java jvm学习笔记十一(访问控制器)

     欢迎装载请说明出处: http://blog.csdn.net/yfqnihao/article/details/8271665 这一节,我们要学习的是访问控制器,在阅读本节之前,如果没有前面几节的 ...

  9. JVM内存堆布局图解分析

    JAVA能够实现跨平台的一个根本原因,是定义了class文件的格式标准,凡是实现该标准的JVM都能够加载并解释该class文件,据此也可以知道,为啥Java语言的执行速度比C/C++语言执行的速度要慢 ...

随机推荐

  1. python3 使用random函数批量产生注册邮箱

    '''你是一个高级测试工程师,现在要做性能测试,需要你写一个函数,批量生成一些注册使用的账号. 1.产生的账号是以@163.com结尾,长度由用户输,产生多少条也由用户输入,2.用户名不能重复,用户名 ...

  2. 10.15 wget:命令行下载工具

    wget命令   用于从网络上下载某些资料,该命令对于能够连接到互联网的Linux系统的作用非常大,可以直接从网络上下载自己所需要的文件. wget的特点如下: 支持断点下载功能. 支持FTP和HTT ...

  3. 基于开源Tars的动态负载均衡实践

    一.背景 vivo 互联网领域的部分业务在微服务的实践过程当中基于很多综合因素的考虑选择了TARS微服务框架. 官方的描述是:TARS是一个支持多语言.内嵌服务治理功能,与Devops能很好协同的微服 ...

  4. System Verilog MCDF(二)

    整形器的接口时序: reg,grant是维持了两个clk的. chid ,length在发送数据期间不可以变化. 第一个data数据必须在start上升沿的同一个clk发送. reg,grant两者之 ...

  5. 一:windows10开启虚拟化服务(也可用于部署docker提前准备)

    查看虚拟化已开启: 如果未启用,则需要添加虚拟化功能:控制面板 -> 启用或关闭Windows功能 选择Hyper-V的所有功能,确定: 系统会自动搜索并安装功能.安装完毕即可. 完结,撒花~~

  6. 密码学系列之:碰撞抵御和碰撞攻击collision attack

    密码学系列之:碰撞抵御和碰撞攻击collision attack 简介 hash是密码学和平时的程序中经常会用到的一个功能,如果hash算法设计的不好,会产生hash碰撞,甚至产生碰撞攻击. 今天和大 ...

  7. [leetcode] 75. 分类颜色(常数空间且只扫描一次算法)

    75. 分类颜色 我们直接按难度最高的要求做:你能想出一个仅使用常数空间的一趟扫描算法吗? 常数空间 只能扫描一趟.注意,是一趟,而不是O(n) 题中只会出现3个数字:0,1,2.换句话说,0肯定在最 ...

  8. CVPR2020无人驾驶论文摘要

    CVPR2020无人驾驶论文摘要 无人 导读/ Starsky是一种比较独特的方案.它是在高速上自动驾驶,第一公里最后一公里采用远程驾驶的模式,Starsky的卡车可以由人类远程操作.没有使用较为昂贵 ...

  9. GitHub上开源的YOLOv5

    GitHub上开源的YOLOv5 代码地址:https://github.com/ultralytics/YOLOv5 该存储库代表Ultralytics对未来的对象检测方法的开源研究,并结合了我们在 ...

  10. 深度学习加速器堆栈Deep Learning Accelerator Stack

    深度学习加速器堆栈Deep Learning Accelerator Stack 通用张量加速器(VTA)是一种开放的.通用的.可定制的深度学习加速器,具有完整的基于TVM的编译器堆栈.设计了VTA来 ...