前言
最近一年面试很多人,因为公司预算有限,所以学校较好、资历较好的都不来的。在面试和接触过程中,发现很多人的问题在于自制力和学习能力。自制力这方面我自认做的也不好,不过对学习还略有经验,毕竟这么多年总是在不断切换航道 😭😭😭。
刚好本人用了大概一年Java,但还没有详细了解过JVM(断续在工作过程了解过一点),就以此为例,梳理下本人的学习过程。
整体目标和阶段划分
目标正如标题所言,入门,不求深入,学习完成后:能把握住JVM的核心概念、能指导后续的开发。
整体分为几个阶段:
- 收集资料: 找出所用学习材料并大体梳理学习目标
- 快速浏览: 快速的掌握全局的概念和架构,知道后续学习的重点,注意不要纠缠细节
- 梳理概念: 真正的学习过程, 将重点和不清晰的概念梳理并记录,重点不是记忆,是前后、上下层次之间的理解
- 整体输出: 根据记录和思考,做回顾并梳理一份学习材料
上述阶段,虽然看上去阶段分明,在实际的学习过程中,实际上是没那么明显的区分的,比如:在快速浏览时,看到一个完全无法理解概念,我也会快速检索下Google,但是会注意不要陷入进去,如果还是无法理解就记录下来。
一. 收集学习资料
目的: 梳理学习过程中所需要的学习材料,大致明确学习需要达成的目标,为后续打基础
1.1 检索学习材料
google 检索关键词 : 中文 - jvm 入门
英文 - jvm learning
然后通过搜索结果相互的推荐、关联找需要的材料
看看我搜到了什么, 大部头:
- Java SE 规范 里面有各版本JVM规范,我挑了Java8
- Java虚拟机规范(Java SE 8版) (豆瓣) 中文版,自己去买吧
- A Comprehensive Introduction to Java Virtual Machine (JVM) | Udemy 付费教程,这次我不准备花钱,只花时间
- 深入理解Java虚拟机(第2版) (豆瓣) 自己去买
- Apache Commons BCEL™ – Home 分析和操作 class文件
学习笔记:
- Java虚拟机规范(Java SE 8版)读后总结 - 简书
- 《深入理解Java虚拟机》笔记_第一遍 - 简书 这个整理的真不错
- 《Java虚拟机规范》阅读(一):简介和Java虚拟机结构 - 朱样年华 - 博客园
1.2 暂定目标
快速翻一下各材料,感觉觉得不错的,看的人多的,先留下来
暂定学习目标 - 了解概念:
- 了解JVM概貌
- 内存管理
- class文件结构
- GC基本概念
所以是暂定,因为随着学习的深入,文档会越看越多,目标也会调整。
暂定学习材料:
- 《深入理解Java虚拟机》笔记_第一遍 - 简书
- 《Java虚拟机规范》阅读(一):简介和Java虚拟机结构 - 朱样年华 - 博客园
- Java虚拟机规范(JavaSE7).pdf -- 中文的
- 深入理解Java虚拟机(第2版) (豆瓣)
二. 快速浏览
目的: 对概念有全局的了解,避免陷入太多细节中,就像看风景先扫一眼,然后才会逐步关注到那朵花更美。
<江雪>: 千山鸟飞绝,万径人踪灭,孤舟蓑笠翁,独钓寒江雪。
先是群山间没鸟、然后路上都没人,接着江边有个孤舟、上有人、在钓鱼,体会下由大到小,由广到细。很多事情都这样,学习也是这样,先有全局的把握很重要。
个人对学习过程中对知识点要掌握的三个要点: Why,What,How。具体的掌握的顺序根据实际会有不同,但个人尤其喜欢思考 Why,因为具体的点有时很难记忆,但为什么比较容易串联起不同的知识领域。
过程, 虽然说快速过一遍,但是工作忙碌、家事繁杂,断续花了10天左右:
- 两个学习笔记比较简短,并提炼了很多概念,快速看一遍。
- 接入快速看了一遍 《深入理解Java虚拟机(第2版)》
- 简单的翻一遍 《Java虚拟机规范(JavaSE7)》,就看基本概念,详细的指令一略而过
记录一些思考和理解:
-
为何要有JVM?
因为Java的目标是到处运行啊,但CPU、OS等差异如此大,那做个抽象层就呼之欲出来。这个抽象的运行环境就是JVM - Java虚拟机,抽象的描述就是 class文件。
记录一些过程中不明白的问题:
- JVM 基于栈,但是大部分CPU都是基于寄存器,那实际中JVM有没有将操作栈映射到寄存器?如果都在内存计算也太慢了,这不符合现在的java程序性能状况。
- JVM 整体架构是什么?每个地方都讲到了,但是这些文章好像都没讲到全局的概念。
- JVM 指令如何分类?如何抽象出这么少的指令来保证程序逻辑?
其实很多问题都没记录在此,只是在书上随意标记了下
三. 梳理概念
主要是梳理下WHY和自己心中的一些困惑,通过思考将梳理,其次将整体的概念按照一定组织形式进行梳理。就是一堆的WHY和WHAT,但无需追求全面,主要是适合自己最重要。
3.1 JVM 的历史?
JVM的历史基本上也是Java语言的历史,对任何技术了解的它的历史对于知道其为何是今天这个样子是非常有帮助的。《深入理解 Java 虚拟机 》1.3节 对此有比较细致的介绍。
梳理了下个人认为其中最重要的一些:
- 1991年最初的目标是能在各种消费电子产品上能运行的程序架构,前身:Oak。(😂,今天万物互联的时代,大家依然在试图统一该目标,Android Things、AliOS Things)
- 但其并不成功,1995年随着互联网的发展,快速找到新的定位,Java 正式提出,迅速占领市场。( 好玩的事情来了,网景公司为了推广其浏览器,与Sun结盟,未来的网页脚本语言必须"看上去与Java足够相似", Brendan Eich 受命之下10天设计了 Javascript,现在 Javascript 统一了前端天下 )
- 1998年,JDK 1.2 发布,HotSpot VM、JIT引入,Collections 集合类
- 2002年,JDK 1.4,真正成熟的版本,正则表达式、异常链、NIO、日志类、XML解析器和XSLT转换器
- 2004年,JDK 1.5,受 .Net 平台刺激,发布大量语法易用性的特性
- 2009年,Sun 被 Oracle收购,JDK 1.7一直难产到2011年才发布了经过大幅度裁剪的版本。(那几年似乎也是基于JVM的其它语言快速发展的几年)
- 2014年,JDK 1.8 发布,提供了大量函数式特性,引入lambda\流,这应该Java大量收复失地的版本吧(新闻: Java是2015年度编程语言)。 说句多余话,现在学习Java,应该从 Java8 开始学起了。
其它参考:
3.2 JVM 解决什么?JVM 到底抽象了什么?JVM 整体架构是什么?
参考:
- 最主要是解决 Java 的运行时环境,完整的运行环境还要包括相关库文件,
class
文件就是运行时的抽象描述。 - JVM 本身和 Java 语言并不直接关联,这也是设计者的设计目标,是其它基于 JVM 的语言能在其上运行并能和 Java 互操作的基础。
- 主要抽象了:
- 二进制可执行文件 -
class
文件,代表一个类或接口 - 各种数据类型:基本类型、类类型等
- 指令集
- 内存管理布局
- 基于栈执行指令
- 异常处理
- 类操作库
- 其架构主要包括:
- ClassLoader 负责 class 文件的加载,实现类的加载、验证、链接、初始化
- 内存管理:方法区、堆、栈、PC 计数器
- 执行引擎
- 本地方法接口 - Java 用来调用本地方法(C/C++)的框架
- 本地方法库
3.3 为什么要GC?
先抛开GC这个问题,计算机内任何资源的使用都有一个问题,就是分配、共享和回收的问题。比如CPU是算力资源-基本上从时间维度来划分;硬盘是存储资源 - 基本以空间维度划分。
在一个程序中,程序员可见的资源中,使用最频繁的资源就是内存,其它像CPU、寄存器、L1 Cache之类对大部分程序这都是不可见的,外部存储、网络资源等虽然可见,但使用上基本上都有相应比较简单的抽象,使用频率(从编码角度)没那么高;内存就不同了,程序员时刻都在和内存打交道,在栈上的临时变量我们需要操心,但更多的是在堆上。
最初的内存管理都是程序员手动管理的,比如C语言中 malloc、free ,C++ 中 new 、 delete,其基本原则说来很简单,要用的时候分配,用完了释放,但是啥使用要用这个简单,重点就在与啥时候算用完了啊??!!比如:
- B 模块调用 A 模块的接口,A 返回一个对象作为结果,B 用完了是不是要释放这个对象?答案是:麻烦看仔细接口说明。有兴趣的可以自己思考下 A 还是 B 来释放合理?
- E 对象在多个函数那使用到,有些情况下在 函数 A 就使用结束了,有些情况下,在函数 B 里还要使用。
- 多线程共享同一个对象 T,线程全部退出时释放 T,代码里何时如何怎么释放T?
- 函数逻辑比较长,每次返回时记得将内存释放(C 语言里
goto
依然被推荐的使用场景)
上面的例子只是说明下实际中知道 用完了 是多么困难,在加上我们的脑子在处理超过7行代码时就开始犯困的特性,即使非常明确的情况下遗漏也是家常便饭。看看业界有多少静态、动态分析工具针对内存泄漏就知道这个问题多麻烦了。
最主要的,老板花钱请你是帮助实现业务,写那么多内存管理能赚钱吗?能帮助分析下个季度的流行趋势吗?应该不能吧。。。
既然内存分配简单,释放麻烦,那如果内存能自动释放就能完美了!这个内存自动回收就是 GC ,具体怎么做呢?
3.4 GC大概是怎么做的?
算法:
引用计数算法
- 引用值为空就清除
- 简单、高效,但需解决循环引用问题,
A -> B -> A
- 很多人认为其不能算是真正的 GC,因为要解决循环引用还是需要开发人员介入,Java 没有采用该方式
Java 使用的是 可达性分析算法:
- 标记-清除算法
- 标记 - 找出要回收的内存
- 清除 - 回收标记的内存
- 标记和清除效率低、内存碎片
- 标记阶段因为需要
- 复制算法
- 内存空间一分为二,直接将要保留的拷贝到另一半
- 浪费空间
- 标记-整理
- 将要保留的对齐移动
- 分代收集
- 将区域分为老生代和新生代
- 新生代 - 每次都只有少量存活,采用复制算法
- 老生代 - 存活率高,标记-整理/标记-清除
重点: "Stop-The-World" 和 SafePoint:
由于内存什么分配是无法预知的,而标记是需要时间的,所以为了让标记能在有限时间内完成,就需要暂停程序的运行,"Stop-The-World" 就是这么来滴。
那是不是所有地方都可以停下来呢,我们简单推导一下就会发现是不能的,举个例子:游戏保存时候,如果人物正在打怪,那重新读取后,如何保证人物还是正在打这个怪?如果单机游戏,是可以的,但是需要额外保存大量信息,如果是网络游戏,别忘记还有别的玩家呢!游戏这种玩家当前状态其实不重要,但是程序不行。Java是个多线程程序 - 多个玩家,同时大量信息的保存花费的代价是不值得的。
时代是发展,GC 也没有完美的,所以 Java 有多种 GC 实现:
- Serial / Serial Old
- ParNew
- Parallel Scavenge
- CMS
- G1
G1的实现很奇妙,将整个Java堆划分成多个大小相等的区域,新生代和老生代只是部分区域的集合,跟踪所有区域,确定回收价值。
Mark下,后面找时间仔细研究下这个实现。
GC 将内存回收自动处理了,也就必须把分配也做了。
内存分配策略:
- 对象优先在Eden分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。 - 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 动态对象年龄判定
- 空间分配担保
额外的话:
具体采用什么方式,都是看应用场景,在工程上是需要做平衡的,比如 C 语言的一些应用场景:内核、嵌入式等领域,GC的所需的额外消耗、停顿、无法预测等是无法接受的。
苹果的objc、swift就采用了 ARC - 也就是引用计数的方式实现内存回收,这需要程序员在编码时额外处理循环引用的问题。但是带来的内存回收系统本身的简化、响应速度的保证 - 这是为何 iOS 系统内存占用少、响应快的因素之一。这也是一种平衡!
在实际使用中,选择那个 GC 实现也是一种平衡,目前来看 G1 是对多数服务端程序更好的选择,但肯定不是所以都最好的。
3.5 JVM 指令如何分类?如何抽象出这么少的指令来保证程序逻辑?
- 数据需要搬到操作栈,再搬回去 --> 加载存储指令
- 数据搬来了要算一下 --> 运算指令
- 有时候还要变一下 --> 类型转换指令
- 别忘记还有类呢 --> 对象创建与操作
- 操作数栈也要管理啊 --> 操作数栈管理指令
- if、else啥逻辑总有的 --> 控制转移指令
- 函数调用啥的 --> 方法调用和返回指令
- 调用过程中万一异常了呢 --> 抛出异常
- 多线程需要啥,同步啊 --> 同步
算一算全了。
3.6 其它问题
一. JVM 基于栈,但是大部分CPU都是基于寄存器,那实际中JVM有没有将操作栈映射到寄存器?
答案肯定是:有!
- JIT在编译过程中会将
bytecode
转化成机器码,这段代码 就和 C 编译代码没什么区别了 - 即使没有优化,那么基于栈的执行过程中,系统依然会使用到寄存器,但是个人怀疑效率感人
- 还有部分方法本身就是使用的本地代码实现的
另外一点,为何 JVM 基于栈,猜测是因为基于寄存器方式的抽象不好做,尤其是部分嵌入式CPU的寄存器数目很少,这个不好平衡。
二. JVM 除了抽象,在工程上还做了哪些平衡?
个人觉得,主要还是本地方法上,尤其是NIO 大量使用本地代码优化,像JIT之类其实并没有偏离最初抽象的想法。
四. 整理思考
思维导图是个好东西,按照《深入理解 Java 虚拟机》划分来画,实操层面的不涉及。
写在最后
- 每个人都有也应该有自己的学习习惯,有人喜欢看视频、聊天,有人喜欢看书、深度思考,所以下面讲的也都是本人自己的习惯,不适用别人,仅仅抛出来,如果万一有人能有所收获,就是万幸。
- 虽然有记录过程,实际学习过程还是有偏差,即使很短时间,前后记忆都有偏差,而且写字都有美化自己的倾向。
- 如果真正写个学习笔记,那应该是纯粹的梳理概念,而不是像本人这篇写的这么乱七八糟 😭
- 通过这样的梳理,大体上可以对一个知识有比较基本的认识了,后面就是要进一步通过练习来固化。