小傅哥

小傅哥 查看完整档案

北京编辑剑桥大学  |  软件工程 编辑公众号  |  bugstack虫洞栈 编辑 bugstack.cn 编辑
编辑

CodeGuide | 程序员编码指南 - 原创文章、案例源码、资料书籍、简历模版等下载。
链接:https://github.com/fuzhengwei... - 希望给我点个Star⭐!

作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果我的文章能为您提供帮助,请给予支持(关注、点赞、分享)!

个人动态

小傅哥 发布了文章 · 今天 09:39

每个程序员都该有个自己的博客,分享我的四种博客搭建教程!

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

压测了,小傅哥一天能搭4个博客!

好学、乐学、博学、恒学、会学和用学,学以致用。一起学习成长的很多同好以及我自己,都是同样喜欢折腾的人。

最早大家都喜欢倒腾自己的QQ空间,装修的各式各样,可那炫耀。但终究这个QQ空间里面,还有很多东西不能让自己随意摆弄。不知道是不是此类好奇和爱好,让很多人走上了编程开发的道路。

就折腾博客而言,在大学开始就不停的折腾。从一个网页能被宿舍人访问、被校友访问、被家人看到,那个兴奋劲还是十足的。哪怕是半夜也是一遍遍的折腾写着html,虽然丑了吧唧的!


最近有不少粉丝问小傅哥,自己也想搭建个自己的博客系统写写文章,但不知道怎么弄。正好小傅哥也确实折腾过各种博客的搭建,了解一些坑坑洼洼,算是给后面的司机开开路。

本文主要向大家介绍

  1. 4类静态博客,hexo、docsify、jekyll、vuepress,的差异和特点
  2. 在 GitPage 上部署自己的博客
  3. 独立域名+个人服务器,部署博客
  4. 另外小傅哥把这些博客脚手架统一放到Github仓库,方便大家使用时候可以更方便。关注公众号:bugstack虫洞栈,回复:博客系统

有了这些参考,大家就可以选择适合自己的博客系统了,开心的写博客。

二、你要准备的东西

  • 简单记录:Github账号或者Gitee账号,使用两家的免费静态网页托管服务即可。
  • 绑定域名:如果你想通过自己的域名访问博客,Github与Gitee都支持配置,但Gitee需要付费。不过Gitee对于国内的访问速度要好一些。
  • 访问速度:当你的博客想被更多人访问并且也在意网页的打开速度和体验,那么就需要一个独立的服务器和域名了。这个服务器可以部署静态网页即可

综上,是每一个人建博客的不同目的和需要的内容,按需选择即可。

另外,GitPage配置参考:https://docsify.js.org/#/zh-cn/deploy 在Github的配置中,可以选择根目录和docs两个文件夹,作为静态博客的仓库。所以在选择下面四类博客中,都是把docs文件夹预留出来,方便使用。

三、4种博客的搭建

  • 小傅哥把四类比较常用的博客,源码部分放到这个集中的仓库,方便大家在使用的时候直接克隆走。
  • 关于这四类博客的建设,会在以后陆续的完善内容。如果你感兴趣也可以参与到项目中。
  • 下载地址:https://github.com/BlogGuide

1. hexo

http://hexo.blog.itedus.cn/

npm install hexo-cli -g
hexo init blog
cd blog
npm install
hexo generate # 生成
hexo server   # 启动服务
  • 特点

    • hexo的主题特别多,选择性很高
    • 需要本地编译后,把编译文件推送到Github
  • 其他

    • 因为需要编译和推送,如果只是想简单的写博客,不推荐使用。
    • 但如果想把静态博客部署到个人的服务器,那么就非常适合了。

2. docsify

http://docsify.blog.itedus.cn/

npm i docsify-cli -g # 全局快速安装
docsify init ./docs  # 初始化项目
docsify serve docs   # 本地预览
  • 特点:非常简单、干净,直接把工程文件和md博客推送到Github即可,不需要本地维护编译。

3. jekyll

http://jekyll.blog.itedus.cn

Fork code and clone
Run bower install to install all dependencies in bower.json
Run bundle install to install all dependencies in Gemfile
Update _config.yml with your own settings.
Add posts in /_posts
Commit to your own Username.github.io repository.
Then come back to star this theme!
  • 特点:这个博客的主题其实有点重,在写博客的时候需要人工维护的内容较多。但同样这个主题有一个好处就是如果使用Github,那么就直接把项目和博客传到Github即可,不需要本地编译。

4. vuepress

http://vuepress.blog.itedus.cn

npm install -g vuepress # 安装
vuepress build docs     # 构建,生成html,可以用于部署
vuepress dev docs       # 启动,http://localhost:8080/
  • 特点:基于vue实现的博客,功能很多适合扩展。很适合部署到个人独立的服务器,如果是部署到Github,可以参考源码,在一个工程中提供docs用于存放生成的网页,这样在Github就不需要再维护额外的分支。

四、部署到自己的服务器

  • 博客:vuepress
  • 软件:Idea、ftp[可选]
  • 环境:域名、备案、SSL证书、服务器

vuepress的博客项目放IDEA中打开和日常维护就可以了,而且IDEA只提供了FTP的功能,也可以方便上传服务到远程服务器。

关于域名和服务器等需要购买,另外还需要备案才能正常使用。如果你想域名有一个小锁头的安全提示,则需要ssl证书,一般可以免费获取。

其实小傅哥已经有一个 bugstack.cn 博客,本次是又申请了一个新的域名 itedus.cn 想着再搭建一个玩玩,折腾!

1. IDEA 配置 FTP

在IDEA的菜单栏上,Tools 中有一个 Deployment 的选项,可以配置FTP以及其他SFTP。

IDEA 配置 FTP

  • Host:你购买的服务器都会提供FTP功能,在里面有host地址
  • User name:用户名
  • Password:密码
  • 配置完成后,在Deployment打开的菜单选项中,会有一个 Browse Remote Host 打开以后可以在IDEA中看到了。

2. 上传静态网页

上传静态网页

  • 到这就可以直接上传了你的静态网页到服务器了
  • 其实你还可以基于 Github 的 Webhooks 配置自动推送,但整体配置和实现的内容比较多

五、总结

  • 与CSDN、掘金、思否、开源中国等提供的博客相比,自己维护的博客开发还是需要一些时间精力和运营成本的。但如果想给自己的知识一个实践的机会,就值得折腾。
  • hexo、docsify、jekyll、vuepress,四类博客各有自己的特点,有的需要编译上传,有的直接推送Github即可。但想有自己的域名和整体的体验,就需要购买服务器和备案域名。
  • 本篇文章只为送给那些想折腾一下的伙伴提供一些可实现的路径,但这条路径上如果你想真的搭出一个称心如意的博客,要搞的东西还很多。甚至你会像我一样折腾到公众号开发与博客联动等等,好!助力你做个喜欢折腾的人!

六、系列推荐

查看原文

赞 5 收藏 3 评论 0

小傅哥 发布了文章 · 1月21日

JVM 判断对象已死,实践验证GC回收

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

提升自身价值有多重要?

经过了风风雨雨,看过了男男女女。时间经过的岁月就没有永恒不变的!

在这趟车上有人下、有人上,外在别人给你点评的标签、留下的烙印,都只是这趟车上的故事。只有个人成长了、积累了、沉淀了,才有机会当自己的司机。

可能某个年龄段的你还看不懂,但如果某天你不那么忙了,要思考思考自己的路、自己的脚步。看看这些是不是你想要的,如果都是你想要的,为什么你看起来不开心?

好!加油,走向你想成为的自己!

二、面试题

谢飞机,小记!,中午吃饱了开始发呆,怎么就学不来这些知识呢,它也不进脑子!

谢飞机:喂,面试官大哥,我想问个问题。

面试官:什么?

谢飞机:就是这知识它不进脑子呀!

面试官:这....

谢飞机:就是看了忘,忘了看的!

面试官:是不是没有实践?只是看了就觉得会了,收藏了就表示懂了?哪哪都不深入!?

谢飞机:好像是!那有什么办法?

面试官:也没有太好的办法,学习本身就是一件枯燥的事情。减少碎片化的时间浪费,多用在系统化的学习上会更好一些。哪怕你写写博客记录下,验证下也是好的。

三、先动手验证垃圾回收

说是垃圾回收,我不引用了它就回收了?什么时候回收的?咋回收的?

没有看到实际的例子,往往就很难让理科生接受这类知识。我自己也一样,最好是让我看得见。代码是对数学逻辑的具体实现,没有实现过程只看答案是没有意义的。

测试代码

public class ReferenceCountingGC {

    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 这个成员属性的唯一意义就是占点内存, 以便能在GC日志中看清楚是否有回收过
     */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        testGC();
    }

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假设在这行发生GC, objA和objB是否能被回收?
        System.gc();
    }

}

例子来自于《深入理解Java虚拟机》中引用计数算法章节。

例子要说明的结果是,相互引用下却已经置为null的两个对象,是否会被GC回收。如果只是按照引用计数器算法来看,那么这两个对象的计数标识不会为0,也就不能被回收。但到底有没有被回收呢?

这里我们先采用 jvm 工具指令,jstat来监控。因为监控的过程需要我手敲代码,比较耗时,所以我们在调用testGC()前,睡眠会 Thread.sleep(55000);。启动代码后执行如下指令。

E:\itstack\git\github.com\interview>jps -l
10656
88464
38372 org.itstack.interview.ReferenceCountingGC
26552 sun.tools.jps.Jps
110056 org.jetbrains.jps.cmdline.Launcher

E:\itstack\git\github.com\interview>jstat -gc 38372 2000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0   1288.0 65536.0    0.0     175104.0     8.0     4864.0 3982.6 512.0  440.5       1    0.003   1      0.000    0.003
10752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.015
10752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.015
  • S0C、S1C,第一个和第二个幸存区大小
  • S0U、S1U,第一个和第二个幸存区使用大小
  • EC、EU,伊甸园的大小和使用
  • OC、OU,老年代的大小和使用
  • MC、MU,方法区的大小和使用
  • CCSC、CCSU,压缩类空间大小和使用
  • YGC、YGCT,年轻代垃圾回收次数和耗时
  • FGC、FGCT,老年代垃圾回收次数和耗时
  • GCT,垃圾回收总耗时

注意:观察后面三行,S1U = 1288.0GCT = 0.003,说明已经在执行垃圾回收。

接下来,我们再换种方式测试。在启动的程序中,加入GC打印参数,观察GC变化结果。

-XX:+PrintGCDetails  打印每次gc的回收情况 程序运行结束后打印堆空间内存信息(包含内存溢出的情况)
-XX:+PrintHeapAtGC  打印每次gc前后的内存情况
-XX:+PrintGCTimeStamps 打印每次gc的间隔的时间戳 full gc为每次对新生代老年代以及整个空间做统一的回收 系统中应该尽量避免
-XX:+TraceClassLoading  打印类加载情况
-XX:+PrintClassHistogram 打印每个类的实例的内存占用情况
-Xloggc:/Users/xiaofuge/Desktop/logs/log.log  配合上面的使用将上面的日志打印到指定文件
-XX:HeapDumpOnOutOfMemoryError 发生内存溢出将堆信息转存起来 以便分析

这回就可以把睡眠去掉了,并添加参数 -XX:+PrintGCDetails,如下:

测试结果

[GC (System.gc()) [PSYoungGen: 9346K->936K(76288K)] 9346K->944K(251392K), 0.0008518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 936K->0K(76288K)] [ParOldGen: 8K->764K(175104K)] 944K->764K(251392K), [Metaspace: 3405K->3405K(1056768K)], 0.0040034 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b500000,0x000000076b6eb9e0,0x000000076f500000)
  from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
  to   space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
 ParOldGen       total 175104K, used 764K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)
  object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1ebf100,0x00000006cc900000)
 Metaspace       used 3449K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K
  • 从运行结果可以看出内存回收日志,Full GC 进行了回收。
  • 也可以看出JVM并不是依赖引用计数器的方式,判断对象是否存活。否则他们就不会被回收啦

有了这个例子,我们再接着看看JVM垃圾回收的知识框架!

四、JVM 垃圾回收知识框架

垃圾收集(Garbage Collection,简称GC),最早于1960年诞生于麻省理工学院的Lisp是第一门开始使用内存动态分配和垃圾收集技术的语言。

垃圾收集器主要做的三件事:哪些内存需要回收什么时候回收、怎么回收。

而从垃圾收集器的诞生到现在有半个世纪的发展,现在的内存动态分配和内存回收技术已经非常成熟,一切看起来都进入了“自动化”。但在某些时候还是需要我们去监测在高并发的场景下,是否有内存溢出、泄漏、GC时间过程等问题。所以在了解和知晓垃圾收集的相关知识对于高级程序员的成长就非常重要。

垃圾收集器的核心知识项主要包括:判断对象是否存活、垃圾收集算法、各类垃圾收集器以及垃圾回收过程。如下图;

图 27-1 垃圾收集器知识框架

原图下载链接:http://book.bugstack.cn/#s/6jJp2icA

1. 判断对象已死

1.1 引用计数器

  1. 为每一个对象添加一个引用计数器,统计指向该对象的引用次数。
  2. 当一个对象有相应的引用更新操作时,则对目标对象的引用计数器进行增减。
  3. 一旦当某个对象的引用计数器为0时,则表示此对象已经死亡,可以被垃圾回收。

从实现来看,引用计数器法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但是它的实现方案简单,判断效率高,是一个不错的算法。

也有一些比较出名的引用案例,比如:微软COM(Component Object Model) 技术、使用ActionScript 3的FlashPlayer、 Python语言等。

但是,在主流的Java虚拟机中并没有选用引用技术算法来管理内存,主要是因为这个简单的计数方式在处理一些相互依赖、循环引用等就会非常复杂。可能会存在不再使用但又不能回收的内存,造成内存泄漏

1.2 可达性分析法

Java、C#等主流语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

它的算法思路是通过定义一系列称为 GC Roots 根对象作为起始节点集,从这些节点出发,穷举该集合引用到的全部对象填充到该集合中(live set)。这个过程教过标记,只标记那些存活的对象 好,那么现在未被标记的对象就是可以被回收的对象了。

GC Roots 包括;

  1. 全局性引用,对方法区的静态对象、常量对象的引用
  2. 执行上下文,对 Java方法栈帧中的局部对象引用、对 JNI handles 对象引用
  3. 已启动且未停止的 Java 线程

两大问题

  1. 误报:已死亡对象被标记为存活,垃圾收集不到。多占用一会内存,影响较小。
  2. 漏报:引用的对象(正在使用的)没有被标记为存活,被垃圾回收了。那么直接导致的就是JVM奔溃。(STW可以确保可达性分析法的准确性,避免漏报)

2. 垃圾回收算法

2.1 标记-清除算法(mark-sweep)

标记-清除算法(mark-sweep)

  • 标记无引用的死亡对象所占据的空闲内存,并记录到空闲列表中(free list)。
  • 当需要创建新对象时,内存管理模块会从 free list 中寻找空闲内存,分配给新建的对象。
  • 这种清理方式其实非常简单高效,但是也有一个问题内存碎片化太严重了。
  • Java 虚拟机的堆中对象,必须是连续分布的,所以极端的情况下可能即使总剩余内存充足,但寻找连续内存分配效率低,或者严重到无法分配内存。重启汤姆猫!
  • 在CMS中有此类算法的使用,GC暂停时间短,但存在算法缺陷。

2.2 标记-复制算法(mark-copy)

标记-复制算法(mark-copy)

  • 从图上看这回做完垃圾清理后连续的内存空间就大了。
  • 这种方式是把内存区域分成两份,分别用两个指针 from 和 to 维护,并且只使用 from 指针指向的内存区域分配内存。
  • 当发生垃圾回收时,则把存活对象复制到 to 指针指向的内存区域,并交换 from 与 to 指针。
  • 它的好处很明显,就是解决内存碎片化问题。但也带来了其他问题,堆空间浪费了一半。

2.3 标记-压缩算法(mark-compact)

标记-压缩算法(mark-compact)

  • 1974年,Edward Lueders 提出了标记-压缩算法,标记的过程和标记清除算法一样,但在后续对象清理步骤中,先把存活对象都向内存空间一端移动,然后在清理掉其他内存空间。
  • 这种算法能够解决内存碎片化问题,但压缩算法的性能开销也不小。

3. 垃圾回收器

3.1 新生代

  1. Serial

    1. 算法:标记-复制算法
    2. 说明:简单高效的单核机器,Client模式下默认新生代收集器;
  2. Parallel ParNew

    1. 算法: 标记-复制算法
    2. 说明:GC线程并行版本,在单CPU场景效果不突出。常用于Client模式下的JVM
  3. Parallel Scavenge

    1. 算法:标记-复制算法
    2. 说明:目标在于达到可控吞吐量(吞吐量=用户代码运行时间/(用户代码运行时间+垃圾回收时间));

3.2 老年代

  1. Serial Old

    1. 算法:标记-压缩算法
    2. 说明:性能一般,单线程版本。1.5之前与Parallel Scavenge配合使用;作为CMS的后备预案。
  2. Parallel Old

    1. 算法:标记-压缩算法
    2. 说明:GC多线程并行,为了替代Serial Old与Parallel Scavenge配合使用。
  3. CMS

    1. 算法:标记-清除算法
    2. 说明:对CPU资源敏感、停顿时间长。标记-清除算法,会产生内存碎片,可以通过参数开启碎片的合并整理。基本已被G1取代

3.3 G1

  1. 算法:标记-压缩算法
  2. 说明:适用于多核大内存机器、GC多线程并行执行,低停顿、高回收效率。

五、总结

  • JVM 的关于自动内存管理的知识众多,包括本文还没提到的 HotSpot 实现算法细节的相关知识,包括:安全节点、安全区域、卡表、写屏障等。每一项内容都值得深入学习。
  • 如果不仅仅是为了面试背题,最好的方式是实践验证学习。否则这类知识就像3分以下的过电影一样,很难记住它的内容。
  • 整个的内容也是小傅哥学习整理的一个过程,后续还会不断的继续深挖和分享。感兴趣的小伙伴可以一起讨论学习。

六、系列推荐

查看原文

赞 2 收藏 1 评论 0

小傅哥 发布了文章 · 1月18日

数学,离一个程序员有多近?

作者:小傅哥

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

数学离程序员有多近?

ifelse也好、for循环也罢,代码可以说就是对数学逻辑的具体实现。所以敲代码的程序员几乎就离不开数学,难易不同而已。

那数学不好就写不了代码吗😳?不,一样可以写代码,可以写出更多的CRUD出来。那你不要总觉得是产品需求简单所以你的实现过程才变成了增删改查,往往也是因为你还不具备可扩展、易维护、高性能的代码实现方案落地能力,才使得你小小年纪写出了更多的CRUD

与一锥子买卖的小作坊相比,大厂和超级大厂更会注重数学能力。

first 10-digit prime found in consecutive digits of e

2004年,在硅谷的交通动脉 101 公路上突然出现一块巨大的广告牌,上面是一道数学题: {e 的连续数字中最先出现的 10 位质数}.com。

广告:这里的 e 是数学常数,自然对数的底数,无限不循环小数。这道题的意思就是,找出 e 中最先出现的 10 位质数,然后可以得出一个网址。进入这个网址会看到 Google 为你出的第二道数学题,成功解锁这步 Google 会告诉你,我们或许是”志同道合“的人,你可以将简历发到这个邮箱,我们一起做点改变世界的事情。

计算 e 值可以通过泰勒公式推导出来:e^x≈1 + x + x^2/2! + x^3/3! +……+ x^n/n! (1) 推导计算过程还包括埃拉托色尼筛选法(the Sieve of Eratosthenes)线性筛选法的使用。感兴趣的小伙伴可以用代码实现下。

二、把代码写好的四步

业务提需求、产品定方案、研发做实现。最终这个系统开发的怎么样是由三方共同决定的!

  • 地基挖的不好,楼就盖不高
  • 砖头摆放不巧,楼就容易倒
  • 水电走线不妙,楼就危险了
  • 格局设计不行,楼就卖不掉

这里的地基、砖头、水电、格局,对应的就是,数据结构、算法逻辑、设计模式、系统架构。从下到上相互依赖、相互配合,只有这一层做好,下一层才好做!

图 20-2 代码实现过程分层

  • 数据结构:高矮胖瘦、长宽扁细,数据的存放方式,是一套程序开发的核心基础。不合理的设计往往是从数据结构开始,哪怕你仅仅是使用数据库存放业务信息,也一样会影响到将来各类数据的查询、汇总等实现逻辑的难易。
  • 算法逻辑:是对数据结构的使用,合适的数据结构会让算法实现过程降低时间复杂度。可能你现在的多层for循环在合适的算法过程下,能被优化为更简单的方式获取数据。注意:算法逻辑实现,并不一定就是排序、归并,还有你实际业务的处理流程。
  • 设计模式:可以这么说,不使用设计模式你一样能写代码。但你愿意看到满屏幕的ifelse判断调用,还是喜欢像膏药一样的代码,粘贴来复制去?那么设计模式这套通用场景的解决方案,就是为你剔除掉代码实现过程中的恶心部分,让整套程序更加易维护、易扩展。就是开发完一个月,你看它你还认识!
  • 系统架构:描述的是三层MVC,还是四层DDD。我对这个的理解就是家里的三居还是四局格局,MVC是我们经常用的大家都熟悉,DDD无非就是家里多了个书房,把各自属于哪一个屋子的摆件规整到各自屋子里。那么乱放是什么效果呢,就是自动洗屁屁马桶🚽给按到厨房了,再贵也格楞子! 好,那么我们在延展下,如果你的卫生间没有流出下水道咋办?是不这个地方的数据结构就是设计缺失的,而到后面再想扩展就难了吧!

所以,研发在承接业务需求、实现产品方案的时候。压根就不只是在一个房子的三居或者四居格局里,开始随意码砖。

没有合理的数据结构、没有优化的算法逻辑、没有运用的设计模式,最终都会影响到整个系统架构变得臃肿不堪,调用混乱。在以后附加、迭代、新增的需求下,会让整个系统问题不断的放大,当你想用重构时,就有着千丝万缕般调用关系。 重构就不如重写了!

三、for循环没算法快

在《编程之美》一书中,有这样一道题。求:1~n中,1出现的次数。比如:1~10,1出现了两次。

1. for 循环实现

long startTime = System.currentTimeMillis();
int count = 0;
for (int i = 1; i <= 10000000; i++) {
    String str = String.valueOf(i);
    for (int j = 0; j < str.length(); j++) {
        if (str.charAt(j) == 49) {
            count++;
        }
    }
}
System.out.println("1的个数:" + count);
System.out.println("计算耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");

使用 for 循环的实现过程很好理解,就是往死了循环。之后把循环到的数字按照字符串拆解,判断每一位是不是数字,是就+1。这个过程很简单,但是时间复杂很高。

2. 算法逻辑实现

图 20-3 1的个数循环规则

如图 20-3 所示,其实我们能发现这个1的个数在100、1000、10000中是有规则的循环出现的。11、12、13、14或者21、31、41、51,以及单个的1出现。最终可以得出通用公式:abcd...=(abc+1)*1+(ab+1)*10+(a+1)*100+(1)*1000...,abcd代表位数。另外在实现的过程还需要考虑比如不足100等情况,例如98、1232等。

实现过程

long startTime = System.currentTimeMillis();
int num = 10000000, saveNum = 1, countNum = 0, lastNum = 0;
int copyNum = num;
while (num != 0) {
    lastNum = num % 10;
    num /= 10;
    if (lastNum == 0) {
        // 如果是0那么正好是少了一次所以num不加1了
        countNum += num * saveNum;
    } else if (lastNum == 1) {
        // 如果是1说明当前数内少了一次所以num不加1,而且当前1所在位置
        // 有1的个数,就是去除当前1最高位,剩下位数,的个数。
        countNum += num * saveNum + copyNum % saveNum + 1;
    } else {
        // 如果非1非0.直接用公式计算
        // abcd...=(abc+1)*1+(ab+1)*10+(a+1)*100+(1)*1000...
        countNum += (num + 1) * saveNum;
    }
    saveNum *= 10;
}
System.out.println("1的个数:" + countNum);
System.out.println("计算耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");

在《编程之美》一书中还不只这一种算法,感兴趣的小伙伴可以查阅。但自己折腾实现后的兴奋感更强哦!

3. 耗时曲线对比

按照两种不同方式的实现逻辑,我们来计算1000、10000、10000到一个亿,求1出现的次数,看看两种方式的耗时曲线。

图 20-4 耗时曲线对比

  • for循环随着数量的不断增大后,已经趋近于无法使用了。
  • 算法逻辑依靠的计算公式,所以无论增加多少基本都会在1~2毫秒内计算完成。

那么,你的代码中是否也有类似的地方。如果使用算法逻辑配合适合的数据结构,是否可以替代一些for循环的计算方式,来使整个实现过程的时间复杂度降低。

四、Java中的算法运用

在 Java 的 JDK 实现中有很多数学知识的运用,包括数组、链表、红黑树的数据结构以及相应的实现类ArrayList、Linkedlist、HashMap等。当你深入的了解这些类的实现后,会发现它们其实就是使用代码来实现数学逻辑而已。就像你使用数学公式来计算数学题一样

接下来小傅哥就给你介绍几个隐藏在我们代码中的数学知识。

1. HashMap的扰动函数

未使用扰动函数

未使用扰动函数,数据分布

已使用扰动函数

未使用扰动函数,数据分布

扰动函数公式

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 描述:以上这段代码是HashMap中用于获取hash值的扰动函数实现代码。HashMap通过哈希值与桶定位坐标 那么直接获取哈希值就好了,这里为什么要做一次扰动呢?
  • 作用:为了证明扰动函数的作用,这里选取了10万单词计算哈希值分布在128个格子里。之后把这128个格子中的数据做图表展示。从实现数据可以看到,在使用扰动函数后,曲线更加平稳了。那么,也就是扰动后哈希碰撞会更小。
  • 用途:当你有需要把数据散列分散到不同格子或者空间时,又不希望有太严重的碰撞,那么使用扰动函数就非常有必要了。比如你做的一个数据库路由,在分库分表时也是尽可能的要做到散列的。

2. 斐波那契(Fibonacci)散列法

ThreadLocal 中 斐波那契(Fibonacci)散列法

  • 描述:在 ThreadLocal 类中的数据存放,使用的是斐波那契(Fibonacci)散列法 + 开放寻址。之所以使用斐波那契数列,是为了让数据更加散列,减少哈希碰撞。具体来自数学公式的计算求值,公式f(k) = ((k * 2654435769) >> X) << Y对于常见的32位整数而言,也就是 f(k) = (k * 2654435769) >> 28
  • 作用:与 HashMap 相比,ThreadLocal的数据结构只有数组,并没有链表和红黑树部分。而且经过我们测试验证,斐波那契散列的效果更好,也更适合 ThreadLocal。
  • 用途:如果你的代码逻辑中需要存储类似 ThreadLocal 的数据结构,又不想有严重哈希碰撞,那么就可以使用 斐波那契(Fibonacci)散列法。其实除此之外还有,除法散列法平方散列法随机数法等。

3. 梅森旋转算法(Mersenne twister)

梅森旋转算法的三个阶段,来自CSDN博客网图

// Initializes mt[N] with a simple integer seed. This method is
// required as part of the Mersenne Twister algorithm but need
// not be made public.
private final void setSeed(int seed) {
    // Annoying runtime check for initialisation of internal data
    // caused by java.util.Random invoking setSeed() during init.
    // This is unavoidable because no fields in our instance will
    // have been initialised at this point, not even if the code
    // were placed at the declaration of the member variable.
    if (mt == null) mt = new int[N];
    // ---- Begin Mersenne Twister Algorithm ----
    mt[0] = seed;
    for (mti = 1; mti < N; mti++) {
        mt[mti] = (MAGIC_FACTOR1 * (mt[mti-1] ^ (mt[mti-1] >>> 30)) + mti);
    }
    // ---- End Mersenne Twister Algorithm ----
}
梅森旋转算法(Mersenne twister)是一个伪随机数发生算法。由松本真和西村拓士在1997年开发,基于有限二进制字段上的矩阵线性递归。可以快速产生高质量的伪随机数,修正了古典随机数发生算法的很多缺陷。 最为广泛使用Mersenne Twister的一种变体是MT19937,可以产生32位整数序列。
  • 描述:梅森旋转算法分为三个阶段,获得基础的梅森旋转链、对于旋转链进行旋转算法、对于旋转算法所得的结果进行处理。
  • 用途:梅森旋转算法是R、Python、Ruby、IDL、Free Pascal、PHP、Maple、Matlab、GNU多重精度运算库和GSL的默认伪随机数产生器。从C++11开始,C++也可以使用这种算法。在Boost C++,Glib和NAG数值库中,作为插件提供。

五、程序员数学入门

与接触到一个有难度的知识点学起来辛苦相比,是自己不知道自己不会什么!就像上学时候老师说,你不会的就问我。我不会啥?我从哪问?一样一样的!

代码是对数学逻辑的实现,简单的逻辑调用关系是很容易看明白的。但还有那部分你可能不知道的数学逻辑时,就很难看懂了。比如:扰动函数、负载因子、斐波那契(Fibonacci)等,这些知识点的学习都需要对数学知识进行验证,否则也就学个概念,背个理论。

书到用时方恨少,在下还是个宝宝!

那如果你想深入的学习下程序员应该会的数学,推荐给你一位科技博主 Jeremy Kun 花了4年时间,写成一本书 《程序员数学入门》

 Jeremy Kun,《程序员数学入门》

这本书为程序员提供了大量精简后数学知识,包括:多项式、集合、图论、群论、微积分和线性代数等。同时在wiki部分还包括了抽象代数、离散数学、傅里叶分析和拓扑学等。

《程序员数学入门》书中插图

作者表示,如果你本科学过一些数学知识,那么本书还是挺适合你的,不会有什么难度。书中的前三章是基础数学内容,往后的难度依次递增。

六、总结

  • Programming is one of the most difficult branches of applied mathematics; the poorer mathematicians had better remain pure mathematicians. https://www.cs.utexas.edu/users/EWD/transcriptions/EWD04xx/EWD498.html
  • 单纯的只会数学写不了代码,能写代码的不懂数学只能是CRUD码农。数学知识帮助你设计数据结构和实现算法逻辑,代码能力帮你驾驭设计模式和架构模型。多方面的知识结合和使用才是码农和工程师的主要区别,也是是否拥有核心竞争力的关键点。
  • 学习知识有时候看不到前面的路有多远,但哪怕是个泥坑,只要你不停的蠕动、折腾、翻滚,也能抓出一条泥鳅。知识的路上是发现知识的快乐,还学会知识的成就感,不断的促使你前行

七、系列推荐


博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki

查看原文

赞 9 收藏 7 评论 0

小傅哥 赞了文章 · 1月16日

思否有约 | @小傅哥:无论工作还是生活,都是生命每一个值得被珍惜的瞬间

image.png
小傅哥(喜欢的动漫形象)

本期访谈嘉宾:小傅哥
访谈编辑:袁钰涵

引子

电影《心灵奇旅》结尾处,杰瑞问准备回到地球的高纳:“So what do you think you'll do?How are you gonna spend your life?”
(那你认为你会做什么?你将如何度过这一生?)

高纳回道:“I'm not sure.But,I'm going to live every minute of it.”
(我也不知道,但是,我会珍惜当下的每一分钟。)


刚完成对小傅哥的采访时,对于他在采访过程中多次提到的“我喜欢比客户跑远一步,提出更高的需求去完成,即使加班也没有关系”,并不太理解。

但采访结束后看了一部名为《心灵奇旅》的影片后好像懂了,因为我们总喜欢把工作和生活分得明明白白,仿佛加班会让我们成为可怜的社畜,但某种程度上,工作也是生活,我们在其中追求自我,付出了我们的精力和时间只为收获属于自己更绚烂的人生。

小傅哥把工作和生活放在了一起,珍惜当下的每一分钟也包括了珍惜工作中的每一个任务,正是这些人生中的碎片组成了他。

在采访过程中,他说得最多的一句话就是:很顺利,我的生活没有太多的故事或困难。

仿佛在和我说:我没有什么不同,和许许多多的程序员都一样,顺着生活轨迹走到了今天。

是不是和许多程序员一样我并不知道,但是我知道的是——他的顺利和幸运都来源于他对生活的珍惜以及对人生的负责。

顺利在何处?

一本顺产的优秀书籍

小傅哥曾出过一本名为《重学 Java 设计模式》的书,当提到出书历程时,问小傅哥:“会不会觉得很难熬,有的作者形容出书就像难产”,小傅哥说:“并不是,它是顺产的,沉淀积累到了一定程度,它就出来了。”

这本书的诞生来自于小傅哥平时在思否这类社区平台分享的文章,他喜欢写这些文章,于他而言,写文没有什么 KPI,也没有什么压力,这不过是对于自身工作经验的沉淀,他用文章记录下那些他认为值得被记录的项目,其行为就像手账爱好者用手账本记录今日发生事件一般,究其本质都是对生活的记录与回顾。

后来这些文章在平台获得了不错的反响,也有许多朋友在平台中与小傅哥进行探讨,大家互相学习,在一次次讨论中成为前行路上的伙伴,而这些项目是连接彼此的介质。

项目文章越来越多后,连接的伙伴也越来越多,小傅哥想,如果把它整理成书籍,不仅是对这段日子总体的回顾,对自身有更深层次的沉淀,同时也能帮助到许多想学习 Java 的朋友,此般,《重学 Java 设计模式》便水到渠成地诞生了。

后来这本书收获了不俗的成绩,还冲上了 Github 全球推荐榜,小傅哥用“羞答答的成绩”来形容这份喜悦。

没来得及经历的职场小白时光

初入职场,许多人单是适应工作节奏就已经筋疲力尽了,但小傅哥并没有经历过这种“职场小白”的时光。

读大学时学习的内容有许多都是进行考试、理论性操作,小傅哥感觉这样对于后期的职业发展不太好,于是那时混迹各种大佬建的 QQ 群。

这里发生了一件非常好玩的事情,当时作为一个有理想的青年,小傅哥混迹各种 Q 群后发现,许多群都是水聊天,有用的群并不好找。

没有条件就要创造条件,他把群内认识爱聊技术的,人单独创了群,名称叫东软帝国 ,当时说着要成为东北最大的软件帝国,后来某年发现真的有家公司叫东软,与朋友提起仍会相视一笑。

技术群内能学到很多东西,群内看到看到需要人的项目小傅哥就会去帮忙,到了真正毕业的时候,他已经做到:开发掌握得不错,需求能很顺利完成的程度了。

工作开始便能很好地完成需求,于是小傅哥不希望自己负责的内容只停留在完成这一步,他主动地提出更多需求,增加项目难度,这个习惯延续多年,成了他工作的常态,这种做法让他后期的职场生涯中少了许多焦头烂额的日子,倒是多了几分对于工作的热爱与优化项目的成就感。

这种做法一开始为我所不了解,后来我才明白于他而言生活与工作是一体的,那些项目是他生命中的一部分,对自己出品的东西填以热情,珍惜每一次接受委托的机会,这何尝不是对生命每一个瞬间的尊重。

对新生代程序员的一些建议

当初的项目讨论 QQ 群给了小傅哥很多帮助,这些年见证技术讨论群从 QQ 走到微信,其中不少还是小傅哥自己创建的,他仍非常建议新生程序员去加入大佬的微信群,虽然大家平时会水聊天,但在分享一些技术事件的时候,大家会认真地讨论与研究,这个过程中互相学习对方观点,某种程度上能拓宽自身视角,让程序员之路走得更宽更远,同样他自己也仍然在这些群里一边水聊天、一边探讨学习技术。

为何幸运?

热爱自身从事的行业

许多人在大学报志愿的时候,都是亮眼一黑,全家沟通进行了无数次大大小小的讨论才选定下来,最后可能因为家庭原因、学校问题、调剂问题,最后选择从事与专业毫无关系的工作或者跨专业考研,再给自己一次选择专业的机会。

但小傅哥的选择就简单而明晰,被问道为什么选择学计算机,他说好像无法选择一样,喜欢的东西和程序员的工作吻合度是如此的高,同时作为一名喜欢理工科的男同志,成为程序员成为了理所应当的事情。

他提到,如果一个人在迷茫的状态下选择了自身不喜欢的专业,日后的路会很困难,因为无法为自身带来愉悦感的工作,就很难提升到职业的高度,无法拥有归属感,在这种环境下工作,仿佛是被生活针对了一般,遇到困难也很难主动地想去克服,会有四处碰壁的挫折感。

所以感到自己能选择喜欢并且擅长的行业工作,是一件很幸运的事情,这让他拥有恒久的热情去投入到工作当中,这份热情如同熊熊燃烧的生命之火,点亮了他的人生。

幸运的职业生涯

小傅哥在一家偏传统的公司工作两年后,来到了互联网,工资提高的同时工作时长也变长了,面对这种改变,小傅哥很坦然地说:时间变长是为了更好的优化项目,对他而言,如果在固定时间内把需求完成并且做好,他是不会进行加班的,甚至还会提前离开。

工作中遇到的许多事情他都能从一个很积极的角度去面对。

从传统公司到互联网公司,当问小傅哥面试时感觉难吗,小傅哥没有直接说难度大还是小,而是回道“我从毕业到现在,一共面试了两次,一个是前公司,一个是现公司”。

面试的时候,小傅哥的兴趣爱好给了他很大的帮助,在日常生活中他喜欢写有关 Java 的东西、写开源项目、研究元码,即使自身可能没从事相关方面的工作,也会去看去写。

现在的小傅哥在一家不错的公司工作,小编问他要不要说一下东家的名字,小傅哥很快拒绝了这个提议,他说,现在还没有到能公布东家的程度,等到自己做出一些成绩后,再把自己和公司放在一起。

他的顺利和幸运都来源于他对生活的珍惜以及对人生的负责

小傅哥是一个优秀而不自知的人,许多他认简单且普通的事情,其实对于许多处于那个阶段的人而言,都是一个个需要去闯的难关,比如参加开源项目、跟着大佬们学做项目等等。

小傅哥说得风轻云淡,但谁也不知道他背后付出过多少努力,他很少提及这些付出,因为在他看来,那些努力都是向前走的助力,他从来不会认为给自己增加额外的工作量、加班是一件辛苦的事情,他接受这些存在,感谢这些存在为他带来的进步,甚至珍惜这些提升自我的时刻,然后感谢自己顺利又幸运的人生。

小傅哥对于工作的态度并非是“打工人”“搬砖”“恰饭工具”,他的经验与研究欲督促他不要机械化。

他很喜欢比需求方多走一步,通过自身的经验与思考,最后做出一个更优的项目能让他收获除了完成工作外的成就感,这些成就感来源与他与合作方对项目的认同,这些都是他工作的动力,在这个过程中他面临了加班,却又享受了成功。

不过对于这种态度,小傅哥坦诚地说:每个人会有自己的想法,不一样也正常,而他也并不是永远这么勤奋,只是他的兴趣刚好落在了研究上,对于一个项目进行深层次的研究与探讨能给他快乐,在生活中的其他事情上,他也有懒惰的时候。

无论工作上的勤奋还是生活中的懒惰,这些就是他对于人生的态度,该负责的时候负责,该偷懒就偷懒,在这种放松的状态下,享受自己短暂而又珍贵的人生。

坦荡、坦然还是他

虽然有时低调得都有点凡尔赛了,但小傅哥的确是个坦诚的人,说到为什么会在社区分享文章时,小傅哥特别实在地说:做这件事主要是对自身的一个沉淀,分享技术文章的时候,会有技术同好和自己一起讨论,交流的过程中会得到很多成长,这是属于他的学习方式,帮助他人的同时也收获了他人的帮助。后面还补上,当然阅读量也是支持作者走下去的重要因素。

对于写文、和工作小傅哥都把自己看的很清楚,而对于年龄,他更是坦然。

被问到年纪的时候,小傅哥利落地给出了一个数字:31,甚至他也很乐意和别人说自己已经是个工作 7 年的人,年轻有年轻的好,而成熟又有成熟的好。

网络上有许多关于这个年龄段的焦虑情绪,但小傅哥并没有为此所扰,循序渐进往下走就是他的态度,而职场中,只要仍旧拥有留下来和走出去的能力,其他事情倒不必去担忧那么多。

努力学习,充实自己,是他人生非常长一段时间的代言词,而这么做也不会让他感到生活如同紧绷的弦,难以支撑,因为这一切努力也好、奋斗也好,出发点不过是他对于工作与生活的珍惜,以及对那转而 30 年已过的人生衷心的感谢。


欢迎有兴趣参与访谈的小伙伴踊跃报名,《思否有约》将把你与编程有关的故事记录下来。报名邮箱:mango@sifou.com

segmentfault公众号

查看原文

赞 5 收藏 0 评论 5

小傅哥 发布了文章 · 1月14日

JVM故障处理工具,使用总结

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

用都用不到怎么学?

没有场景、没有诉求,怎么学习这些似乎用不上知识点。

其实最好的方式就是归纳、整理、实践、输出,一套组合拳下来,你就掌握了这个系列的知识了。

但在当前阶段可能真的用不上,JVM是一个稳定服务,哪能天天出问题,哪需要你老排查。又不是像你写的代码那样!

可是知识的学习就是把你垫基到更高层次后,才有机会接触更有意思的工作和技术创新。如果只是单纯的学几个指令,其实并没有多有意思。但让你完成一套全链路监控,里面需要含有一次方法调用的整体耗时、执行路径、参数信息、异常结果、GC次数、堆栈数据、分代内容等等的时候,那么你的知识储备够开发一个这样的系统吗?

好,先上图看看本文要讲啥,再跟着小傅哥的步伐往下走。

JVM 故障处理工具

二、面试题

谢飞机,小记!,周末休息在家无聊,把已经上灰了的JVM虚拟机学习翻出来。

谢飞机:呱...呱...,喂大哥,这个,这个JVM虚拟机看啥呀。

面试官:看啥?不知道从哪开始?嗯,那你从问题点下手!

谢飞机:啥问题点呢,我就是不知道自己不会啥,也不知道问你啥。

面试官:啊!那我问你个,怎么通过JVM故障处理工具,查看JVM启动时参数都配置了什么呢?

谢飞机:这个!?不道呀!

面试官:那你熟悉的监控指令都有啥,如果问你堆内存统计如何统计,你可知晓!?

谢飞机:也不知道,哈哈哈,好像知道要去看啥了!

面试官:去吧,带着问题看,看完整理出来!

三、基础故障处理工具

1. jps 虚拟机进程状况

jps(JVM Process Status Tool),它的功能与ps命令类似,可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID),类似于 ps -ef | grep java 的功能。

这小家伙虽然不大,功能又单一。但可以说基本你用其他命令都得先用它,来查询到LVMID来确定要监控的是哪个虚拟机进程。

命令格式

jps [ options ] [ hostid ]

  • options:选项、参数,不同的参数可以输出需要的信息
  • hostid:远程查看

选项列表

选项描述
-q只输出进程ID,忽略主类信息
-l输出主类全名,或者执行JAR包则输出路径
-m输出虚拟机进程启动时传递给主类main()函数的参数
-v输出虚拟机进程启动时的JVM参数

1.1 jps -q,只列出进程ID

E:\itstack\git\github.com\interview>jps -q
104928
111552
26852
96276
59000
8460
76188

1.2 jps -l,输出当前运行类全称

E:\itstack\git\github.com\interview>jps -l
111552 org/netbeans/Main
26852
96276 org.jetbrains.jps.cmdline.Launcher
59000
62184 sun.tools.jps.Jps
8460 org/netbeans/Main
76188 sun.tools.jstatd.Jstatd
  • 用这个命令输出的内容就清晰多了,-l 也是非常常用的一个参数选项。

1.3 jps -m,列出传给main()函数的参数

E:\itstack\git\github.com\interview>jps -m
111552 Main --branding visualvm --cachedir C:\Users\xiaofuge\AppData\Local\VisualVM\Cache/8u131 --openid 3041391569375200
26852
96276 Launcher C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/plugins/java/lib/javac2.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/plugins/java/lib/aether-api-1.1.0.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/lib/jna-platform.jar;C:/Program Fi
les/JetBrains/IntelliJ IDEA 2019.3.1/lib/guava-27.1-jre.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/lib/httpclient-4.5.10.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/lib/forms-1.1-preview.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/plu
gins/java/lib/aether-connector-basic-1.1.0.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/plugins/java/lib/maven-model-builder-3.3.9.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/lib/jps-model.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/plu
gins/java/lib/maven-model-3.3.9.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/plugins/java/lib/aether-impl-1.1.0.jar;C:/Program Files/JetBrains/IntelliJ IDEA 2019.3.1/lib/gson-2.8.5.jar;C:/Program File
59000
16844 Jps -m
8460 Main --branding visualvm --cachedir C:\Users\xiaofuge\AppData\Local\VisualVM\Cache/8u131 --openid 3041414336579200
76188 Jstatd

1.4 jps -v,输出虚拟机进程启动时JVM参数[-Xms24m -Xmx256m]

E:\itstack\git\github.com\interview>jps -v
111552 Main -Xms24m -Xmx256m -Dsun.jvmstat.perdata.syncWaitMs=10000 -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dnetbeans.keyring.no.master=true -Dplugin.manager.install.global=false --add-exports=java.desktop/sun.awt=ALL-UNNAMED --add-exports=jdk.jvmstat/sun
.jvmstat.monitor.event=ALL-UNNAMED --add-exports=jdk.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED --add-exports=java.desktop/sun.swing=ALL-UNNAMED --add-exports=jdk.attach/sun.tools.attach=ALL-UNNAMED --add-modules=java.activation -XX:+IgnoreUnrecognizedVMOptions -Djdk.
home=C:/Program Files/Java/jdk1.8.0_161 -Dnetbeans.home=C:\Program Files\Java\jdk1.8.0_161\lib\visualvm\platform -Dnetbeans.user=C:\Users\xiaofuge1\AppData\Roaming\VisualVM\8u131 -Dnetbeans.default_userdir_root=C:\Users\xiaofuge1\AppData\Roaming\VisualVM -XX:+H
eapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\Users\xiaofuge1\AppData\Roaming\VisualVM\8u131\var\log\heapdump.hprof -Dsun.awt.keepWorkingSetOnMinimize=true -Dnetbeans.dirs=C:\Program Files\Java\jdk1.8.0_161\lib\visualvm\visualvm;C:\Program
59000  -Dfile.encoding=UTF-8 -Xms128m -Xmx1024m -XX:MaxPermSize=256m
76188 Jstatd -Denv.class.path=.;C:\Program Files\Java\jre1.8.0_161\lib;C:\Program Files\Java\jre1.8.0_161\lib\tool.jar; -Dapplication.home=C:\Program Files\Java\jdk1.8.0_161 -Xms8m -Djava.security.policy=jstatd.all.policy

1.5 jps -lv 127.0.0.1,输出远程机器信息

jps 链接远程输出JVM信息,需要注册RMI,否则会报错 RMI Registry not available at 127.0.0.1

注册RMI开启 jstatd 在你的 C:\Program Files\Java\jdk1.8.0_161\bin 目录下添加名称为 jstatd.all.policy 的文件。无其他后缀

jstatd.all.policy 文件内容如下:

grant codebase "file:${java.home}/../lib/tools.jar" {
   permission java.security.AllPermission;
};

添加好配置文件后,在 bin 目录下注册添加的 jstatd.all.policy 文件:C:\Program Files\Java\jdk1.8.0_161\bin>jstatd -J-Djava.security.policy=jstatd.all.policy

顺利的话现在就可以查看原创机器JVM信息了,如下:

E:\itstack\git\github.com\interview>jps -l 127.0.0.1
111552 org/netbeans/Main
26852
96276 org.jetbrains.jps.cmdline.Launcher
36056 sun.tools.jps.Jps
59000
8460 org/netbeans/Main
76188 sun.tools.jstatd.Jstatd
  • 也可以组合使用 jps 的选项参数,比如:jps -lm 127.0.0.1

2. jcmd 虚拟机诊断命令

jcmd,是从jdk1.7开始新发布的 JVM 相关信息诊断工具,可以用它来导出堆和线程信息、查看Java进程、执行GC、还可以进行采样分析(jmc 工具的飞行记录器)。注意其使用条件是只能在被诊断的JVM同台sever上,并且具有相同的用户和组(user and group).

命令格式

jcmd <pid | main class> <command ...|PerfCounter.print|-f file>

  • pid,接收诊断命令请求的进程ID

    • main class,接收诊断命令请求的进程main类。
  • command,接收诊断命令请求的进程main类。
  • PerfCounter.print,打印目标 Java 进程上可用的性能计数器。
  • -f file,从文件file中读取命令,然后在目标Java进程上调用这些命令。
  • -l,查看所有进程列表信息。
  • -h、-help,查看帮助信息。

2.1 jcmd pid VM.flags,查看JVM启动参数

E:\itstack\git\github.com\interview>jcmd 111552 VM.flags
111552:
-XX:CICompilerCount=4 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\Users\xiaofuge1\AppData\Roaming\VisualVM\8u131\var\log\heapdump.hprof -XX:+IgnoreUnrecognizedVMOptions -XX:InitialHeapSize=25165824 -XX:MaxHeapSize=268435456 -XX:MaxNewSize=89128960 -XX:Min
HeapDeltaBytes=524288 -XX:NewSize=8388608 -XX:OldSize=16777216 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

2.2 jcmd pid VM.uptime,查看JVM运行时长

E:\itstack\git\github.com\interview>jcmd 111552 VM.uptime
111552:
583248.912 s

2.3 jcmd pid PerfCounter.print,查看JVM性能相关参数

E:\itstack\git\github.com\interview>jcmd 111552 PerfCounter.print
111552:
java.ci.totalTime=56082522
java.cls.loadedClasses=5835
java.cls.sharedLoadedClasses=0
java.cls.sharedUnloadedClasses=0
java.cls.unloadedClasses=37
...

2.4 jcmd pid GC.class_histogram,查看系统中类的统计信息

E:\itstack\git\github.com\interview>jcmd 111552 GC.class_histogram
111552:

 num     #instances         #bytes  class name
----------------------------------------------
   1:         50543        3775720  [C
   2:          3443        2428248  [I
   3:         50138        1203312  java.lang.String
   4:         25351         811232  java.util.HashMap$Node
   5:          6263         712208  java.lang.Class
   6:          3134         674896  [B
   7:          6687         401056  [Ljava.lang.Object;
   8:          2468         335832  [Ljava.util.HashMap$Node;

2.5 jcmd pid Thread.print,查看线程堆栈信息

E:\itstack\git\github.com\interview>jcmd 111552 Thread.print
111552:
2021-01-10 23:31:13
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.161-b12 mixed mode):

"Computes values in handlers" #52 daemon prio=5 os_prio=0 tid=0x0000000019839000 nid=0x16014 waiting for monitor entry [0x0000000026bce000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.sun.tools.visualvm.core.model.ModelFactory.getModel(ModelFactory.java:76)
        - waiting to lock <0x00000000f095bcf8> (a com.sun.tools.visualvm.jvmstat.application.JvmstatApplication)
        at com.sun.tools.visualvm.application.jvm.JvmFactory.getJVMFor(JvmFactory.java:45)
        at com.sun.tools.visualvm.application.options.Open.openApplication(Open.java:108)
        at com.sun.tools.visualvm.application.options.Open.process(Open.java:93)
        at org.netbeans.spi.sendopts.Option$1.process(Option.java:348)
        at org.netbeans.api.sendopts.CommandLine.process(CommandLine.java:278)
        at org.netbeans.modules.sendopts.HandlerImpl.execute(HandlerImpl.java:23)
        at org.netbeans.modules.sendopts.Handler.cli(Handler.java:30)
        at org.netbeans.CLIHandler.notifyHandlers(CLIHandler.java:195)
        at org.netbeans.core.startup.CLICoreBridge.cli(CLICoreBridge.java:43)
        at org.netbeans.CLIHandler.notifyHandlers(CLIHandler.java:195)
        at org.netbeans.CLIHandler$Server$1ComputingAndNotifying.run(CLIHandler.java:1176)

2.6 jcmd pid VM.system_properties,查看JVM系统参数

E:\itstack\git\github.com\interview>jcmd 111552 VM.system_properties
111552:
#Sun Jan 13 23:33:19 CST 2021
java.vendor=Oracle Corporation
netbeans.user=C\:\\Users\\xiaofuge1\\AppData\\Roaming\\VisualVM\\8u131
sun.java.launcher=SUN_STANDARD
sun.management.compiler=HotSpot 64-Bit Tiered Compilers
netbeans.autoupdate.version=1.23
os.name=Windows 10

2.7 jcmd pid GC.heap_dump 路径,导出heap dump文件

E:\itstack\git\github.com\interview>jcmd 111552 GC.heap_dump C:\Users\xiaofuge1\Desktop\_dump_0110
111552:
Heap dump file created
  • 导出的文件需要配合 jvisualvm 查看

2.8 jcmd pid help,列出可执行操作

E:\itstack\git\github.com\interview>jcmd 111552 help
111552:
The following commands are available:
JFR.stop
JFR.start
JFR.dump
JFR.check

2.9 jcmd pid help JFR.stop,查看命令使用

E:\itstack\git\github.com\interview>jcmd 111552 help JFR.stop
111552:
JFR.stop
Stops a JFR recording

Impact: Low

Permission: java.lang.management.ManagementPermission(monitor)

Syntax : JFR.stop [options]

Options: (options must be specified using the <key> or <key>=<value> syntax)
        name : [optional] Recording name,.e.g \"My Recording\" (STRING, no default value)
        recording : [optional] Recording number, see JFR.check for a list of available recordings (JLONG, -1)
        discard : [optional] Skip writing data to previously specified file (if any) (BOOLEAN, false)
        filename : [optional] Copy recording data to file, e.g. \"C:\Users\user\My Recording.jfr\" (STRING, no default value)
        compress : [optional] GZip-compress "filename" destination (BOOLEAN, false)

3. jinfo Java配置信息工具

jinfo(Configuration Info for Java),实时查看和调整JVM的各项参数。

在上面讲到 jps -v 指令时,可以看到它把虚拟机启动时显式的参数列表都打印出来了,但如果想更加清晰的看具体的一个参数或者想知道未被显式指定的参数时,就可以通过 jinfo -flag 来查询了。

命令格式

jinfo [ option ] pid

使用方式

E:\itstack\git\github.com\interview>jinfo -flag MetaspaceSize 111552
-XX:MetaspaceSize=21807104

E:\itstack\git\github.com\interview>jinfo -flag MaxMetaspaceSize 111552
-XX:MaxMetaspaceSize=18446744073709486080

E:\itstack\git\github.com\interview>jinfo -flag HeapDumpPath 111552
-XX:HeapDumpPath=C:\Users\xiaofuge\AppData\Roaming\VisualVM\8u131\var\log\heapdump.hprof
  • 各种JVM参数你都可以去查询,这样更加方便的只把你要的显示出来。

4. jstat 收集虚拟机运行数据

jstat(JVM Statistics Monitoring Tool),用于监视虚拟机各种运行状态信息。它可以查看本地或者远程虚拟机进程中,类加载、内存、垃圾收集、即时编译等运行时数据。

命令格式

`jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
`

  • vmid:如果是查看远程机器,需要按照此格式:[protocol:][//]lvmid[@hostname[:port]/servername]
  • interval和count,表示查询间隔和次数,比如每隔1000毫秒查询一次进程ID的gc收集情况,每次查询5次。jstat -gc 111552 1000 5

选项列表

选项描述
-class监视类加载、卸载数量、总空间以及类装载所耗费时长
-gc监视 Java 堆情况,包括Eden区、2个 Survivor区、老年代、永久代或者jdk1.8元空间等,容量、已用空间、垃圾收集时间合计等信息
-gccapacity监视内容与-gc基本一致,但输出主要关注 Java 堆各个区域使用到的最大、最小空间
-gcutil监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因
-gcnew监视新生代垃圾收集情况
-gcnewcapacity监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间
-gcold监视老年代垃圾收集情况
-gcoldcapacity监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间
-compiler输出即时编译器编译过的方法、耗时等信息
-printcompilation输出已经被即时编译的方法
-gcpermcapacityjdk1.7 及以下,永久代空间统计
-gcmetacapacityjdk1.8,元空间统计
  • jstat 的监视选项还是非常多的,但最常用的主要有上面这些。

4.01 jstat -class,类加载统计

E:\itstack\git\github.com\interview>jstat -class 111552
Loaded  Bytes  Unloaded  Bytes     Time
  5835 12059.6       37    53.5       3.88
  • Loaded,加载class的数量
  • Bytes:所占用空间大小
  • Unloaded:未加载数量
  • Bytes:未加载占用空间
  • Time:时间

4.02 jstat -compiler,编译统计

E:\itstack\git\github.com\interview>jstat -compiler 111552
Compiled Failed Invalid   Time   FailedType FailedMethod
    3642      0       0     5.61          0
  • Compiled:编译数量
  • Failed:失败数量
  • Invalid:不可用数量
  • Time:时间
  • FailedType:失败类型
  • FailedMethod:失败方法

4.03 jstat -gc,垃圾回收统计

E:\itstack\git\github.com\interview>jstat -gc 111552
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
1024.0 512.0   0.0    0.0   77312.0    35.1    39424.0    13622.9   37120.0 34423.3 5376.0 4579.4     60    0.649  52      3.130    3.779
  • S0C、S1C,第一个和第二个幸存区大小
  • S0U、S1U,第一个和第二个幸存区使用大小
  • EC、EU,伊甸园的大小和使用
  • OC、OU,老年代的大小和使用
  • MC、MU,方法区的大小和使用
  • CCSC、CCSU,压缩类空间大小和使用
  • YGC、YGCT,年轻代垃圾回收次数和耗时
  • FGC、FGCT,老年代垃圾回收次数和耗时
  • GCT,垃圾回收总耗时

4.04 jstat -gccapacity,堆内存统计

E:\itstack\git\github.com\interview>jstat -gccapacity 111552
 NGCMN    NGCMX     NGC     S0C   S1C       EC      OGCMN      OGCMX       OGC         OC       MCMN     MCMX      MC     CCSMN    CCSMX     CCSC    YGC    FGC
  8192.0  87040.0  80384.0 1024.0  512.0  77312.0    16384.0   175104.0    39424.0    39424.0      0.0 1081344.0  37120.0      0.0 1048576.0   5376.0     60    52
  • NGCMN、NGCMX,新生代最小和最大容量
  • NGC,当前新生代容量
  • S0C、S1C,第一和第二幸存区大小
  • EC,伊甸园区的大小
  • OGCMN、OGCMX,老年代最小和最大容量
  • OGC、OC,当前老年代大小
  • MCMN、MCMX,元数据空间最小和最大容量
  • MC,当前元空间大小
  • CCSMN、CCSMX,压缩类最小和最大空间
  • YGC,年轻代GC次数
  • FGC,老年代GC次数

4.05 jstat -gcnewcapacity,新生代内存统计

E:\itstack\git\github.com\interview>jstat -gcnewcapacity 111552
  NGCMN      NGCMX       NGC      S0CMX     S0C     S1CMX     S1C       ECMX        EC      YGC   FGC
    8192.0    87040.0    80384.0  28672.0   1024.0  28672.0    512.0    86016.0    77312.0    60    52
  • NGCMN、NGCMX,新生代最小和最大容量
  • NGC,当前新生代容量
  • S0CMX,最大幸存0区大小
  • S0C,当前幸存0区大小
  • S1CMX,最大幸存1区大小
  • S1C,当前幸存1区大小
  • ECMX,最大伊甸园区大小
  • EC,当前伊甸园区大小
  • YGC,年轻代垃圾回收次数
  • FGC,老年代回收次数

4.06 jstat -gcnew,新生代垃圾回收统计

E:\itstack\git\github.com\interview>jstat -gcnew 111552
 S0C    S1C    S0U    S1U   TT MTT  DSS      EC       EU     YGC     YGCT
1024.0  512.0    0.0    0.0  3  15  512.0  77312.0     70.2     60    0.649
  • S0C、S1C,第一和第二幸存区大小
  • S0U、S1U,第一和第二幸存区使用
  • TT,对象在新生代存活的次数
  • MTT,对象在新生代存活的最大次数
  • DSS:期望的幸存区大小
  • EC,伊甸园区的大小
  • EU,伊甸园区的使用
  • YGC,年轻代垃圾回收次数
  • YGCT,年轻代垃圾回收消耗时间

4.07 jstat -gcold,老年代垃圾回收统计

E:\itstack\git\github.com\interview>jstat -gcold 111552
   MC       MU      CCSC     CCSU       OC          OU       YGC    FGC    FGCT     GCT
 37120.0  34423.3   5376.0   4579.4     39424.0     13622.9     60    52    3.130    3.779
  • MC、MU,方法区的大小和使用
  • CCSC、CCSU,压缩类空间大小和使用
  • OC、OU,老年代大小和使用
  • YGC,年轻代垃圾回收次数
  • FGC,老年代垃圾回收次数
  • FGCT,老年代垃圾回收耗时
  • GCT,垃圾回收总耗时

4.08 jstat -gcoldcapacity,老年代内存统计

E:\itstack\git\github.com\interview>jstat -gcoldcapacity 111552
   OGCMN       OGCMX        OGC         OC       YGC   FGC    FGCT     GCT
    16384.0    175104.0     39424.0     39424.0    60    52    3.130    3.779
  • OGCMN、OGCMX,老年代最小和最大容量
  • OGC,当前老年代大小
  • OC,老年代大小
  • YGC,年轻代垃圾回收次数
  • FGC,老年代垃圾回收次数
  • FGCT,老年代垃圾回收耗时
  • GCT,垃圾回收消耗总耗时

4.09 jstat -gcmetacapacity,元空间统计

E:\itstack\git\github.com\interview>jstat -gcmetacapacity 111552
   MCMN       MCMX        MC       CCSMN      CCSMX       CCSC     YGC   FGC    FGCT     GCT
       0.0  1081344.0    37120.0        0.0  1048576.0     5376.0    60    52    3.130    3.779
  • MCMN、MCMX,元空间最小和最大容量
  • MC,当前元数据空间大小
  • CCSMN、CCSMX,压缩类最小和最大空间
  • CCSC,压缩类空间大小
  • YGC,年轻代垃圾回收次数
  • FGC,老年代垃圾回收次数
  • FGCT,老年代垃圾回收耗时
  • GCT,垃圾回收消耗总耗时

4.10 jstat -gcutil,垃圾回收统计

E:\itstack\git\github.com\interview>jstat -gcutil 111552
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00   0.09  34.55  92.74  85.18     60    0.649    52    3.130    3.779
  • S0、S1、幸存1区和2区,当前使用占比
  • E,伊甸园区使用占比
  • O,老年代区使用占比
  • M,元数据区使用占比
  • CCS,压缩类使用占比
  • YGC,年轻代垃圾回收次数
  • FGC,老年代垃圾回收次数
  • FGCT,老年代垃圾回收耗时
  • GCT,垃圾回收消耗总耗时

4.11 jstat -printcompilation,JVM编译方法统计

E:\itstack\git\github.com\interview>jstat -printcompilation 111552
Compiled  Size  Type Method
    3642      9    1 java/io/BufferedWriter min
  • Compiled:最近编译方法的数量
  • Size:最近编译方法的字节码数量
  • Type:最近编译方法的编译类型
  • Method:方法名标识

5. jmap 内存映射工具

jmap(Memory Map for Java),用于生成堆转储快照(heapdump文件)。

jmap 的作用除了获取堆转储快照,还可以查询finalize执行队列、Java 堆和方法区的详细信息。

命令格式

jmap [ option ] pid

  • option:选项参数
  • pid:需要打印配置信息的进程ID
  • executable:产生核心dump的Java可执行文件
  • core:需要打印配置信息的核心文件
  • server-id:可选的唯一id,如果相同的远程主机上运行了多台调试服务器,用此选项参数标识服务器
  • remote server IP or hostname: 远程调试服务器的IP地址或主机名

选项列表

选项描述
-dump生成 Java 堆转储快照。
-finalizerinfo显示在F-Queue中等待Finalizer线程执行finalize方法的对象。Linux平台
-heap显示 Java 堆详细信息,比如:用了哪种回收器、参数配置、分代情况。Linux平台
-histo显示堆中对象统计信息,包括类、实例数量、合计容量
-permstat显示永久代内存状态,jdk1.7,永久代
-F当虚拟机进程对 -dump 选项没有响应式,可以强制生成快照。Linux平台

5.1 jmap,打印共享对象映射

E:\itstack\git\github.com\interview>jmap 111552
Attaching to process ID 111552, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.161-b12
0x000000005b4a0000      1632K   C:\Program Files\Java\jdk1.8.0_161\jre\bin\awt.dll
0x000000005b8c0000      264K    C:\Program Files\Java\jdk1.8.0_161\jre\bin\t2k.dll
0x000000005b910000      284K    C:\Program Files\Java\jdk1.8.0_161\jre\bin\fontmanager.dll
0x000000005b960000      224K    C:\Program Files\Java\jdk1.8.0_161\jre\bin\splashscreen.dll
0x000000005b9a0000      68K     C:\Program Files\Java\jdk1.8.0_161\jre\bin\nio.dll

5.2 jmap -heap,堆详细信息

E:\itstack\git\github.com\interview>jmap -heap 111552
Attaching to process ID 111552, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.161-b12

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 268435456 (256.0MB)
   NewSize                  = 8388608 (8.0MB)
   MaxNewSize               = 89128960 (85.0MB)
   OldSize                  = 16777216 (16.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

5.3 jmap -clstats,打印加载类

E:\itstack\git\github.com\interview> jmap -clstats 111552
Attaching to process ID 111552, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.161-b12
finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness.................................................................liveness analysis may be inaccurate ...
class_loader    classes bytes   parent_loader   alive?  type

<bootstrap>     3779    6880779   null          live    <internal>
0x00000000f03853b8      57      132574  0x00000000f031aac8      live    org/netbeans/StandardModule$OneModuleClassLoader@0x00000001001684f0
0x00000000f01b9b98      0       0       0x00000000f031aac8      live    org/netbeans/StandardModule$OneModuleClassLoader@0x00000001001684f0
0x00000000f005b280      0       0       0x00000000f031aac8      live    java/util/ResourceBundle$RBClassLoader@0x00000001000c6ae0
0x00000000f01dfa98      0       0       0x00000000f031aac8      live    org/netbeans/StandardModule$OneModuleClassLoader@0x00000001001684f0
0x00000000f01ec518      79      252894  0x00000000f031aac8      live    org/netbeans/StandardModule$OneModuleClassLoader@0x00000001001684f0

5.4 jmap -dump,堆转储文件

E:\itstack\git\github.com\interview>jmap -dump:live,format=b,file=C:/Users/xiaofuge/Desktop/heap.bin 111552
Dumping heap to C:\Users\xiaofuge\Desktop\heap.bin ...
Heap dump file created

6. jhat 堆转储快照分析工具

jhat(JVM Heap Analysis Tool),与jmap配合使用,用于分析jmap生成的堆转储快照。

jhat内置了一个小型的http/web服务器,可以把堆转储快照分析的结果,展示在浏览器中查看。不过用途不大,基本大家都会使用其他第三方工具。

命令格式

`jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
`

命令使用

E:\itstack\git\github.com\interview>jhat -port 8090 C:/Users/xiaofuge1/Desktop/heap.bin
Reading from C:/Users/xiaofuge1/Desktop/heap.bin...
Dump file created Wed Jan 13 16:53:47 CST 2021
Snapshot read, resolving...
Resolving 246455 objects...
Chasing references, expect 49 dots.................................................
Eliminating duplicate references.................................................
Snapshot resolved.
Started HTTP server on port 8090
Server is ready.

http://localhost:8090/

jhat -port 8090

7. jstack Java堆栈跟踪工具

jstack(Stack Trace for Java),用于生成虚拟机当前时刻的线程快照(threaddump、javacore)。

线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如:线程死锁、死循环、请求外部资源耗时较长导致挂起等。

线程出现听顿时通过jstack来查看各个线程的调用堆栈,就可以获得没有响应的线程在搞什么鬼。

命令格式

jstack [ option ] vmid

选项参数

选项描述
-F当正常输出的请求不被响应时,强制输出线程堆栈
-l除了堆栈外,显示关于锁的附加信息
-m如果调用的是本地方法的话,可以显示c/c++的堆栈

命令使用

E:\itstack\git\github.com\interview>jstack 111552
2021-01-10 23:15:03
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.161-b12 mixed mode):

"Inactive RequestProcessor thread [Was:StdErr Flush/org.netbeans.core.startup.logging.PrintStreamLogger]" #59 daemon prio=1 os_prio=-2 tid=0x000000001983a800 nid=0x688 in Object.wait() [0x0000000017fbf000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        at org.openide.util.RequestProcessor$Processor.run(RequestProcessor.java:1939)
        - locked <0x00000000fab31d88> (a java.lang.Object)
  • 在验证使用的过程中,可以尝试写一个死循环的线程,之后通过jstack查看线程信息。

四、可视化故障处理工具

1. jconsole,Java监视与管理控制台

JConsole( Java Monitoring and Management Console),是一款基于JMX( Java Manage-ment
Extensions) 的可视化监视管理工具。

它的功能主要是对系统进行收集和参数调整,不仅可以在虚拟机本身管理还可以开发在软件上,是开放的服务,有相应的代码API调用。

JConsole 启动

JConsole 启动

JConsole 使用

JConsole 使用

2. VisualVM,多合故障处理工具

VisualVM( All-in-One Java Troubleshooting Tool),是功能最强大的运行监视和故障处理工具之一。

它除了常规的运行监视、故障处理外,还可以做性能分析等工作。因为它的通用性很强,对应用程序影响较小,所以可以直接接入到生产环境中。

VisualVM IDEA安装

VisualVM IDEA安装

VisualVM 使用

public static void main(String[] args) throws InterruptedException {
    
    Thread.sleep(5000);
    ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
    while (true) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(MetaSpaceOomMock.class);
        enhancer.setCallbackTypes(new Class[]{Dispatcher.class, MethodInterceptor.class});
        enhancer.setCallbackFilter(new CallbackFilter() {
            @Override
            public int accept(Method method) {
                return 1;
            }
            @Override
            public boolean equals(Object obj) {
                return super.equals(obj);
            }
        });
        System.out.println(enhancer.createClass().getName() + loadingBean.getTotalLoadedClassCount() + loadingBean.getLoadedClassCount() + loadingBean.getUnloadedClassCount());
    }
}

记得调整元空间大小

-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=80m
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=7397
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
  • 我们就监测这段让元空间溢出的代码,java.lang.OutOfMemoryError: Metaspace

监控结果

VisualVM,监控结果

五、总结

  • 本文也是小傅哥在学习《深入理解Java虚拟机》过程中的一个总结,这里包括了很多常用的指令,通过这些指令的学习我们也大概会知道JVM都给我们提供了什么样的监控信息。
  • 其实实际的业务使用中很少通过指令去监控JVM而是有一整套的非入侵全链路监控,在监控服务里与之方法调用时的JVM一并监控,可以让研发人员更快速的排查问题。但这些工具的实现依然是需要这些基础,在有了基础的知识掌握后,可以更好多使用工具。
  • 编程技术类知识的学习一定要实践验证,否则很容易忘记,也很难掌握。当你经过自己手多敲几遍以后,就会有完全不一样的认识。好了,加油!希望本篇文章能为你的薪资鼓鼓劲!

六、系列推荐

查看原文

赞 3 收藏 3 评论 0

小傅哥 关注了用户 · 1月12日

思否编辑部 @writers

让我们陷入困境的不是无知,而是看似正确的谬误论断。思考、否定、再思考,出家人不打诳语,撰文者不说空话。

欢迎通过私信投稿、提建议、分享素材、传闲话。

联系邮箱 pr@sifou.com 小姐姐微信:https://segmentfault.com/n/13...

关注 5932

小傅哥 发布了文章 · 1月11日

互联网大厂,常见研发线上事故总结!

作者:小傅哥

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

你的代码出过事故吗?

老人言:常在河边走哪有不湿鞋。只要你在做着编程开发的工作就一定会遇到事故,或大或小而已。

当然可能有一部分研发同学,在相对传统的行业或者做着用户体量较小的业务等,很难遇到让人出名的事故,多数都是一些线上的小bug,修复了也就没人问了。

但如果你在较大型的互联网公司,那么你负责的开发的系统功能,可能面对的就是成百万、上千万级别用户体量。哪怕你有一点小bug也会被迅速放大,造成大批量的客诉以及更严重的资金损失风险。就像:

  1. 拼多多“薅羊毛”事件,朋友圈疯狂转发。
  2. 淘宝昨现重大线上bug,S1级事故,疑似程序员故意埋雷。您使用的程序是内测版本,将于当地时间 2020-03-28 到期,到期后将无法使用,请尽快下载最新版本。
  3. GitHub忘记续订SSL证书导致网站排版混乱,部分网站不能正常打开。

类似这样事故的出现,可能是因为技术流程、方案实现、技术服务以及运营配置等等原因产生的。综合可以概括为以下几点:

图 19-1 事故类型总结

  • 功能流程设计类:通常指的是研发在设计产品逻辑功能实现流程中,错误的执行调用关系而造成的风险事故。
  • 技术方案实现类:在研发设计好流程后,每一个功能点的实现方案会因人而异,也会由于理解偏差或不足,而导致实现过程中缺少了对代码在运行过程中健壮性的评估。
  • 技术服务使用类:这一类说的是在研发使用数据库服务、缓存服务、大数据服务、配置中心服务以及发布上线服务等时,对各项服务的配置以及使用上缺少一定的了解,而造成的事故。
  • 后门违规操作类:这一类因公司对研发规范的执行强度不同,而是否会有此类风险。例如:有些研发同学会开发一些后门程序,比如可以在某个ERP页面执行数据库语句,临时修改数据。这样造成的风险,通常为后门违规操作,会有开除风险。
  • 运营操作失误类:在研发以为还有一部分公司内的伙伴会使用研发同学开发的运营系统,配置活动、变更用户、执行流程等操作,但一般情况下这类系统缺少一定的强规则验证,导致运营小白在操作过程中造成风险,从而引发事故。一般线上配置出错误卷,或者推错短信给用户等等,都是这样发生的。

可以说,大多数比较蠢的事故主要是个人责任心问题。但那些有技术含量的事故,犯一次还是挺值得的。虽然公司很讨厌你造成事故,因为会给公司带来损失嘛!但这样具有具有技术含量的事故,却对你个人成长非常好的案例。不过禁酒虽好,可不能贪杯!

接下来,小傅哥就带着你领略下各类事故的风采,看看在什么场景、遇到什么问题、怎么解决的以及能学到什么!

二、研发事故

1. 功能流程设计类

图 19-2 功能流程设计类事故

  • 事故级别:P1
  • 事故判责:相应的研发、测试总结复盘,罚款50元给参加的会议的伙伴买棒棒糖以示警告。
  • 事故名称:抽奖积分支付流程不合理
  • 事故现象:用户积分多支付,造成批量客诉,当天紧急排查修复,并给用户补充积分。
  • 事故描述:这个产品功能的背景可能很大一部分研发都参与开发过,简单说就是满足用户使用积分抽奖的一个需求。上图左侧就是研发最开始设计的流程,通过RPC接口扣减用户积分,扣减成功后进行抽奖。但由于当天RPC服务不稳定,造成RPC实际调用成功,但返回超时失败。而调用RPC接口的uuid是每次自动生成的,不具备调用幂等性。所以造成了用户积分多支付现象。
  • 事故处理:事故后修改抽奖流程,先生成待抽奖的抽奖单,由抽奖单ID调用RPC接口,保证接口幂等性。在RPC接口失败时由定时任务补偿的方式执行抽奖。流程整改后发现,补偿任务每周发生1~3次,那么也就是证明了RPC接口确实有可用率问题,同时也说明很久之前就有流程问题,但由于用户客诉较少,所以没有反馈。
  • 学习总结: 调用的接口、发送的MQ,并不一定会每次都成功。那么一定要做好幂等性以及失败后的补偿,来把整个技术实现流程做的更加完善。就像小傅哥说的,擦屁屁的纸80%的面积其实都是保护手的!

网友事故分享:

事故名称事故描述事故结果
业务流程搞错+代码频繁开辟线程池业务流程搞错导致的问题就是改动特别大,有点类似重构代码了哎西,导致服务宕机很久,客户疯狂反馈疯狂加班改业务,加了不知道多少个晚上,由于是菜狗子,写的代码太垃圾了,被大佬给疯狂叼
线上修改用户收货地址失败(同事需要的问题,也可以借鉴下^v^)场景:客服反应用户需要把收货地址从河北省改为浙江省(因为疫情),公司要求修改线上数据需要提交工单,因此l到审核平台提交申请等一系列流程。 问题:工单显示修改结果成功,但是数据没有改过来,多为同事一起查看sql发现sql编写没有问题。 解决过程:检查sql是否正确,平台是否修改成功,又检查了数据是否正确,还检查了修改时间是没有问题的。首先,忽略了一个问题,这个订单数据是淘宝下单同步到我们订单这边的,数据修改的后,淘宝又同步也数据过来,把修改正确的数据又改为了河北的地址。然后就怀疑sql审核平台问题。到这里故事已经讲完了。结论:想告诉大家要相信代码,多检查不确定的情况,不要钻到死胡同,老是怀疑审核平台问题,多检查自身问题。
业务相关事故刚加入一个新的团队,没有深入了解别人的代码就进行复用,没有理解业务的场景就限制条件,类似的情况很多,只能说,再简单的代码都要保持敬畏,因为你不知道哪里会出问题用户投诉、领导批评

2. 技术方案实现类

图 19-3 技术方案实现类事故

  • 事故级别:P0
  • 事故判责:营销活动推广用户较多,影响范围较大,研发整改代码并做复盘。
  • 事故名称:秒杀方案独占竞态实现问题
  • 事故现象:用户看到可以购买,但只要一点下单就活动太火爆,换个小手试试。造成了大量客诉,紧急下线活动排查。
  • 事故描述:这个一个商品活动秒杀的实现方案,最开始的设计是基于一个活动号ID进行锁定,秒杀时锁定这个ID,用户购买完后就进行释放。但在大量用户抢购时,出现了秒杀分布式锁后的业务逻辑处理中发生异常,释放锁失败。导致所有的用户都不能再拿到锁,也就造成了有商品但不能下单的问题。
  • 事故处理:优化独占竞态为分段静态,将活动ID+库存编号作为动态锁标识。当前秒杀的用户如果发生锁失败那么后面的用户可以继续秒杀不受影响。而失败的锁会有worker进行补偿恢复,那么最终会避免超卖以及不能售卖。
  • 学习总结: 核心的技术实现需要经过大量的数据验证以及压测,否则各个场景下很难评估是否会有风险。当然这也不是唯一的实现方案,可以根据不同的场景有不同的实现处理。

网友事故分享:

事故名称事故描述事故结果
gc疯狂回收最近调整了自己业余项目,跑一段时间就内存狂涨,还不能主动诱发dump内存中
重复扣入账并发数过多,数据库连接满,等待超时,session断开。事务未提交,捞出继续干500块,深入并发编程,目前并发模型在我心中,欢迎battle
数据覆盖循环更新数据时,开启事务,持续时间过长,然后覆盖掉了用户在持续事务中提交的数据没影响,就是多加了几天班
数据穿透第三分使用脚本海里请求并发造成数据穿透削峰天谷, 使用队列处理请求
这序列号咋重复了??序列号应具有全局的唯一,一条数据代表一条收入,序列号生成规则+代码bug导致序列号重复,影响了几万单收入核对一级事故,回溯+通报
业务流程数据覆盖启流程是一个公共类,各种交易都在这个里面做,公共类一开始没有经过设计,有一个方法返回了这个模板类型字段,同时这个方法又是一个检验类,当时加了一个检验返回成错误码了,导致所有的交易都启不了流程。挨批长记性。。。
simpledateformat的线程不安全导致多线程定时任务解析日期出错某定时任务运行时,需要做一些日期解析动作,就用了一个公共变量simpledateformat,来格式化,结果任务经常间歇性报错,几天报一次或者一两周报一次,没啥规律。看异常信息才发现解析日期的字符串很奇怪,经常出现很多奇奇怪怪的数字。定时任务报错,不过还好,定时任务只是为了做缓存而已,不涉及到数据库的更新,仅仅是查询而已。
前端解析主键异常由于Long类型最大19位而JavaScript最大接收数字为16位,固存在精度丢失问题统一处理将id转字符串再返回前端
list遍历删除遍历删除清空list数组,为了节省计数器那小小一点内存,日了报错被叼了呗,为啥不用计数器?不香吗?
商品超卖售卖一个兄弟部门的电子券商品,同步库存的代码有问题,导致了超卖 对客户造成了损失罚款1000元

3. 技术服务使用类

图 19-4 技术服务使用类事故

  • 事故级别:P2
  • 事故判责:网友说被叼了一会,问题不大!
  • 事故名称:扩容时忽略了连接池梳理,导致连接池被打满
  • 事故现象:线上突然收到报警短信,打开电脑一看,简单的查询接口超时到3分钟才返回。
  • 事故描述:幸好监控报警加的全,及时收到了报警短信,联系DBA检查发现连接池被打满了。为了快速解决线上报警,优先临时扩容了连接池以及把服务重启。观察后连接池打满消失了。
  • 事故处理:检查应用数据库连接池配置,以及额外不经常上线的服务一并排查。经查询发现所有的应用加起来连接池的最高配置超过数据库分配的连接池数量。尤其是定时任务较长时间扫库处理,是直接导致连接池打满的重要原因。
  • 学习总结: 研发不仅是代码开发搬砖人员,还要了解熟悉与之配套的服务。合理的使用、全面的考量才能避免一些看似不应该出现的事故问题。

网友事故分享:

事故名称事故描述事故结果
使用fastjson全身上下都是高危漏洞,一年不停升级版本打补丁珍惜生命,远离fastjson
微信名存储bug微信名的emj头等存入mysql编码是utf8的库报错被怼了!改成utf8mb4编码
磁盘不足数据库集群磁盘空间不足,提前两周提交扩容申请,甲方运维没提交上去,最后某台机器空间不足,导致整个集群彻底不能工作,体验一把某国产号称可以方便横向扩容的某idb的优越性罚款,责任归系统建设方

4. 后门违规操作类

图 19-5 后门违规操作类事故

  • 事故级别:P0
  • 事故判责:网友反馈,私自开发后门,执行sql错误,影响较大。开除!
  • 事故名称:通过后门程序修改线上数据
  • 事故现象:这次修改影响范围比想象的要小,只有部分数据因为缓存失效了,才读取数据库的活动信息。所有有少部分客诉说活动与名称不符合。
  • 事故描述:研发人员应运营要求修改线上配置错误的活动名称,但任何邮件记录以及负责人审批。所以只是研发私自通过后门程序提交sql语句修改,但忘记写where条件,造成几千条活动名称被同时修改。
  • 事故处理:事后联系DBA紧急通过binlog日志进行数据修复。
  • 学习总结: 研发人员应避免操作线上数据,尤其是变更数据类。也不要开发各类改数据、上线、传配置文件等后门。而应该严格遵守研发流程,紧急事情需要请求批准处理。

网友事故分享:

事故名称事故描述事故结果
删除整个项目目录文件测试区,测试删除文件时目录写错,导致整个weblogic子项目目录被删请项目中负责集成部署的公司帮忙重新部署,测试区瘫痪了两天
误更新生产订单数据3万多条下班前,未带核心过滤条件,导致误更新3万多条订单数据,偷偷利用binlog恢复了,耗时3个小时完美恢复数据
线上库整库误删除应业务方要求要在线上环境创建线上联调库,使用了导出数据库DDL语句后,直接执行,导致执行了exists drop语句,删除了线上库所有数据,数据量大表均在千万级,APP、网站全线瘫痪。使用前一天的备份副本数据恢复,下载binlog日志按操作避开事发时间点分割后编译,导入数据,然后再修复事故之后的数据,共计耗时48小时。

5. 运营操作失误类

图 19-6 运营操作失误类事故

  • 事故级别:P2
  • 事故判责:网友说,金额太大没发出去!被喷了一会!
  • 事故名称:运营把券配置成红包
  • 事故现象:线上用户客诉,看到几百亿大的红包,领不到!
  • 事故描述:运营人员配置优惠券,但是类型选成了红包,导致页面展示出超大额的红包金额待领取,都超出屏幕长度了!
  • 事故处理:紧急下线活动,重新配置上线。同时产品设计需求,由研发人员实现对于此类配置提供明确、醒目的配置和完整的审核流程。如果配置红包、优惠券,会有校验此券的是否存在以及红包最大金额限制。
  • 学习总结: 看上去是运营配置错误,但从某个角度看其实也可以说是研发在做功能实现时,太过于单一完成产品功能,而没有加深考虑以及产品的易用性。有时候多问一句就少一个风险!

网友事故分享:

事故名称事故描述事故结果
业务漏洞业务乱配优惠券,可以叠加,超级优惠,然后被薅羊毛部门帮着查羊毛记录,处理订单,挽回损失。然后对外发公告宣称是被部门的风控系统误杀的。
贷款费率运营配置T+1日结算贷款费率错误,导致用户贷款金额发生错误。上线新费率替换旧费率,已经产生的费率错误联系贷款用户修复。
多活动互斥三个部门的都做活动,但最后导致重复发奖。一个用户邀请别人奖励,变成了三份奖励。产品提供渠道和互斥功能,让运营自己选择是否可以并行发放奖励。

三、总结

  • 讲道理,开发没事故,不是没用户体量,就是没用户规模。否则只要是人就一定会出现事故,要不是小bug被你销声匿迹隐藏了,或者是大事故被喷了或者送飞机了。
  • 而尽可能减少事故的方式是需要尽可能按照一定的研发流程来实现功能逻辑。就像:设计评审,把控的是实现流程、代码评审,把控的是实现方案,在配合上完善的监控和报警。只有这样才能更少的减少不必要的事故。
  • 关于研发在职场中的事故本文就讲到这了,感谢粉丝分享出自己的遇到的事故,让大家可以互相学习,减少离职扣工资的风险。😄多关注小傅哥,一个写有价值原创好文章的男人!

四、系列推荐


博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki

查看原文

赞 21 收藏 14 评论 0

小傅哥 发布了文章 · 1月7日

JVM内存模型总结,有各版本JDK对比、有元空间OOM监控案例、有Java版虚拟机,综合实践学习!


作者:小傅哥
博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

看了一篇文章30岁有多难!

每篇文章的开篇总喜欢写一些,从个人视角看这个世界的感悟。

最近看到一篇文章,30岁有多难。文中的一些主人公好像在学业、工作、生活、爱情等方面都过的都不如意。要不是错过这,要不是走错那。总结来看,就像是很倒霉的一群倒霉蛋儿在跟生活对干!

但其实每个人可能都遇到过生活中最难的时候,或早或晚。就像我刚毕业不久时一连串遇到;冬天里丢过第一部手机修一个进了水的电脑租的房子第一次被骗,一连串下来头一次要赶在工资没发的时候,选择少吃早饭还是午饭,看看能扛过去那顿。

哈哈哈哈哈,现在想想还挺有意思的,不过这些乱遭的事很多是自己的意识和能力不足时做出的错误选择而导致的。

人那,想开车就要考驾照,想走远就要有能力。多提升认知,多拓宽眼界!生活的意义就是不断的更新自己!

二、面试题

谢飞机,小记!,冬风吹、战鼓擂。被窝里,谁怕谁。

谢飞机:歪?大哥,你在吗?

面试官:咋了,大周末的,这么早打电话!?

谢飞机:我梦见,我去谷歌写JVM了,给你们公司用,之后蹦了,让我起来改bug!

面试官:啊!?啊,那我问你,JDK 1.8 与 JDK 1.7 在运行时数据区的设计上,你都怎么做的优化策略的?

谢飞机:我没写这,我不知道!

面试官:擦。。。

三、 JDK1.6、JDK1.7、JDK1.8 内存模型演变

图 25-1  JDK1.6、JDK1.7、JDK1.8,内存模型演变

如图 25-1 是 JDK 1.6、1.7、1.8 的内存模型演变过程,其实这个内存模型就是 JVM 运行时数据区依照JVM虚拟机规范的具体实现过程。

在图 25-1 中各个版本的迭代都是为了更好的适应CPU性能提升,最大限度提升的JVM运行效率。这些版本的JVM内存模型主要有以下差异:

  • JDK 1.6:有永久代,静态变量存放在永久代上。
  • JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
  • JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。

四、内存模型各区域介绍

1. 程序计数器

  • 较小的内存空间、线程私有,记录当前线程所执行的字节码行号。
  • 如果执行 Java 方法,计数器记录虚拟机字节码当前指令的地址,本地方法则为空。
  • 这一块区域没有任何 OutOfMemoryError 定义。

以上,就是关于程序计数器的定义,如果这样看没有感觉,我们举一个例子。

定义一段 Java 方法的代码,这段代码是计算圆形的周长。

public static float circumference(float r){
        float pi = 3.14f;
        float area = 2 * pi * r;
        return area;
}

接下来,如图 25-2 是这段代码的在虚拟机中的执行过程,左侧是它的程序计数器对应的行号。

图 25-2 程序计数器

  • 这些行号每一个都会对应一条需要执行的字节码指令,是压栈还是弹出或是执行计算。
  • 之所以说是线程私有的,因为如果不是私有的,那么整个计算过程最终的结果也将错误。

2. Java虚拟机栈

  • 每一个方法在执行的同时,都会创建出一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口、线程等信息。
  • 方法从调用到执行完成,都对应着栈帧从虚拟机中入栈和出栈的过程。
  • 最终,栈帧会随着方法的创建到结束而销毁。

可能这么只从定义看上去仍然没有什么感觉,我们再找一个例子。

这是一个关于斐波那契数列(Fibonacci sequence)求值的例子,我们通过斐波那契数列在虚拟机中的执行过程,来体会Java虚拟机栈的用途。

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

图 25-3 斐波那契数列在虚拟机栈中的执行过程

  • 整个这段流程,就是方法的调用和返回。在调用过程申请了操作数栈的深度和局部变量的大小。
  • 以及相应的信息从各个区域获取并操作,其实也就是入栈和出栈的过程。

3. 本地方法栈

  • 本地方法栈与Java虚拟机栈作用类似,唯一不同的就是本地方法栈执行的是Native方法,而虚拟机栈是为JVM执行Java方法服务的。
  • 另外,与 Java 虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
  • JDK1.8 HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。

关于本地方法栈在以上的例子已经涉及了这部分内容,这里就不在赘述了。

4. 堆和元空间

图 25-4 Java 堆区域划分

  • JDK 1.8 JVM 的内存结构主要由三大块组成:堆内存、元空间和栈,Java 堆是内存空间占据最大的一块区域。
  • Java 堆,由年轻代和年老代组成,分别占据1/3和2/3。
  • 而年轻代又分为三部分,EdenFrom SurvivorTo Survivor,占据比例为8:1:1,可调。
  • 另外这里我们特意画出了元空间,也就是直接内存区域。在 JDK 1.8 之后就不在堆上分配方法区了。
  • 元空间从虚拟机Java堆中转移到本地内存,默认情况下,元空间的大小仅受本地内存的限制,说白了也就是以后不会因为永久代空间不够而抛出OOM异常出现了。jdk1.8以前版本的 class和JAR包数据存储在 PermGen下面 ,PermGen 大小是固定的,而且项目之间无法共用,公有的 class,所以比较容易出现OOM异常。
  • 升级 JDK 1.8后,元空间配置参数,-XX:MetaspaceSize=512M XX:MaxMetaspaceSize=1024M。教你个小技巧通过jps、jinfo查看元空间,如下:

    • 通过命令查看元空间
    • 通过jinfo查看默认MetaspaceSize大小(约20M),MaxMetaspaceSize比较大。

其他:关于 JDK1.8 元空间的介绍: Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory. http://openjdk.java.net/jeps/122

5. 常量池

  • 从 JDK 1.7开始把常量池从永久代中剥离,直到 JDK1.8 去掉了永久代。而字符串常量池一直放在堆空间,用于存储字符串对象,或是字符串对象的引用。

五、手撸虚拟机(内存模型)

其实以上的内容,已经完整的介绍了JVM虚拟机的内存模型,也就是运行时数据区的结构。但是这东西看完可能就忘记了,因为缺少一个可亲手操作的代码。

所以,这里我给大家用Java代码写一段关于数据槽、栈帧、局部变量、虚拟机栈以及堆的代码结构,让大家更好的加深对虚拟机内存模型的印象。

1. 工程结构

运行时数据区
├── heap
│   ├── constantpool
│   ├── methodarea
│   │   ├── Class.java
│   │   ├── ClassMember.java
│   │   ├── Field.java
│   │   ├── Method.java
│   │   ├── MethodDescriptor.java
│   │   ├── MethodDescriptorParser.java
│   │   ├── MethodLookup.java
│   │   ├── Object.java
│   │   ├── Slots.java
│   │   └── StringPool.java
│   └── ClassLoader.java
├── Frame.java
├── JvmStack.java
├── LocalVars.java
├── OperandStack.java
├── Slot.java
└── Thread.java

以上这部分就是使用Java实现的部分JVM虚拟机功能,这部分主要包括如下内容:

  • Frame,栈帧
  • JvmStack,虚拟机栈
  • LocalVars,局部变量
  • OperandStack,操作数栈
  • Slot,数据槽
  • Thread,线程
  • heap,堆,里面包括常量池和方法区

2. 重点代码

操作数栈 OperandStack

public class OperandStack {

    private int size = 0;
    private Slot[] slots;

    public OperandStack(int maxStack) {
        if (maxStack > 0) {
            slots = new Slot[maxStack];
            for (int i = 0; i < maxStack; i++) {
                slots[i] = new Slot();
            }
        }
    }
    //...
}

虚拟机栈 OperandStack

public class JvmStack {

    private int maxSize;
    private int size;
    private Frame _top;
    
    //...
}

栈帧 Frame

public class Frame {

    //stack is implemented as linked list
    Frame lower;

    //局部变量表
    private LocalVars localVars;

    //操作数栈
    private OperandStack operandStack;

    private Thread thread;

    private Method method;

    private int nextPC;
 
    //...
}
  • 关于代码结构看到这有点感觉了吗?
  • Slot数据槽,就是一个数组结构,用于存放数据的。
  • 操作数栈、局部变量表,都是使用数据槽进行入栈入栈操作。
  • 在栈帧里,可以看到连接、局部变量表、操作数栈、方法、线程等,那么文中说到的当有一个新的每一个方法在执行的同时,都会创建出一个栈帧,是不就对了上,可以真的理解了。
  • 如果你对JVM的实现感兴趣,可以阅读用Java实现JVM源码https://github.com/fuzhengwei/itstack-demo-jvm

六、jconsole监测元空间溢出

不是说 JDK 1.8 的内存模型把永久代下掉,换上元空间了吗?但不测试下,就感受不到呀,没有证据!

所有关于代码逻辑的学习,都需要有数据基础和证明过程,这样才能有深刻的印象。走着,带你把元空间干满,让它OOM!

1. 找段持续创建大对象的代码

public static void main(String[] args) throws InterruptedException {
    
    Thread.sleep(5000);
    
    ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
    while (true) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(MetaSpaceOomMock.class);
        enhancer.setCallbackTypes(new Class[]{Dispatcher.class, MethodInterceptor.class});
        enhancer.setCallbackFilter(new CallbackFilter() {
            @Override
            public int accept(Method method) {
                return 1;
            }
            @Override
            public boolean equals(Object obj) {
                return super.equals(obj);
            }
        });
        System.out.println(enhancer.createClass().getName() + loadingBean.getTotalLoadedClassCount() + loadingBean.getLoadedClassCount() + loadingBean.getUnloadedClassCount());
    }
}
  • 网上找了一段基于CGLIB的,你可以写一些其他的。
  • Thread.sleep(5000);,睡一会,方便我们点检测,要不程序太快就异常了。

2. 调整元空间大小

默认情况下元空间太大了,不方便测试出结果,所以我们把它调的小一点。

-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=80m

3. 设置监控参数

基于 jconsole 监控,我们需要设置下参数。

-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=7397
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

4. 测试运行

4.1 配置参数

以上的测试参数,配置到IDEA中运行程序里就可以,如下:

图 25-5 设置程序运行参数,监控OOM

另外,jconsole 可以通过 IDEA 提供的 Terminal 启动,直接输入 jconsole,回车即可。

4.2 测试结果

org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$bd2bb16e999099900
org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$9c774e64999199910
org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$cac97732999299920
org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$91c6a15a999399930
Exception in thread "main" java.lang.IllegalStateException: Unable to load cache item
    at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:79)
    at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:119)
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
    at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
    at net.sf.cglib.proxy.Enhancer.createClass(Enhancer.java:337)
    at org.itstack.interview.MetaSpaceOomMock.main(MetaSpaceOomMock.java:34)
Caused by: java.lang.OutOfMemoryError: Metaspace
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:467)
    at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
    at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:96)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:94)
    at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
    at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
    at java.util.concurrent.FutureTask.run(FutureTask.java)
    at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
    ... 6 more
  • 要的就是这句,java.lang.OutOfMemoryError: Metaspace,元空间OOM,证明 JDK1.8 已经去掉永久代,换位元空间。

4.3 监控截图

图 25-6 jconsole监测元空间溢出

  • 图 25-6,就是监测程序OOM时的元空间表现。这回对这个元空间就有感觉了吧!

七、总结

  • 本文从 JDK 各个版本关于内存模型结构的演变,来了解各个区域,包括:程序计数器、Java 虚拟机栈、本地方法栈、堆和元空间。并了解从 JDK 1.8 开始去掉方法区引入元空间的核心目的和作用。
  • 在通过手撸JVM代码的方式让大家对运行时数据区有一个整体的认知,也通过这样的方式让大家对学习这部分知识有一个抓手。
  • 最后我们通过 jconsole 检测元空间溢出的整个过程,来学以致用,看看元空间到底在解决什么问题以及怎么测试。

八、系列推荐

查看原文

赞 2 收藏 1 评论 0

小傅哥 发布了文章 · 1月4日

谁说明天上线,这货压根不知道开发流程!


作者:小傅哥

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

互联网公司常见工种有哪些?

互联网中一个项目的上线会需要各个工种间的配合,以研发为视角上会承接产品需求,下会交给测试验证,最终完成项目交付上线。其实除此之外,还会有业务、运营、UI设计、运维,来配合项目的发起、使用和运维维护。

图 18-1,互联网工种协同合作。

图 18-1 互联网工种协同合作

除了一条线上的工作交替配合,还有同工种间的跨部门协同工作。 比如:

  • 产品阶段:A产品中的部分服务,需要由另外一个部门配合开发相关服务支撑。那么双方产品需要协调好时间节奏,配合上线。
  • 研发阶段:承接着产品跨部门的对接功能,双方研发会定义好对接接口、对接时间,以及最终的联调上线。
  • 测试阶段:按照产品的功能节点、研发的开发流程以及接口描述,进行测试验证。

最终,同部门工作的交替、跨部门的工作协同,保障项目开发过程所需的各项物料都如期上线。

接下来我们来说一说,项目上线中各个阶段的执行过程。 当然,并不一定所有的开发都是按照这个过程执行。​会根据公司的体量、项目的大小、架构的模式有些许差异。所以,仅作为参考学习即可,不需要强制趋同。

二、时间节奏

图 18-2 定义时间节奏

  • 级别:⭐⭐⭐⭐
  • 事项:定义项目开发时间节点
  • 人员:业务、产品、研发组长、测试组长、架构师、核心项目成员
  • 描述:这个时间节奏的定义非常重要,它可以是项目经理发起也可以是产品发起。一般很多时候互联公司发一个项目,经常会听到老板说我要这个时间上。可能这句话看上去很不合理,但为了活下去,为了快速站住市场,压到下面执行人员就是一个必须要上线的时间。,这个上线的时间如果想满足, 那么就需要把整体的时间节奏确认出来。比如业务和产品什么时候把需求确认清楚什么时间与研发过PRD研发什么时候开发到提测测试什么时间测试完成如果,没有这个时间节奏,前面的职责人员把时间都耗费没了以后,越往后面风险越高。就像最后研发只有4天,测试只有2天,那带BUG上线吗!?所以要整体把控才是对项目的负责。

三、资源投入

图 18-3 安排资源投入

  • 级别:⭐⭐⭐
  • 事项:研发资源投入
  • 人员:架构师、研发人员、测试人员
  • 描述:站在研发视角,研发需要从工程开发、配合测试(改bug)、项目上线等的全流程参与,是一个较长周期的工作。但在某个阶段所投入的时间成本会有差异,可以按照一定的资源占比进行投入(1是100%、0.8是80%)。那么,当一个新的项目下来以后,就需要按照最近原则和项目的人员可投入情况,进行资源投入安排。如果项目较多的情况下,资源安排不合理。可能会导致项目提交测试晚或某些功能全部由一个研发提交测试的,最终改不过来BUG。从而也就导致了,项目的延期风险。

四、研发、测试、上线阶段

图 18-4 研发、测试、上线阶段

  • 级别:⭐⭐⭐⭐
  • 事项:研发、测试、上线阶段
  • 人员:研发人员、测试人员、架构师/技术组长
  • 描述:这个阶段包括的内容较多,主要是以研发视角看上下衔接人员。研发接过产品的需求开始做设计,设计完成后由研发主导发起设计评审,这个阶段参与的人员较多(研发、架构师、测试、产品等)。功能的合理设计也是可以非常有效的保障资源使用的重要一环,另外一个需求的合理架构将会为后续需求迭代做好铺垫。就像女厕改男厕,如果没有流出小便的水管,就很麻烦。 最终研发完成需要提交相应的成果物,尤其是提测报告、接口文档、单测信息。如果研发不能有完整的单元测试覆盖度,那么交给测试以后,日常的修复bug的事情就会非常多。当研发和测试工作完成以后,接下来就是发布上线。上线前夕会有研发发起上线报告,同时各方配合以及产品、运用准备相应的线上配置数据和权限。最终上线完成交付产品运营使用。

五、项目复盘

图 18-5 项目复盘

  • 级别:⭐⭐⭐⭐
  • 事项:项目复盘
  • 人员:面向研发和测试人员
  • 描述:复盘可能会因为出现事故、技术总结、分享成长,几个方向而进行的归纳、总结,避免同类事情的发生。复盘内容一般会包括技术方面的使用,例如:DB、应用开发、网关等,也包括业务领域逻辑的建设。
  • 复盘DB:

    1. 数据库连接数配置依照业务场景申请增加
  1. 禁止使用复杂嵌套和函数类等做业务查询
  2. 防重逻辑字段加强避免造成不能防重问题
  3. 索引字段初始化检测以及慢查询问题优化
  • 复盘业务:

    1. 对于所有营销类场景的设计需符合标准流程,缓存使用的一致性问题
    1. 资金流水结算方面在防重复设计上加强验证,测试环境模拟多样场景
    2. 对于外部支撑系统的依赖按照业务体量发展,进行通知压测报告流量
    3. 所有核心功能流程加强研发侧代码评审质量,并不断按照发展量优化
    4. 研发侧代码质量提升定期复盘问题以及优化,通过锻炼不断加强质量
    5. 在研发提测、修复、上线流程注意开发分支,避免错乱合并产生问题
    6. 所有的业务流程配置监控与图表并打印日志,方便及时追踪线上异常
    7. 核心场景的全链路压测可以有效的保证质量,也可很好降低流量风险
  • 复盘功能:

    1. 功能逻辑封装优化,缓存、线程、验证
    2. 日志完整性校验,入参、出参、异常
    3. 调用外部接口的超时时长设定以及重试约定
    4. 异常展示的紧急问题,测试环境复现追溯
  • 复盘部署:

    1. 按照压测标准部署服务
    2. 核心业务双机房三机房
    3. 非核心业务隔离RPC接口配置
    4. 按需调整JVM、连接数、日志等参数
  • 复盘接口:

    1. 功能验证的完整性
    2. 异常流程的复测性
    3. 数据指标监控范围
    4. 新上线后定期检测
  • 综上,可能仅仅是对某一次项目的总结性复盘,便于新人接受和理解项目的重点内容。如果团队中能及时有效的汇总技术并落地资料,可以非常有效的做好技术传承。

    六、总结

    • 互联网中一般中大型项目的开发过程,涉及的流程一般较多,也需要合理的把控。否则可能会出现一些过程中的风险,导致项目不能如期上线。当然也并不是所有项目都需要这样处理,例如一些小功能的迭代和简单需求的开发,可以简化流程,快速迭代。盖茅坑、猪圈、三居室还是不同的,不能一概而论
    • 做好技术分析、复盘、总结、归纳,沉淀出的技术资料非常有价值,既可以把项目开发经验传承给新人,也可以让所有人做好各自的技术成长。并且通过复盘和总结,又可以提炼出更多新的思路和提升技术氛围。
    • 好了,本章就总结到这,可能对具体的你或者具体的公司,会有不同的视角和结果。如果有一些好的点可以互相讨论学习。另外最近学会了个新东西分享给大家:内卷的反义词是:外包,合同的反义词是:离异!

    七、系列推荐


    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    查看原文

    赞 31 收藏 19 评论 1

    小傅哥 发布了文章 · 2020-12-31

    为了搞清楚类加载,竟然手撸JVM!


    作者:小傅哥
    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    学习,不知道从哪下手?

    当学习一个新知识不知道从哪下手的时候,最有效的办法是梳理这个知识结构的脉络信息,汇总出一整张的思维导出。接下来就是按照思维导图的知识结构,一个个学习相应的知识点,并汇总记录。

    就像JVM的学习,可以说它包括了非常多的内容,也是一个庞大的知识体系。例如:类加载加载器生命周期性能优化调优参数调优工具优化方案内存区域虚拟机栈直接内存内存溢出元空间垃圾回收可达性分析标记清除回收过程等等。如果没有梳理的一头扎进去,东一榔头西一棒子,很容易造成学习恐惧感。

    如图 24-1 是 JVM 知识框架梳理,后续我们会按照这个结构陆续讲解每一块内容。

    图 24-1 JVM 知识框架

    二、面试题

    谢飞机,小记!,很多知识根本就是背背背,也没法操作,难学!

    谢飞机:大哥,你问我两个JVM问题,我看看我自己还行不!

    面试官:啊?嗯!往死了问还是?

    谢飞机:就就就,都行!你看着来!

    面试官:啊,那 JVM 加载过程都是什么步骤?

    谢飞机:巴拉巴拉,加载、验证、准备、解析、初始化、使用、卸载!

    面试官:嗯,背的挺好!我怀疑你没操作过! 那加载的时候,JVM 规范规定从第几位开始是解析常量池,以及数据类型是如何定义的,u1、u2、u4,是怎么个玩意?

    谢飞机:握草!算了,告诉我看啥吧!

    三、类加载过程描述

    图 24-2 JVM 类加载过程

    JVM 类加载过程分为加载链接初始化使用卸载这四个阶段,在链接中又包括:验证准备解析

    • 加载:Java 虚拟机规范对 class 文件格式进行了严格的规则,但对于从哪里加载 class 文件,却非常自由。Java 虚拟机实现可以从文件系统读取、从JAR(或ZIP)压缩包中提取 class 文件。除此之外也可以通过网络下载、数据库加载,甚至是运行时直接生成的 class 文件。
    • 链接:包括了三个阶段;

      • 验证,确保被加载类的正确性,验证字节流是否符合 class 文件规范,例魔数 0xCAFEBABE,以及版本号等。
      • 准备,为类的静态变量分配内存并设置变量初始值等
      • 解析,解析包括解析出常量池数据和属性表信息,这里会包括 ConstantPool 结构体以及 AttributeInfo 接口等。
    • 初始化:类加载完成的最后一步就是初始化,目的就是为标记常量值的字段赋值,以及执行 <clinit> 方法的过程。JVM虚拟机通过锁的方式确保 clinit 仅被执行一次
    • 使用:程序代码执行使用阶段。
    • 卸载:程序代码退出、异常、结束等。

    四、写个代码加载下

    JVM 之所以不好掌握,主要是因为不好实操。虚拟机是 C++ 写的,很多 Java 程序员根本就不会去读,或者读不懂。那么,也就没办法实实在在的体会到,到底是怎么加载的,加载的时候都干了啥。只有看到代码,我才觉得自己学会了!

    所以,我们这里要手动写一下,JVM 虚拟机的部分代码,也就是类加载的过程。通过 Java 代码来实现 Java 虚拟机的部分功能,让开发 Java 代码的程序员更容易理解虚拟机的执行过程。

    1. 案例工程

    interview-24
    ├── pom.xml
    └── src
        └── main
        │    └── java
        │        └── org.itstack.interview.jvm
        │             ├── classpath
        │             │   ├── impl
        │             │   │   ├── CompositeEntry.java
        │             │   │   ├── DirEntry.java 
        │             │   │   ├── WildcardEntry.java 
        │             │   │   └── ZipEntry.java    
        │             │   ├── Classpath.java
        │             │   └── Entry.java    
        │             ├── Cmd.java
        │             └── Main.java
        └── test
             └── java
                 └── org.itstack.interview.jvm.test
                     └── HelloWorld.java

    以上,工程结构就是按照 JVM 虚拟机规范,使用 Java 代码实现 JVM 中加载 class 文件部分内容。当然这部分还不包括解析,因为解析部分的代码非常庞大,我们先从把 .class 文件加载读取开始了解。

    2. 代码讲解

    2.1 定义类路径接口(Entry)

    public interface Entry {
    
        byte[] readClass(String className) throws IOException;
        
        static Entry create(String path) {
            //File.pathSeparator;路径分隔符(win\linux)
            if (path.contains(File.pathSeparator)) {
                return new CompositeEntry(path);
            }
            if (path.endsWith("*")) {
                return new WildcardEntry(path);
            }
            if (path.endsWith(".jar") || path.endsWith(".JAR") ||
                    path.endsWith(".zip") || path.endsWith(".ZIP")) {
                return new ZipEntry(path);
            }
            return new DirEntry(path);
        }
    }
    • 接口中提供了接口方法 readClass 和静态方法 create(String path)
    • jdk1.8 是可以在接口中编写静态方法的,在设计上属于补全了抽象类的类似功能。这个静态方法主要是按照不同的路径地址类型,提供不同的解析方法。包括:CompositeEntry、WildcardEntry、ZipEntry、DirEntry,这四种。接下来分别看每一种的具体实现

    2.2 目录形式路径(DirEntry)

    public class DirEntry implements Entry {
    
        private Path absolutePath;
    
        public DirEntry(String path){
            //获取绝对路径
            this.absolutePath = Paths.get(path).toAbsolutePath();
        }
    
        @Override
        public byte[] readClass(String className) throws IOException {
            return Files.readAllBytes(absolutePath.resolve(className));
        }
    
        @Override
        public String toString() {
            return this.absolutePath.toString();
        }
    }
    • 目录形式的通过读取绝对路径下的文件,通过 Files.readAllBytes 方式获取字节码。

    2.3 压缩包形式路径(ZipEntry)

    public class ZipEntry implements Entry {
    
        private Path absolutePath;
    
        public ZipEntry(String path) {
            //获取绝对路径
            this.absolutePath = Paths.get(path).toAbsolutePath();
        }
    
        @Override
        public byte[] readClass(String className) throws IOException {
            try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
                return Files.readAllBytes(zipFs.getPath(className));
            }
        }
    
        @Override
        public String toString() {
            return this.absolutePath.toString();
        }
    
    }
    • 其实压缩包形式与目录形式,只有在文件读取上有包装差别而已。FileSystems.newFileSystem

    2.4 混合形式路径(CompositeEntry)

    public class CompositeEntry implements Entry {
    
        private final List<Entry> entryList = new ArrayList<>();
    
        public CompositeEntry(String pathList) {
            String[] paths = pathList.split(File.pathSeparator);
            for (String path : paths) {
                entryList.add(Entry.create(path));
            }
        }
    
        @Override
        public byte[] readClass(String className) throws IOException {
            for (Entry entry : entryList) {
                try {
                    return entry.readClass(className);
                } catch (Exception ignored) {
                    //ignored
                }
            }
            throw new IOException("class not found " + className);
        }
    
    
        @Override
        public String toString() {
            String[] strs = new String[entryList.size()];
            for (int i = 0; i < entryList.size(); i++) {
                strs[i] = entryList.get(i).toString();
            }
            return String.join(File.pathSeparator, strs);
        }
        
    }
    • File.pathSeparator,是一个分隔符属性,win/linux 有不同的类型,所以使用这个方法进行分割路径。
    • 分割后的路径装到 List 集合中,这个过程属于拆分路径。

    2.5 通配符类型路径(WildcardEntry)

    public class WildcardEntry extends CompositeEntry {
    
        public WildcardEntry(String path) {
            super(toPathList(path));
        }
    
        private static String toPathList(String wildcardPath) {
            String baseDir = wildcardPath.replace("*", ""); // remove *
            try {
                return Files.walk(Paths.get(baseDir))
                        .filter(Files::isRegularFile)
                        .map(Path::toString)
                        .filter(p -> p.endsWith(".jar") || p.endsWith(".JAR"))
                        .collect(Collectors.joining(File.pathSeparator));
            } catch (IOException e) {
                return "";
            }
        }
    
    }
    • 这个类属于混合形式路径处理类的子类,唯一提供的方法就是把类路径解析出来。

    2.6 类路径解析(Classpath)

    启动类路径扩展类路径用户类路径,熟悉吗?是不经常看到这几句话,那么时候怎么实现的呢?

    有了上面我们做的一些基础类的工作,接下来就是类解析的实际调用过程。代码如下:

    public class Classpath {
    
        private Entry bootstrapClasspath;  //启动类路径
        private Entry extensionClasspath;  //扩展类路径
        private Entry userClasspath;       //用户类路径
    
        public Classpath(String jreOption, String cpOption) {
            //启动类&扩展类 "C:\Program Files\Java\jdk1.8.0_161\jre"
            bootstrapAndExtensionClasspath(jreOption);
            //用户类 F:\..\org\itstack\demo\test\HelloWorld
            parseUserClasspath(cpOption);
        }
    
        private void bootstrapAndExtensionClasspath(String jreOption) {
            
            String jreDir = getJreDir(jreOption);
    
            //..jre/lib/*
            String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
            bootstrapClasspath = new WildcardEntry(jreLibPath);
    
            //..jre/lib/ext/*
            String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
            extensionClasspath = new WildcardEntry(jreExtPath);
    
        }
    
        private static String getJreDir(String jreOption) {
            if (jreOption != null && Files.exists(Paths.get(jreOption))) {
                return jreOption;
            }
            if (Files.exists(Paths.get("./jre"))) {
                return "./jre";
            }
            String jh = System.getenv("JAVA_HOME");
            if (jh != null) {
                return Paths.get(jh, "jre").toString();
            }
            throw new RuntimeException("Can not find JRE folder!");
        }
    
        private void parseUserClasspath(String cpOption) {
            if (cpOption == null) {
                cpOption = ".";
            }
            userClasspath = Entry.create(cpOption);
        }
    
        public byte[] readClass(String className) throws Exception {
            className = className + ".class";
    
            //[readClass]启动类路径
            try {
                return bootstrapClasspath.readClass(className);
            } catch (Exception ignored) {
                //ignored
            }
    
            //[readClass]扩展类路径
            try {
                return extensionClasspath.readClass(className);
            } catch (Exception ignored) {
                //ignored
            }
    
            //[readClass]用户类路径
            return userClasspath.readClass(className);
        }
    
    }
    • 启动类路径,bootstrapClasspath.readClass(className);
    • 扩展类路径,extensionClasspath.readClass(className);
    • 用户类路径,userClasspath.readClass(className);
    • 这回就看到它们具体在哪使用了吧!有了具体的代码也就方便理解了

    2.7 加载类测试验证

    private static void startJVM(Cmd cmd) {
        Classpath cp = new Classpath(cmd.jre, cmd.classpath);
        System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
        //获取className
        String className = cmd.getMainClass().replace(".", "/");
        try {
            byte[] classData = cp.readClass(className);
            System.out.println(Arrays.toString(classData));
        } catch (Exception e) {
            System.out.println("Could not find or load main class " + cmd.getMainClass());
            e.printStackTrace();
        }
    }

    这段就是使用 Classpath 类进行类路径加载,这里我们测试加载 java.lang.String 类。你可以加载其他的类,或者自己写的类

    • 配置IDEA,program arguments 参数:-Xjre "C:\Program Files\Java\jdk1.8.0_161\jre" java.lang.String
    • 另外这里读取出的 class 文件信息,打印的是 byte 类型信息。

    测试结果

    [-54, -2, -70, -66, 0, 0, 0, 52, 2, 28, 3, 0, 0, -40, 0, 3, 0, 0, -37, -1, 3, 0, 0, -33, -1, 3, 0, 1, 0, 0, 8, 0, 15, 8, 0, 61, 8, 0, 85, 8, 0, 88, 8, 0, 89, 8, 0, 112, 8, 0, -81, 8, 0, -75, 8, 0, -47, 8, 0, -45, 1, 0, 0, 1, 0, 3, 40, 41, 73, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 3, 40, 41, 86, 1, 0, 3, 40, 41, 90, 1, 0, 4, 40, 41, 91, ...]

    这块部分截取的程序运行打印结果,就是读取的 class 文件信息,只不过暂时还不能看出什么。接下来我们再把它翻译过来!

    五、解析字节码文件

    JVM 在把 class 文件加载完成后,接下来就进入链接的过程,这个过程包括了内容的校验、准备和解析,其实就是把 byte 类型 class 翻译过来,做相应的操作。

    整个这个过程内容相对较多,这里只做部分逻辑的实现和讲解。如果读者感兴趣可以阅读小傅哥的《用Java实现JVM》专栏。

    1. 提取部分字节码

    //取部分字节码:java.lang.String
    private static byte[] classData = {
            -54, -2, -70, -66, 0, 0, 0, 52, 2, 26, 3, 0, 0, -40, 0, 3, 0, 0, -37, -1, 3, 0, 0, -33, -1, 3, 0, 1, 0, 0, 8, 0,
            59, 8, 0, 83, 8, 0, 86, 8, 0, 87, 8, 0, 110, 8, 0, -83, 8, 0, -77, 8, 0, -49, 8, 0, -47, 1, 0, 3, 40, 41, 73, 1,
            0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 1, 0, 20, 40, 41,
            76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 3, 40, 41, 86, 1, 0, 3,
            40, 41, 90, 1, 0, 4, 40, 41, 91, 66, 1, 0, 4, 40, 41, 91, 67, 1, 0, 4, 40, 67, 41, 67, 1, 0, 21, 40, 68, 41, 76,
            106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 40, 73, 41, 67, 1, 0, 4};
    • java.lang.String 解析出来的字节码内容较多,当然包括的内容也多,比如魔数、版本、类、常量、方法等等。所以我们这里只截取部分进行进行解析。

    2. 解析魔数并校验

    很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,这几个字节主要起到标识作用,叫作魔数(magic number)。

    例如;

    • PDF文件以4字节“%PDF”(0x25、0x50、0x44、0x46)开头,
    • ZIP文件以2字节“PK”(0x50、0x4B)开头
    • class文件以4字节“0xCAFEBABE”开头
    private static void readAndCheckMagic() {
        System.out.println("\r\n------------ 校验魔数 ------------");
        //从class字节码中读取前四位
        byte[] magic_byte = new byte[4];
        System.arraycopy(classData, 0, magic_byte, 0, 4);
        
        //将4位byte字节转成16进制字符串
        String magic_hex_str = new BigInteger(1, magic_byte).toString(16);
        System.out.println("magic_hex_str:" + magic_hex_str);
        
        //byte_magic_str 是16进制的字符串,cafebabe,因为java中没有无符号整型,所以如果想要无符号只能放到更高位中
        long magic_unsigned_int32 = Long.parseLong(magic_hex_str, 16);
        System.out.println("magic_unsigned_int32:" + magic_unsigned_int32);
        
        //魔数比对,一种通过字符串比对,另外一种使用假设的无符号16进制比较。如果使用无符号比较需要将0xCAFEBABE & 0x0FFFFFFFFL与运算
        System.out.println("0xCAFEBABE & 0x0FFFFFFFFL:" + (0xCAFEBABE & 0x0FFFFFFFFL));
        
        if (magic_unsigned_int32 == (0xCAFEBABE & 0x0FFFFFFFFL)) {
            System.out.println("class字节码魔数无符号16进制数值一致校验通过");
        } else {
            System.out.println("class字节码魔数无符号16进制数值一致校验拒绝");
        }
    }
    • 读取字节码中的前四位,-54, -2, -70, -66,将这四位转换为16进制。
    • 因为 java 中是没有无符号整型的,所以只能用更高位存放。
    • 解析后就是魔数的对比,看是否与 CAFEBABE 一致。

    测试结果

    ------------ 校验魔数 ------------
    magic_hex_str:cafebabe
    magic_unsigned_int32:3405691582
    0xCAFEBABE & 0x0FFFFFFFFL:3405691582
    class字节码魔数无符号16进制数值一致校验通过

    3. 解析版本号信息

    刚才我们已经读取了4位魔数信息,接下来再读取2位,是版本信息。

    魔数之后是class文件的次版本号和主版本号,都是u2类型。假设某class文件的主版本号是M,次版本号是m,那么完整的版本号可以表示成“M.m”的形式。次版本号只在J2SE 1.2之前用过,从1.2开始基本上就没有什么用了(都是0)。主版本号在J2SE 1.2之前是45,从1.2开始,每次有大版本的Java版本发布,都会加1{45、46、47、48、49、50、51、52}

    private static void readAndCheckVersion() {
        System.out.println("\r\n------------ 校验版本号 ------------");
        //从class字节码第4位开始读取,读取2位
        byte[] minor_byte = new byte[2];
        System.arraycopy(classData, 4, minor_byte, 0, 2);
        
        //将2位byte字节转成16进制字符串
        String minor_hex_str = new BigInteger(1, minor_byte).toString(16);
        System.out.println("minor_hex_str:" + minor_hex_str);
        
        //minor_unsigned_int32 转成无符号16进制
        int minor_unsigned_int32 = Integer.parseInt(minor_hex_str, 16);
        System.out.println("minor_unsigned_int32:" + minor_unsigned_int32);
        
        //从class字节码第6位开始读取,读取2位
        byte[] major_byte = new byte[2];
        System.arraycopy(classData, 6, major_byte, 0, 2);
        
        //将2位byte字节转成16进制字符串
        String major_hex_str = new BigInteger(1, major_byte).toString(16);
        System.out.println("major_hex_str:" + major_hex_str);
        
        //major_unsigned_int32 转成无符号16进制
        int major_unsigned_int32 = Integer.parseInt(major_hex_str, 16);
        System.out.println("major_unsigned_int32:" + major_unsigned_int32);
        System.out.println("版本号:" + major_unsigned_int32 + "." + minor_unsigned_int32);
    }
    • 这里有一个小技巧,class 文件解析出来是一整片的内容,JVM 需要按照虚拟机规范,一段一段的解析出所有的信息。
    • 同样这里我们需要把2位byte转换为16进制信息,并继续从第6位继续读取2位信息。组合出来的才是版本信息。

    测试结果

    ------------ 校验版本号 ------------
    minor_hex_str:0
    minor_unsigned_int32:0
    major_hex_str:34
    major_unsigned_int32:52
    版本号:52.0

    4. 解析全部内容对照

    按照 JVM 的加载过程,其实远不止魔数和版本号信息,还有很多其他内容,这里我们可以把测试结果展示出来,方便大家有一个学习结果的比对印象。

    classpath:org.itstack.demo.jvm.classpath.Classpath@4bf558aa class:java.lang.String args:null
    version: 52.0
    constants count:540
    access flags:0x31
    this class:java/lang/String
    super class:java/lang/Object
    interfaces:[java/io/Serializable, java/lang/Comparable, java/lang/CharSequence]
    fields count:5
    value          [C
    hash          I
    serialVersionUID          J
    serialPersistentFields          [Ljava/io/ObjectStreamField;
    CASE_INSENSITIVE_ORDER          Ljava/util/Comparator;
    methods count: 94
    <init>          ()V
    <init>          (Ljava/lang/String;)V
    <init>          ([C)V
    <init>          ([CII)V
    <init>          ([III)V
    <init>          ([BIII)V
    <init>          ([BI)V
    checkBounds          ([BII)V
    <init>          ([BIILjava/lang/String;)V
    <init>          ([BIILjava/nio/charset/Charset;)V
    <init>          ([BLjava/lang/String;)V
    <init>          ([BLjava/nio/charset/Charset;)V
    <init>          ([BII)V
    <init>          ([B)V
    <init>          (Ljava/lang/StringBuffer;)V
    <init>          (Ljava/lang/StringBuilder;)V
    <init>          ([CZ)V
    length          ()I
    isEmpty          ()Z
    charAt          (I)C
    codePointAt          (I)I
    codePointBefore          (I)I
    codePointCount          (II)I
    offsetByCodePoints          (II)I
    getChars          ([CI)V
    getChars          (II[CI)V
    getBytes          (II[BI)V
    getBytes          (Ljava/lang/String;)[B
    getBytes          (Ljava/nio/charset/Charset;)[B
    getBytes          ()[B
    equals          (Ljava/lang/Object;)Z
    contentEquals          (Ljava/lang/StringBuffer;)Z
    nonSyncContentEquals          (Ljava/lang/AbstractStringBuilder;)Z
    contentEquals          (Ljava/lang/CharSequence;)Z
    equalsIgnoreCase          (Ljava/lang/String;)Z
    compareTo          (Ljava/lang/String;)I
    compareToIgnoreCase          (Ljava/lang/String;)I
    regionMatches          (ILjava/lang/String;II)Z
    regionMatches          (ZILjava/lang/String;II)Z
    startsWith          (Ljava/lang/String;I)Z
    startsWith          (Ljava/lang/String;)Z
    endsWith          (Ljava/lang/String;)Z
    hashCode          ()I
    indexOf          (I)I
    indexOf          (II)I
    indexOfSupplementary          (II)I
    lastIndexOf          (I)I
    lastIndexOf          (II)I
    lastIndexOfSupplementary          (II)I
    indexOf          (Ljava/lang/String;)I
    indexOf          (Ljava/lang/String;I)I
    indexOf          ([CIILjava/lang/String;I)I
    indexOf          ([CII[CIII)I
    lastIndexOf          (Ljava/lang/String;)I
    lastIndexOf          (Ljava/lang/String;I)I
    lastIndexOf          ([CIILjava/lang/String;I)I
    lastIndexOf          ([CII[CIII)I
    substring          (I)Ljava/lang/String;
    substring          (II)Ljava/lang/String;
    subSequence          (II)Ljava/lang/CharSequence;
    concat          (Ljava/lang/String;)Ljava/lang/String;
    replace          (CC)Ljava/lang/String;
    matches          (Ljava/lang/String;)Z
    contains          (Ljava/lang/CharSequence;)Z
    replaceFirst          (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    replaceAll          (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    replace          (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
    split          (Ljava/lang/String;I)[Ljava/lang/String;
    split          (Ljava/lang/String;)[Ljava/lang/String;
    join          (Ljava/lang/CharSequence;[Ljava/lang/CharSequence;)Ljava/lang/String;
    join          (Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;
    toLowerCase          (Ljava/util/Locale;)Ljava/lang/String;
    toLowerCase          ()Ljava/lang/String;
    toUpperCase          (Ljava/util/Locale;)Ljava/lang/String;
    toUpperCase          ()Ljava/lang/String;
    trim          ()Ljava/lang/String;
    toString          ()Ljava/lang/String;
    toCharArray          ()[C
    format          (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
    format          (Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
    valueOf          (Ljava/lang/Object;)Ljava/lang/String;
    valueOf          ([C)Ljava/lang/String;
    valueOf          ([CII)Ljava/lang/String;
    copyValueOf          ([CII)Ljava/lang/String;
    copyValueOf          ([C)Ljava/lang/String;
    valueOf          (Z)Ljava/lang/String;
    valueOf          (C)Ljava/lang/String;
    valueOf          (I)Ljava/lang/String;
    valueOf          (J)Ljava/lang/String;
    valueOf          (F)Ljava/lang/String;
    valueOf          (D)Ljava/lang/String;
    intern          ()Ljava/lang/String;
    compareTo          (Ljava/lang/Object;)I
    <clinit>          ()V
    
    Process finished with exit code 0

    六、总结

    • 学习 JVM 最大的问题是不好实践,所以本文以案例实操的方式,学习 JVM 的加载解析过程。也让更多的对 JVM 感兴趣的研发,能更好的接触到 JVM 并深入的学习。
    • 有了以上这段代码,大家可以参照 JVM 虚拟机规范,在调试Java版本的JVM,这样就可以非常容易理解整个JVM的加载过程,都做了什么。
    • 如果大家需要文章中一些原图 xmind 或者源码,可以添加作者小傅哥(fustack),或者关注公众号:bugstack虫洞栈进行获取。好了,本章节就扯到这,后续还有很多努力,持续原创,感谢大家的支持!

    七、系列推荐

    查看原文

    赞 10 收藏 7 评论 0

    小傅哥 发布了文章 · 2020-12-28

    2020总结 | 作为技术号主的一年!


    作者:小傅哥
    <br/>博客:https://bugstack.cn
    <br/>Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    快到年底了,写个总结吧!

    关注我的粉丝朋友,谢谢你!滴水之恩,永不相忘!
    我没照顾到的伙伴,对不起!我不是有意忽略了你。
    致我相识的每一位同好,所求皆如愿、所行化坦途。
    再见2020,迎接2021!

    二、缘起

    怎么就开始写公众号了呢?

    故事:听过郭德纲的一个相声片段,大概意思是有一盒价值连城的珠宝,运往京城。途中盒子和珠宝聊天,盒子一直觉得自己和珠宝一样值钱,但盒子没有意识到值钱的是珠宝而不是自己。这让我想到了香蕉和香蕉皮,是不是我们自己可能就是那个干饭的香蕉皮而已!

    19年的时候,我差不多工作6年。看到一些部门调整、人员变动、伙伴离职,虽然在互联网这是常态,但也让我也有了一丝不安。因为我感受到即使在职场有着非常不错的团队成绩、成长路线、薪资待遇,但仍然可能会因为某一时刻的某些因素而导致香蕉与香蕉皮分离。

    可能也就是单纯的不想只做个干饭的香蕉皮,从19年5月开始陆续再想怎么把我的空闲时间投资给自己,只有让自己长期有价值,在职场拥有能留下的本事和走出去的能力,才不会担心才35岁的壮年危机!

    19年5月开始,每个周末我曾尝试过:

    • 接私活,失败告终:不靠谱,也接不到。主要是了解后发现,根本没有整块的时间可以投入,也不太可能主力搞这个,所以就放弃了。
    • 合伙开发网站,失败告终:开发网站,主要是以学习为目的,也想着万一可以有流量就更好了。可是这事几乎做不成,没业务规划方向、没产品思维逻辑、没运营运作技巧,只是个写代码很难成事。况且合伙,这个稳定性还差。所有能成事的起点要不你自己干,要不你出钱雇人干。
    • 自己搭个论坛:搭起来容易,搭完运营起来就难了,谁来你这发呢!?好家伙来了一堆广告、卖大力丸、宣传澳门赌场的。最终失败,因为这种靠成型组件搭起来的论坛,不说是否会运营以为。就这套网站你总得要随着时间推移和需求变化不断的更新迭代吧,但根本就没有时间和能力来迭代,这种迭代可不是单纯的改改CRUD,而是整个页面的布局、功能、服务的优化。

    2个月后,这些都失败了!但好在这些失败让我总结出,如果想做点给自己投资的事情,就要做那些与个人能力成长相关的,也需要可以长期投入和产出的事情。

    所以也就是从19年7月开始,算是正式在公众号写技术文章了。一直到20年初写了77篇文章,粉丝1299个。如果是以写文章,运营公众号的角度看,这点粉丝量估计就放弃了。好在初心不改,坚持到现在!

    三、成绩

    让人怪不好意思的,吹个牛!

    2020年,数据增长报告

    2020年,数据增长报告

    1. 公众号:bugstack虫洞栈,粉丝从20年年初1299个增长到年末1.7万
    2. 博客:bugstack.cn全年 PV 31万全年 UV 9万
    3. GitHub:CodeGuide,3.2k Star、itstack-demo-design,2.0k Star
    4. 微信好友:3500,也快5000人了!再加的好友就看不到我的朋友圈了!
    5. PDF书籍:《重学Java设计模式》全网可统计下载量,14万 - 标题,差点给我送字节跳动去!!!
    6. 原创技术文章:总计 172 篇、20年 95篇
    7. CSDN 专家、粉丝1.2万,GitChat 专栏作者

    这些数据并不是个人KPI为导向的结果,只是慢慢坚持原创输出带来的成绩。在这个过程中,原创是我的原则,因为我坚持每一篇文章,我都能真实的学到知识,也能保质保量的交付给粉丝用户。可能这样没有热点标题、也没有互推助力,粉丝增长也不快。但我认为:其实如果你能慢下来,往往就是一种快!

    享受这种创作的乐趣,提升个人的能力,结交交流的同好,还是蛮幸福的!

    四、上车

    滴滴叭叭,你是什么时候成为我的粉丝上车的?

    • 19年5月,编写专栏《用Java实现JVM》
    • 19年7月,编写专栏《基于JavaAgent全链路监控》
    • 19年8月-9月,编写专栏《Netty4.x专题》
    • 19年10月,编写专栏《DDD领域驱动设计》
    • 19年11月,编写专栏《SpringCloud入门教程》
    • 19年12月,源码分析系列和框架搭建实战
    • 20年1月,持续创建源码分析系列
    • 20年2月,编写GitChat付费专栏,《Netty+JavaFx实战:仿桌面版微信聊天》
    • 20年3月,总结职场类文章
    • 20年4月,编写ASM、Javassist、Byte-Buddy,字节码编程系列文章
    • 20年5月-7月,编写专栏《重学Java设计模式》,并推出PDF书籍,全网下载量14万+
    • 20年8月-12月,推出两个大专栏《面经手册 • 拿大厂Offer》、《码场故事》

    以上共计12个专栏,172篇原创文章。滴滴叭叭,那你是什么时候上车关注小傅哥的呢?

    五、方向

    方向也是朝着理想奔跑的地方!

    如果能成,我希望将来35或者40以后,可以这样:

    1. 有自己的事业,倒腾鱼虾也算
    2. 有自己的时间,能9:30点就睡,7点就醒
    3. 有一个大书房,屋里有投影、游戏机、跑步机、台式机、书架、瑜伽垫、音响、大长的电脑桌,对最好再有个单杠,这样就能健康的写代码了
    4. 有自己的计划,比如可以定期和家人开车出去郊游,我喜欢SUV,也喜欢像smart那种小车

    其实很多程序员都有这样的愿望,都想有点自己的时间,做点自己喜欢的事情。谁又希望每天回家只能倒头就睡呢?为了不这样,只有不断的沉淀、积累,将来的某一时刻一定会爆发的!

    2021年,继续沉淀技术,拓展宽度和挖掘深度,所有的学习在运用到工作中,为所有以后的目标做好铺垫。一路上慢慢折腾,慢慢成长。

    六、认识一下?

    小傅哥,一个喜欢写代码的男人。一线互联网架构师、技术号主,擅长钻研技术、编写干净的代码。欢迎加我的微信,与同好交流,互相进步,共同成长!

    GitHub交流https://github.com/fuzhengwei/CodeGuide/wiki

    最后,祝福大家新的一年平安健康、如意吉祥,所求皆如愿、所行化坦途。

    查看原文

    赞 8 收藏 1 评论 0

    小傅哥 发布了文章 · 2020-12-24

    面经手册 · 第23篇《JDK、JRE、JVM,是什么关系?》


    作者:小傅哥
    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    截至到这已经写了22篇面经手册,你看了多少?

    😄其实小傅哥就是借着面经的幌子在讲 Java 核心技术,探索这些核心知识点面试的背后到底在问什么。

    想问一些面试官,是因为大家都在问所以你问,还是你想从这里问出什么? 其实可能很多面试官如果不了解这些技术,往往会被求职者的答案击碎内心,哈哈哈哈哈哈。比如:梅森旋转算法开放寻址斐波那契散列启发式清理Javassist代理方式扰动函数哈希一致等等。

    记住,让懂了就是真的懂,比看水文、背答案要爽的多!嗯,就是有时候烧脑!

    二、面试题

    谢飞机,小记!,也不知道咋了,总感觉有些面试攻击性不大,但侮辱性极强

    面试官:谢飞机写过 Java 吗?

    谢飞机:那当然写过,写了3年多了!

    面试官:那,JDKJREJVM 之间是什么关系?

    谢飞机:嗯 J J J,JDK 里面有 JRE,JVM 好像在 JRE 里!?

    面试官:那,Client模式、Server模式是啥?

    谢飞机:嗯!?啥?

    面试官:好吧,问个简单的。JVM 是如何工作的?背答案了吗?

    谢飞机:再见,面试官!

    三、JDK、JRE、JVM

    1. Java 平台标准(JDK 8)

    Oracle has two products that implement Java Platform Standard Edition (Java SE) 8: Java SE Development Kit (JDK) 8 and Java SE Runtime Environment (JRE) 8.

    JDK 8 is a superset of JRE 8, and contains everything that is in JRE 8, plus tools such as the compilers and debuggers necessary for developing applets and applications. JRE 8 provides the libraries, the Java Virtual Machine (JVM), and other components to run applets and applications written in the Java programming language. Note that the JRE includes components not required by the Java SE specification, including both standard and non-standard Java components.

    The following conceptual diagram illustrates the components of Oracle's Java SE products:

    Description of Java Conceptual Diagram

    Java Platform Standard Edition 8 Documentation

    关于 JDK、JRE、JVM 之间是什么关系,在 Java 平台标准中已经明确定义了。也就是上面的英文介绍部分。

    • Oracle 有两个 Java 平台标准的产品,Java SE 开发工具包(JDK) 和 Java SE 运行时环境(JRE)。
    • JDK(Java Development Kit Java开发工具包),JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。其中的开发工具包括编译工具(javac.exe) 打包工具(jar.exe)等。
    • JRE(Java Runtime Environment Java运行环境) 是 JDK 的子集,也就是包括 JRE 所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了库、Java 虚拟机(JVM)和其他组件,用于运行 Java 编程语言、小程序、应用程序。
    • JVM(Java Virtual Machine Java虚拟机),JVM可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。

    综上,从这段官网的平台标准介绍和概念图可以看出,我们运行程序的 JVM 是已经安装到 JDK 中,只不过可能你开发了很久的代码,也没有注意过。没有注意过的最大原因是,没有开发过一些和 JVM 相关的组件代码

    关于,各 JDK 版本的平台标准,可以自行比对学习,如下:

    2. JDK 目录结构和作用

    我们默认安装完 JDK 会有 jdk1.8.0_45jre1.8.0_45,两个文件夹。其实在 JDK 的文件中还会有 JRE 的文件夹,他们两个 JRE 文件夹的结构是一样的。

    JDK 目录结构

    • bin:一堆 EXE 可执行文件,java.exe、javac.exe、javadoc.exe,已经密钥管理工具等。
    • db:内置了 Derby 数据库,体积小,免安装。
    • include:Java 和 JVM 交互的头文件,例如我们 JVMTI 写的 C++ 工程时,就需要把这个 include 包引入进去jvmti.h例如:基于jvmti设计非入侵监控
    • jre:Java 运行环境,包含了运行时需要的可执行文件,以及运行时需要依赖的 Java 类库和动态链接库.so.dll.dylib
    • lib:Java 类库,例如 dt.jar、tools.jar

    那么 jvm 在哪个文件夹呢?

    jvm.dll

    可能你之前并没有注意过 jvm 原来在这里:C:\Program Files\Java\jdk1.8.0_45\jre\bin\server

    • 这部分是整个 Java 实现跨平台的最核心内容,由 Java 程序编译成的 .class 文件会在虚拟机上执行。
    • 另外在 JVM 解释 class 文件时需要调用类库 lib。在 JRE 目录下有两个文件夹 lib、bin,而 lib 就是 JVM 执行所需要的类库。
    • jvm.dll 并不能独立工作,当 jvm.dll 启动后,会使用 explicit 方法来载入辅助动态链接库一起执行。

    3. JDK 是什么?

    综上通过 Java 平台标准JDK 的目录结构,JDK 是 JRE 的超集,JDK 包含了 JRE 所有的开发、调试以及监视应用程序的工具。以及如下重要的组件:

    • java – 运行工具,运行 .class 的字节码
    • javac– 编译器,将后缀名为.java的源代码编译成后缀名为.class的字节码
    • javap – 反编译程序
    • javadoc – 文档生成器,从源码注释中提取文档,注释需符合规范
    • jar – 打包工具,将相关的类文件打包成一个文件
    • jdb – debugger,调试工具
    • jps – 显示当前java程序运行的进程状态
    • appletviewer – 运行和调试applet程序的工具,不需要使用浏览器
    • javah – 从Java类生成C头文件和C源文件。这些文件提供了连接胶合,使 Java 和 C 代码可进行交互。
    • javaws – 运行 JNLP 程序
    • extcheck – 一个检测jar包冲突的工具
    • apt – 注释处理工具
    • jhat – java堆分析工具
    • jstack – 栈跟踪程序
    • jstat – JVM检测统计工具
    • jstatd – jstat守护进程
    • jinfo – 获取正在运行或崩溃的java程序配置信息
    • jmap – 获取java进程内存映射信息
    • idlj – IDL-to-Java 编译器. 将IDL语言转化为java文件
    • policytool – 一个GUI的策略文件创建和管理工具
    • jrunscript – 命令行脚本运行
    • appletviewer:小程序浏览器,一种执行HTML文件上的Java小程序的Java浏览器

    4. JRE 是什么?

    JRE 本身也是一个运行在 CPU 上的程序,用于解释执行 Java 代码。

    一般像是实施的工作,会在客户现场安装 JRE,因为这是运行 Java 程序的最低要求。

    JRE 目录结构 lib、bin

    • bin:有 java.exe 但没有 javac.exe。也就是无法编译 Java 程序,但可以运行 Java 程序,可以把这个bin目录理解成JVM。
    • lib:Java 基础&核心类库,包含 JVM 运行时需要的类库和 rt.jar。也包含用于安全管理的文件,这些文件包括安全策略(security policy)和安全属性(security properties)等。

    5. JVM 是什么?

    其实简单说 JVM 就是运行 Java 字节码的虚拟机,JVM 是一种规范,各个供应商都可以实现自己 JVM虚拟机。就像小傅哥自己也按照虚拟机规范和手写JVM的相关书籍实现了,基于Java实现的JVM虚拟机。

    用Java实现JVM源码

    源码地址https://github.com/fuzhengwei/itstack-demo-jvm
    内容简介:本代码主要介绍如何通过 java 代码来实现 JVM 的基础功能(搜索解析class文件、字节码命令、运行时数据区等),从而让java程序员通过最熟知的java程序,学习JVM是如何将java程序一步步跑起来的。

    当然,我们下载 Oracle 公司的 JVM 与自己实现的相比,要高级的多。他们的设计有不断优化的内存模型、GC回收策略、自适应优化器等。

    另外,JVM 之所以称为虚拟机,主要就是因为它为了实现 “write-once-run-anywhere”。提供了一个不依赖于底层操作系统和机器硬件结构的运行环境。

    5.1 Client模式、Server模式

    在 JVM 中有两种不同风格的启动模式, Client模式、Server模式。

    • Client模式:加载速度较快。可以用于运行GUI交互程序。
    • Server模式:加载速度较慢但运行起来较快。可以用于运行服务器后台程序。

    修改配置模式文件:C:\\Program Files\\Java\\jre1.8.0_45\\lib\\amd64\\jvm.cfg

    # List of JVMs that can be used as an option to java, javac, etc.
    # Order is important -- first in this list is the default JVM.
    # NOTE that this both this file and its format are UNSUPPORTED and
    # WILL GO AWAY in a future release.
    #
    # You may also select a JVM in an arbitrary location with the
    # "-XXaltjvm=<jvm_dir>" option, but that too is unsupported
    # and may not be available in a future release.
    #
    -server KNOWN
    -client IGNORE
    • 如果需要调整,可以把 client 设置为 KNOWN,并调整到 server 前面。
    • JVM 默认在 Server模式下,-Xms128M、-Xmx1024M
    • JVM 默认在 Client 模式下,-Xms1M、-Xmx64M

    5.2 JVM 结构和执行器

    这部分属于 JVM 的核心知识,但不是本篇重点,会在后续的章节中陆续讲到。本章只做一些介绍。

    • Class Loader:类装载器是用于加载类文件的一个子系统,其主要功能有三个:loading(加载),linking(链接),initialization(初始化)。
    • JVM Memory Areas:方法区、堆区、栈区、程序计数器。
    • Interpreter(解释器):通过查找预定义的 JVM 指令到机器指令映射,JVM 解释器可以将每个字节码指令转换为相应的本地指令。它直接执行字节码,不执行任何优化。
    • JIT Compiler(即时编译器):为了提高效率,JIT Compiler 在运行时与 JVM 交互,并适当将字节码序列编译为本地机器代码。典型地,JIT Compiler执行一段代码,不是每次一条语句。优化这块代码,并将其翻译为优化的机器代码。JIT Compiler是默认开启

    四、总结

    • 这篇的知识并不复杂,涉及的面试内容也较少,更多的是对接下来要讲到 JVM 相关面试内容的一个开篇介绍,为后续的要讲的内容做一个铺垫。
    • 如果你在此之前没有关注过JDK、JRE、JVM的结构和相应的组件配置以及执行模式,那么可以在此基础上继续学习加深印象。另外想深入学习JVM并不太容易,既要学习JVM规范也要上手应用实践,所以很建议先手写JVM,再实践验证JVM。
    • 好了,本章节就扯到这了。这些知识点即使分享给大家,也是我自己学习、收录、整理、验证的过程。互相学习、互相成长,如果有错误之处,直接留言给我,我会不断的改正。大家一起进步!

    五、系列推荐

    查看原文

    赞 4 收藏 3 评论 0

    小傅哥 发布了文章 · 2020-12-21

    工作3年,看啥资料能月薪30K?


    作者:小傅哥 | https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    月薪30K年薪是多少?

    按照月薪30K,年终奖2~3个月来算,再算上季度的绩效奖金、加班费,可能也有一些大小周和节假日的三倍工资等。综合起来的税前年收入整体差不多在46W左右。当然如果你在年会中了个大奖也可以算进去,或者阳光普照个IPhone!

    那30K月薪差不多是一个什么级别?不知道大家有没有看过下面这张图,这个图来自一个薪资统计的网站,如下:

    互联网薪资对标 duibiao.info

    • 以上这种图的收入除了月薪还包括了,奖金、年终奖、股票,有些公司给的股票是比较多的。股票有一定的解禁期,并不是一次能拿完。
    • 那如果想拿月薪30K,基本是拿到了一个阿里的P6以及横向对标的级别。当然可能有些同学是在内部晋升加薪的,那样可能会略有差别。

    30K对于工作3~5年还是蛮香的,但互联网大厂也确实不那么容易进去,如果在传统行业耽误了几年或者头几年做的项目单一,个人技术能力成长缓慢,过了30岁还真的挺难进去的。当然不是说30岁不要,只不过到了30岁,会要求面到更高的级别。

    一般面试会从多方面进行考察,判断求职者是否满足招聘要求,如下图:但也有很牛皮的求职者可能就一两个问题的回答,就已经把面试官镇住了!

    综上,梳理出七个方向的面试考点,包括:基本功底、常用技术、技术深度、技术经验、学习能力、工作能力、项目经验。

    • 基本功底,是一个程序员的主科目语言的学习程度的一个基本考察,这部分内容需要平时大量积累和总结。否则一本简单的Java书很难全部给你讲透彻,因为Java中包括了太多的内容,远不止API使用。
    • 常用技术,聊的是你的技术广度,和岗位技术匹配度。比如需要用到过RPC,那你用过Dubbo。如果你的公司暂时用的技术不多,或者还是处于单体服务,那么需要自己补充。
    • 技术深入,除了技术广度接下来就是技术深入,在你常用的技术栈中,你有多了解他们,了解源码吗、了解运行机制吗、了解设计原理吗。这部分内容常被人说是造火箭,但这部分内容非常重要,可以承上启下的贯穿个人修为和薪资待遇。
    • 技术经验,什么是技术经验呢?这是落地能力,除了你可能认为上面一些是纸上谈兵,是造火箭。那么接下来这部分内容就是你是否真造过一个火箭,真完成过一个难题。所以这部分是从结果证明,不是你会什么,而是你做过什么。
    • 学习能力,作为程序员你是否保持热情,是否依旧在积极努力的关注技术,是否为自己的成长不断添砖加瓦、是否还有好奇心和较强的求知欲。一般会从这里看你是不是一个真正的Coder!
    • 工作能力,以上的种种能力,最终要体现到工作上,要能看出你的交付能力。否则即使你再优秀,也不能把你当成一个吉祥物。工作能力的体现,才是真的为团队、为部门、为公司,贡献价值的。
    • 项目经验,这项内容会根据不同公司的不同业务线而不同,就像你懂交易、支付,那么面试花呗、借呗、白条等工作岗位就会很吃香。

    好! 接下来小傅哥就带着你逐步介绍七个方向中的每一刻具体有哪些内容以及该如何学习。走起!

    二、技术大纲

    1. 基本功底

    图 16-1 基本功底

    • 重要程度:⭐⭐⭐⭐
    • 内容介绍:数据结构讲的就是把数据放在不同形态的结构中,堆栈队列链表数组等。而算法逻辑就是把这些存放在数据结构中的数据按照一定规则进行增删改查,也就是二分、快排、动态规划、搜索等。而一门语言的核心技术就包括了对数据结构和算法的具体实现,像是我们用到的结合框架,ArrayList、HashMap等都是具体的实现。除此之外,在Java的核心技术中还要学习多线程、代理、反射等技术。这不只是面试内容,更是写好代码的基础!
    • 学习资料:算法图解、大话数据结构、数据结构与算法分析、算法导论、算法之美、计算机程序设计艺术
    • 语重心长:学习,从来不只仅仅是为了当下工作需要。简单的CRUD也可能真的不需要复杂的设计,但个人的年龄和能力一直要成正比!

    2. 常用技术

    图 16-2 常用技术

    • 重要程度:⭐⭐⭐⭐
    • 内容介绍:这部分内容是一个互联网研发中常用的技术栈内容,可能每个公司会有一些同类的其他技术,比如RPC框架就有很多种,但技术核心原理基本一致。可能以上的内容看上去比较杂,也可能有一些是你还没有接触过的,可以从上到下逐步了解。
    • 学习资料:http://tutorials.jenkov.comhttps://tech.meituan.com/http://mysql.taobao.org/monthly/、《面向模式的软件架构》、《设计原本》、《架构之美》、《Clean Architecture》
    • 语重心长:如果你并不想做一个工具人,就给自己的知识架构体系建设的完整一些,也算是风险抵抗了!

    3. 技术深度

    图 16-3 技术深度

    • 重要程度:⭐⭐⭐⭐⭐
    • 内容介绍:这一部分内容经常在面试求职过程中被称为造火箭、八股文。因为这部分知识探索到了JVM的运行机制,甚至去翻看C++源码,也包括JDK源码,同时还有框架的实现机制。除此之外,还有的公司会拓展到你可能完全没接触过的字节码插桩、全链路监控等等。
    • 学习资料:《java虚拟机规范》、《Java并发编程实战》、《多处理器编程的艺术》、《面经手册》《字节码编程》
    • 语重心长:有人说这叫内卷,那难道高考不卷?车牌号不卷?只要有资源竞争,就一定会有争夺。

    4. 技术经验

    图 16-4 技术经验

    • 重要程度:⭐⭐⭐⭐⭐
    • 内容介绍:如果你说问你源码、机制是造飞机,那技术的落地才是你真正的本事。这里一部分是框架、架构的搭建,另外一部分是源码和核心组件的使用。也就是你的核心框架源码学习,是否能做到技术迁移运用到你的项目中,做出可落地的程序。学习、沉淀、积累,这更像一盘大棋!
    • 学习资料:CodeGuide
    • 语重心长:不造轮子?对个人来说,轮子越多,车就越稳!

    5. 学习能力

    图 16-5 学习能力

    • 重要程度:⭐⭐⭐⭐
    • 内容介绍:学习能力主要是输入和输出,一遍吸纳知识,一遍沉淀知识。如果只看不记录不写,早早晚晚也就忘没了。这方便沉淀下来的内容都是个人的技术标签,尤其是参与过开源项目,或者自己有一个项目得到认可。
    • 学习资料:https://github.comhttps://stackoverflow.comhttps://www.csdn.nethttps://www.cnblogs.com
    • 语重心长:写博客真的是一种非常好的学习方式,每当你要输出一个知识的时候,你就需要阅读、收集、整理、汇总。日复一日的沉淀,终究会让你有非常大的提升。

    6. 工作能力

    图 16-6 工作能力

    • 重要程度:⭐⭐⭐⭐
    • 内容介绍:招聘人你觉得是先看能力还是先看素质?其实很多团队招聘是先看人的,如果你不能表现出一个积极、乐观、抗压、不玻璃心的态度,团队招聘是会有些抗拒的,谁也不希望招聘一个需要哄着的码宝男。但工作能力同样重要,最终是你的担事心态和担事能力来撑起你的工资和职位。
    • 学习资料:《非暴力沟通》、《关键对话-如何高效能沟通》、《逆商:我们该如何应对坏事件》、《人月神话》
    • 语重心长:沟通是解决双方或多方的认知偏差问题最终达成共识,情商是沟通的润滑剂,无论对谁都应该保持自己为追求更好而有的格局。

    7. 项目经验

    图 16-7 项目经验

    • 重要程度:⭐⭐⭐⭐
    • 内容介绍:项目经验来自于各个不同行业的技术范围,比如:社交、电商、外卖、出行、视频、音乐、汽车、支付、短视频等等,都会在各自的领域有一定的技术壁垒和相同之处。所以一般做游戏开发的可能跳槽到交易支付,还是会有很多不了解的。所以尽可能是在自己的行业内跳槽,或者你可以做到知识的拓展,自己多学习。
    • 语重心长:不要守着自己的一亩三分地,多看看、多了解。

    三、30岁程序员占比

    本周在群里做了一次简单的《2020年互联网程序员年龄分布统计》,因为人群的关系可能数据是有一些不准。但这份数据可以作为参考,也可以参与投票。

    选项票数占比
    未满 18 岁 - 19 岁113.9 %
    20-25 岁10838.6 %
    26-30 岁11139.6 %
    31-35 岁279.6 %
    36-40 岁113.9 %
    41-45 岁93.2 %
    46岁及以上31.1 %
    • 主力程序员集中在25~30岁,也就是刚毕业到工作7年左右。
    • 30以后的程序员呢?是不写代码了吗?其实,其实从这数据可以看出30以后的程序可能是晋升做管理,几乎不怎么参与到各种技术群的学习了。但也有另外一个现实,就是30岁以后基本都已经结婚生子,上有老、下有小。基本是没有自己的时间,也就没有了学习新知识的时间,也没有参与到各种技术群的时间。

    统计数据

    2020年互联网程序员年龄分布统计,截图

    参与投票

    2020年互联网程序员年龄分布统计,投票

    四、总结

    • 与抵抗互联网风险相比能做的,只能是多学习、多沉淀、多积累。让30岁有30岁的能力,35岁有35岁的经历。因为没有所谓的安全,只有拥有留下的本事和走出去的能力才是安全的。
    • 30岁以后面临的不只是学习技术,还有很多原因是没有时间。有家庭、有父母、有妻子,有生活的杂事,有工作的占据,很难拿出一个时间给自己。哪怕是健身、学习,也得要挤时间。
    • 大部分程序员的愿望是什么?做过一次5年后的愿望收集,大部分希望升官发财、家庭美好、买车买房,也有希望一屋两人三餐四季,平平淡淡。其实大家在这个行业都很累,我的愿望可能是以后蜗居在天津,有个大书房、写写书、开车逛逛,有自由的时间。来自:程序员的愿望

    五、系列推荐


    我的博客:https://bugstack.cn

    查看原文

    赞 13 收藏 7 评论 0

    小傅哥 发布了文章 · 2020-12-17

    阿里不允许使用 Executors 创建线程池!那怎么使用,怎么监控?


    作者:小傅哥
    博客:https://bugstack.cn

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    五常大米好吃!

    哈哈哈,是不你总买五常大米,其实五常和榆树是挨着的,榆树大米也好吃,榆树还是天下第一粮仓呢!但是五常出名,所以只认识五常。

    为什么提这个呢,因为阿里不允许使用 Executors 创建线程池!其他很多大厂也不允许,这么创建的话,控制不好会出现OOM。

    ,本篇就带你学习四种线程池的不同使用方式、业务场景应用以及如何监控线程。

    二、面试题

    谢飞机,小记!,上次从面试官那逃跑后,恶补了多线程,自己好像也内卷了,所以出门逛逛!

    面试官:嗨,飞机,飞机,这边!

    谢飞机:嗯?!哎呀,面试官你咋来南海子公园了?

    面试官:我家就附近,跑步来了。最近你咋样,上次问你的多线程学了吗?

    谢飞机:哎,看了是看了,记不住鸭!

    面试官:嗯,不常用确实记不住。不过你可以选择跳槽,来大厂,大厂的业务体量较大!

    谢飞机:我就纠结呢,想回家考教师资格证了,我们村小学要教java了!

    面试官:哈哈哈哈哈,一起!

    三、四种线程池使用介绍

    Executors 是创建线程池的工具类,比较典型常见的四种线程池包括:newFixedThreadPoolnewSingleThreadExecutornewCachedThreadPoolnewScheduledThreadPool。每一种都有自己特定的典型例子,可以按照每种的特性用在不同的业务场景,也可以做为参照精细化创建线程池。

    1. newFixedThreadPool

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 1; i < 5; i++) {
            int groupId = i;
            executorService.execute(() -> {
                for (int j = 1; j < 5; j++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ignored) {
                    }
                    logger.info("第 {} 组任务,第 {} 次执行完成", groupId, j);
                }
            });
        }
        executorService.shutdown();
    }
    
    // 测试结果
    23:48:24.628 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 组任务,第 1 次执行完成
    23:48:24.628 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 组任务,第 1 次执行完成
    23:48:24.628 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 组任务,第 1 次执行完成
    23:48:25.633 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 组任务,第 2 次执行完成
    23:48:25.633 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 组任务,第 2 次执行完成
    23:48:25.633 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 组任务,第 2 次执行完成
    23:48:26.633 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 组任务,第 3 次执行完成
    23:48:26.633 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 组任务,第 3 次执行完成
    23:48:26.633 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 组任务,第 3 次执行完成
    23:48:27.634 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 组任务,第 4 次执行完成
    23:48:27.634 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 组任务,第 4 次执行完成
    23:48:27.634 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 组任务,第 4 次执行完成
    23:48:28.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 组任务,第 1 次执行完成
    23:48:29.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 组任务,第 2 次执行完成
    23:48:30.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 组任务,第 3 次执行完成
    23:48:31.636 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 组任务,第 4 次执行完成
    
    Process finished with exit code 0

    图解

    图 22-1 newFixedThreadPool 执行过程

    • 代码new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
    • 介绍:创建一个固定大小可重复使用的线程池,以 LinkedBlockingQueue 无界阻塞队列存放等待线程。
    • 风险:随着线程任务不能被执行的的无限堆积,可能会导致OOM。

    2. newSingleThreadExecutor

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 1; i < 5; i++) {
            int groupId = i;
            executorService.execute(() -> {
                for (int j = 1; j < 5; j++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ignored) {
                    }
                    logger.info("第 {} 组任务,第 {} 次执行完成", groupId, j);
                }
            });
        }
        executorService.shutdown();
    }
    
    // 测试结果
    23:20:15.066 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 组任务,第 1 次执行完成
    23:20:16.069 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 组任务,第 2 次执行完成
    23:20:17.070 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 组任务,第 3 次执行完成
    23:20:18.070 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 组任务,第 4 次执行完成
    23:20:19.071 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 组任务,第 1 次执行完成
    23:23:280.071 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 组任务,第 2 次执行完成
    23:23:281.072 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 组任务,第 3 次执行完成
    23:23:282.072 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 组任务,第 4 次执行完成
    23:23:283.073 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 组任务,第 1 次执行完成
    23:23:284.074 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 组任务,第 2 次执行完成
    23:23:285.074 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 组任务,第 3 次执行完成
    23:23:286.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 组任务,第 4 次执行完成
    23:23:287.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 组任务,第 1 次执行完成
    23:23:288.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 组任务,第 2 次执行完成
    23:23:289.076 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 组任务,第 3 次执行完成
    23:20:30.076 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 组任务,第 4 次执行完成

    图解

    图 22-2 newSingleThreadExecutor 执行过程

    • 代码new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
    • 介绍:只创建一个执行线程任务的线程池,如果出现意外终止则再创建一个。
    • 风险:同样这也是一个无界队列存放待执行线程,无限堆积下会出现OOM。

    3. newCachedThreadPool

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 1; i < 5; i++) {
            int groupId = i;
            executorService.execute(() -> {
                for (int j = 1; j < 5; j++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ignored) {
                    }
                    logger.info("第 {} 组任务,第 {} 次执行完成", groupId, j);
                }
            });
        }
        executorService.shutdown();
        
        // 测试结果
        23:25:59.818 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 组任务,第 1 次执行完成
        23:25:59.818 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 组任务,第 1 次执行完成
        23:25:59.818 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 组任务,第 1 次执行完成
        23:25:59.818 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 组任务,第 1 次执行完成
        23:25:00.823 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 组任务,第 2 次执行完成
        23:25:00.823 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 组任务,第 2 次执行完成
        23:25:00.823 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 组任务,第 2 次执行完成
        23:25:00.823 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 组任务,第 2 次执行完成
        23:25:01.823 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 组任务,第 3 次执行完成
        23:25:01.823 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 组任务,第 3 次执行完成
        23:25:01.824 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 组任务,第 3 次执行完成
        23:25:01.824 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 组任务,第 3 次执行完成
        23:25:02.824 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 组任务,第 4 次执行完成
        23:25:02.824 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 组任务,第 4 次执行完成
        23:25:02.825 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 组任务,第 4 次执行完成
        23:25:02.825 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 组任务,第 4 次执行完成
    }

    图解

    图 22-3 newCachedThreadPool 执行过程

    • 代码new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())
    • 介绍:首先 SynchronousQueue 是一个生产消费模式的阻塞任务队列,只要有任务就需要有线程执行,线程池中的线程可以重复使用。
    • 风险:如果线程任务比较耗时,又大量创建,会导致OOM

    4. newScheduledThreadPool

    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.schedule(() -> {
            logger.info("3秒后开始执行");
        }, 3, TimeUnit.SECONDS);
        executorService.scheduleAtFixedRate(() -> {
            logger.info("3秒后开始执行,以后每2秒执行一次");
        }, 3, 2, TimeUnit.SECONDS);
        executorService.scheduleWithFixedDelay(() -> {
            logger.info("3秒后开始执行,后续延迟2秒");
        }, 3, 2, TimeUnit.SECONDS);
    }
    
    // 测试结果
    23:28:32.442 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒后开始执行
    23:28:32.444 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒后开始执行,以后每2秒执行一次
    23:28:32.444 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒后开始执行,后续延迟2秒
    23:28:34.441 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒后开始执行,以后每2秒执行一次
    23:28:34.445 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒后开始执行,后续延迟2秒
    23:28:36.440 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒后开始执行,以后每2秒执行一次
    23:28:36.445 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒后开始执行,后续延迟2秒

    图解

    [图 22-4 newScheduledThreadPool 执行过程

    • 代码public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()); }
    • 介绍:这就是一个比较有意思的线程池了,它可以延迟定时执行,有点像我们的定时任务。同样它也是一个无限大小的线程池 Integer.MAX_VALUE。它提供的调用方法比较多,包括:scheduleAtFixedRatescheduleWithFixedDelay,可以按需选择延迟执行方式。
    • 风险:同样由于这是一组无限容量的线程池,所以依旧又OOM风险。

    四、线程池使用场景说明

    什么时候使用线程池?

    说简单是当为了给老板省钱的时候,因为使用线程池可以降低服务器资源的投入,让每台机器尽可能更大限度的使用CPU。

    😄那你这么说肯定没办法升职加薪了!

    所以如果说的高大上一点,那么是在符合科特尔法则阿姆达尔定律 的情况下,引入线程池的使用最为合理。啥意思呢,还得简单说!

    假如:我们有一套电商服务,用户浏览商品的并发访问速率是:1000客户/每分钟,平均每个客户在服务器上的耗时0.5分钟。根据利特尔法则,在任何时刻,服务端都承担着1000*0.5=500个客户的业务处理量。过段时间大促了,并发访问的用户扩了一倍2000客户了,那怎么保障服务性能呢?

    1. 提高服务器并发处理的业务量,即提高到2000×0.5=1000
    2. 减少服务器平均处理客户请求的时间,即减少到:2000×0.25=500

    所以:在有些场景下会把串行的请求接口,压缩成并行执行,如图 22-5

    图22-5 多线程接口查询使用

    但是,线程池的使用会随着业务场景变化而不同,如果你的业务需要大量的使用线程池,并非常依赖线程池,那么就不可能用 Executors 工具类中提供的方法。因为这些线程池的创建都不够精细化,也非常容易造成OOM风险,而且随着业务场景逻辑不同,会有IO密集型和CPU密集型。

    最终,大家使用的线程池都是使用 new ThreadPoolExecutor() 创建的,当然也有基于Spring的线程池配置 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor

    可你想过吗,同样一个接口在有活动时候怎么办、有大促时候怎么办,可能你当时设置的线程池是合理的,但是一到流量非常大的时候就很不适合了,所以如果能动态调整线程池就非常有必要了。而且使用 new ThreadPoolExecutor() 方式创建的线程池是可以通过提供的 set 方法进行动态调整的。有了这个动态调整的方法后,就可以把线程池包装起来,在配合动态调整的页面,动态更新线程池参数,就可以非常方便的调整线程池了。

    五、获取线程池监控信息

    你收过报警短信吗?

    收过,半夜还有报警机器人打电话呢!崴,你的系统有个机器睡着了,快起来看看!!!

    所以,如果你高频、高依赖线程池,那么有一个完整的监控系统,就非重要了。总不能线上挂了,你还不知道!

    可监控内容

    方法含义
    getActiveCount()线程池中正在执行任务的线程数量
    getCompletedTaskCount()线程池已完成的任务数量,该值小于等于taskCount
    getCorePoolSize()线程池的核心线程数量
    getLargestPoolSize()线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize
    getMaximumPoolSize()线程池的最大线程数量
    getPoolSize()线程池当前的线程数量
    getTaskCount()线程池已经执行的和未执行的任务总数

    1. 重写线程池方式监控

    如果我们想监控一个线程池的方法执行动作,最简单的方式就是继承这个类,重写方法,在方法中添加动作收集信息。

    伪代码

    public class ThreadPoolMonitor extends ThreadPoolExecutor {
    
        @Override
        public void shutdown() {
            // 统计已执行任务、正在执行任务、未执行任务数量
            super.shutdown();
        }
    
        @Override
        public List<Runnable> shutdownNow() {
            // 统计已执行任务、正在执行任务、未执行任务数量
            return super.shutdownNow();
        }
    
        @Override
        protected void beforeExecute(Thread t, Runnable r) {
            // 记录开始时间
        }
    
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            // 记录完成耗时
        }
        
        ...
    }

    2. 基于IVMTI方式监控

    这块是监控的重点,因为我们不太可能让每一个需要监控的线程池都来重写的方式记录,这样的改造成本太高了。

    那么除了这个笨方法外,可以选择使用基于JVMTI的方式,进行开发监控组件。

    JVMTI:JVMTI(JVM Tool Interface)位于jpda最底层,是Java虚拟机所提供的native编程接口。JVMTI可以提供性能分析、debug、内存管理、线程分析等功能。

    基于jvmti提供的接口服务,运用C++代码(win32-add_library)在Agent_OnLoad里开发监控服务,并生成dll文件。开发完成后在java代码中加入agentpath,这样就可以监控到我们需要的信息内容。

    环境准备

    1. Dev-C++
    2. JetBrains CLion 2018.2.3
    3. IntelliJ IDEA Community Edition 2018.3.1 x64
    4. jdk1.8.0_45 64位
    5. jvmti(在jdk安装目录下jdk1.8.0_45\include里,把include整个文件夹复制到和工程案例同层级目录下,便于 include 引用)

    配置信息:(路径相关修改为自己的)

    1. C++开发工具Clion配置
      1.配置位置;Settings->Build,Execution,Deployment->Toolchains

      1. MinGM配置:D:\Program Files (x86)\Dev-Cpp\MinGW64
    2. java调试时配置

      1. 配置位置:Run/Debug Configurations ->VM options
      2. 配置内容:-agentpath:E:\itstack\git\github.com\itstack-jvmti\cmake-build-debug\libitstack_jvmti.dll

    2.1 先做一个监控例子

    Java工程

    public class TestLocationException {
    
        public static void main(String[] args) {
            Logger logger = Logger.getLogger("TestLocationException");
            try {
                PartnerEggResourceImpl resource = new PartnerEggResourceImpl();
                Object obj = resource.queryUserInfoById(null);
                logger.info("测试结果:" + obj);
            } catch (Exception e) {
                //屏蔽异常
     }
        }
    }
    
    class PartnerEggResourceImpl {
        Logger logger = Logger.getLogger("PartnerEggResourceImpl");
        public Object queryUserInfoById(String userId) {
            logger.info("根据用户Id获取用户信息" + userId);
            if (null == userId) {
                throw new NullPointerException("根据用户Id获取用户信息,空指针异常");
            }
            return userId;
        }
    }

    c++监控

    #include <iostream>
    #include <cstring>
    #include "jvmti.h"
    
    using namespace std;
    
    //异常回调函数
    static void JNICALL
    callbackException(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID methodId, jlocation location,
    jobject exception, jmethodID catch_method, jlocation catch_location) {
    // 获得方法对应的类
    jclass clazz;
    jvmti_env->GetMethodDeclaringClass(methodId, &clazz);
    
    // 获得类的签名
    char *class_signature;
    jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);
    
    //过滤非本工程类信息
    string::size_type idx;
    string class_signature_str = class_signature;
    idx = class_signature_str.find("org/itstack");
    if (idx != 1) {
    return;
    }
    
    //异常类名称
    char *exception_class_name;
    jclass exception_class = env->GetObjectClass(exception);
    jvmti_env->GetClassSignature(exception_class, &exception_class_name, nullptr);
    
    // 获得方法名称
    char *method_name_ptr, *method_signature_ptr;
    jvmti_env->GetMethodName(methodId, &method_name_ptr, &method_signature_ptr, nullptr);
    
    //获取目标方法的起止地址和结束地址
    jlocation start_location_ptr;    //方法的起始位置
    jlocation end_location_ptr;      //用于方法的结束位置
    jvmti_env->GetMethodLocation(methodId, &start_location_ptr, &end_location_ptr);
    
    //输出测试结果
    cout << "测试结果 - 定位类的签名:" << class_signature << endl;
    cout << "测试结果 - 定位方法信息:" << method_name_ptr << " -> " << method_signature_ptr << endl;
    cout << "测试结果 - 定位方法位置:" << start_location_ptr << " -> " << end_location_ptr + 1 << endl;
    cout << "测试结果 - 异常类的名称:" << exception_class_name << endl;
    
    cout << "测试结果-输出异常信息(可以分析行号):" << endl;
    jclass throwable_class = (*env).FindClass("java/lang/Throwable");
    jmethodID print_method = (*env).GetMethodID(throwable_class, "printStackTrace", "()V");
    (*env).CallVoidMethod(exception, print_method);
    
    }
    
    
    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
        jvmtiEnv *gb_jvmti = nullptr;
        //初始化
        vm->GetEnv(reinterpret_cast<void **>(&gb_jvmti), JVMTI_VERSION_1_0);
        // 创建一个新的环境
        jvmtiCapabilities caps;
        memset(&caps, 0, sizeof(caps));
        caps.can_signal_thread = 1;
        caps.can_get_owned_monitor_info = 1;
        caps.can_generate_method_entry_events = 1;
        caps.can_generate_exception_events = 1;
        caps.can_generate_vm_object_alloc_events = 1;
        caps.can_tag_objects = 1;
        // 设置当前环境
        gb_jvmti->AddCapabilities(&caps);
        // 创建一个新的回调函数
        jvmtiEventCallbacks callbacks;
        memset(&callbacks, 0, sizeof(callbacks));
        //异常回调
        callbacks.Exception = &callbackException;
        // 设置回调函数
        gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
        // 开启事件监听(JVMTI_EVENT_EXCEPTION)
        gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, nullptr);
        return JNI_OK;
    }
    
    JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
    }

    测试结果

    在 VM vptions 中配置:-agentpath:E:\itstack\git\github.com\itstack-jvmti\cmake-build-debug\libitstack_jvmti.dll

    十二月 16, 2020 23:53:27 下午 org.itstack.demo.PartnerEggResourceImpl queryUserInfoById
    信息: 根据用户Id获取用户信息null
    java.lang.NullPointerException: 根据用户Id获取用户信息,空指针异常
        at org.itstack.demo.PartnerEggResourceImpl.queryUserInfoById(TestLocationException.java:26)
        at org.itstack.demo.TestLocationException.main(TestLocationException.java:13)
    测试结果-定位类的签名:Lorg/itstack/demo/PartnerEggResourceImpl;
    测试结果-定位方法信息:queryUserInfoById -> (Ljava/lang/String;)Ljava/lang/Object;
    测试结果-定位方法位置:0 -> 43
    测试结果-异常类的名称:Ljava/lang/NullPointerException;
    测试结果-输出异常信息(可以分析行号):
    • 这就是基于JVMTI的方式进行监控,这样的方式可以做到非入侵代码。不需要硬编码,也就节省了人力,否则所有人都会进行开发监控内容,而这部分内容与业务逻辑并无关系。

    2.2 扩展线程监控

    其实方法差不多,都是基于C++开发DLL文件,引入使用。不过这部分代码会监控方法信息,并采集线程的执行内容。

    static void JNICALL callbackMethodEntry(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID method) {
        // 获得方法对应的类
        jclass clazz;
        jvmti_env->GetMethodDeclaringClass(method, &clazz);
    
        // 获得类的签名
        char *class_signature;
        jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);
    
        //过滤非本工程类信息
        string::size_type idx;
        string class_signature_str = class_signature;
        idx = class_signature_str.find("org/itstack");
    
        gb_jvmti->RawMonitorEnter(gb_lock);
    
        {
            //must be deallocate
            char *name = NULL, *sig = NULL, *gsig = NULL;
            jint thr_hash_code = 0;
    
            error = gb_jvmti->GetMethodName(method, &name, &sig, &gsig);
            error = gb_jvmti->GetObjectHashCode(thr, &thr_hash_code);
    
            if (strcmp(name, "start") == 0 || strcmp(name, "interrupt") == 0 ||
                strcmp(name, "join") == 0 || strcmp(name, "stop") == 0 ||
                strcmp(name, "suspend") == 0 || strcmp(name, "resume") == 0) {
    
                //must be deallocate
                jobject thd_ptr = NULL;
                jint hash_code = 0;
                gb_jvmti->GetLocalObject(thr, 0, 0, &thd_ptr);
                gb_jvmti->GetObjectHashCode(thd_ptr, &hash_code);
    
                printf("[线程监控]: thread (%10d) %10s (%10d)\n", thr_hash_code, name, hash_code);
            }
        }
    
        gb_jvmti->RawMonitorExit(gb_lock);
    }
    
    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
    
        // 初始化
        jvm->GetEnv((void **) &gb_jvmti, JVMTI_VERSION_1_0);
        // 创建一个新的环境
        memset(&gb_capa, 0, sizeof(jvmtiCapabilities));
        gb_capa.can_signal_thread = 1;
        gb_capa.can_get_owned_monitor_info = 1;
        gb_capa.can_generate_method_exit_events = 1;
        gb_capa.can_generate_method_entry_events = 1;
        gb_capa.can_generate_exception_events = 1;
        gb_capa.can_generate_vm_object_alloc_events = 1;
        gb_capa.can_tag_objects = 1;
        gb_capa.can_generate_all_class_hook_events = 1;
        gb_capa.can_generate_native_method_bind_events = 1;
        gb_capa.can_access_local_variables = 1;
        gb_capa.can_get_monitor_info = 1;
        // 设置当前环境
        gb_jvmti->AddCapabilities(&gb_capa);
        // 创建一个新的回调函数
        jvmtiEventCallbacks callbacks;
        memset(&callbacks, 0, sizeof(jvmtiEventCallbacks));
        // 方法回调
        callbacks.MethodEntry = &callbackMethodEntry;
        // 设置回调函数
        gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
    
        gb_jvmti->CreateRawMonitor("XFG", &gb_lock);
    
        // 注册事件监听(JVMTI_EVENT_VM_INIT、JVMTI_EVENT_EXCEPTION、JVMTI_EVENT_NATIVE_METHOD_BIND、JVMTI_EVENT_CLASS_FILE_LOAD_HOOK、JVMTI_EVENT_METHOD_ENTRY、JVMTI_EVENT_METHOD_EXIT)
        error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, (jthread) NULL);
        error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, (jthread) NULL);
        error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_NATIVE_METHOD_BIND, (jthread) NULL);
        error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, (jthread) NULL);
        error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, (jthread) NULL);
        error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, (jthread) NULL);
    
        return JNI_OK;
    }
    • 从监控的代码可以看到,这里有线程的 start、stop、join、interrupt 等,并可以记录执行信息。
    • 另外这里监控的方法执行回调,SetEventCallbacks(&callbacks, sizeof(callbacks)); 以及相应事件的添加。

    六、总结

    • 如果说你所经历的业务体量很小,那么几乎并不需要如此复杂的技术栈深度学习,甚至几乎不需要扩展各类功能,也不需要监控。但终究有一些需要造飞机的大厂,他们的业务体量庞大,并发数高,让原本可能就是一个简单的查询接口,也要做熔断、降级、限流、缓存、线程、异步、预热等等操作。
    • 知其然才敢用,如果对一个技术点不是太熟悉,就不要胡乱使用,否则遇到的OOM并不是那么好复现,尤其是在并发场景下。当然如果你们技术体系中有各种服务,比如流量复现、链路追踪等等,那么还好。
    • 又扯到了这,一个坚持学习、分享、沉淀的男人!好了,如果有错字、内容不准确,欢迎直接怼给我,我喜欢接受。但不要欺负我哦哈哈哈哈哈!

    七、系列推荐

    查看原文

    赞 9 收藏 4 评论 0

    小傅哥 发布了文章 · 2020-12-14

    得物(毒)APP,8位抽奖码需求,这不就是产品给我留的数学作业!

    image
    作者:小傅哥
    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    图 15-1 写好代码的核心

    为什么你的代码一坨坨?其实来自你有那么多为什么你要这样写代码!

    • 为什么你的代码那么多for循环?因为没有合理的数据结构和算法逻辑。
    • 为什么你的代码那么多ifelse?因为缺少设计模式对业务场景的运用。
    • 为什么你的程序应用复杂对接困难?因为没有良好的系统架构拆分和规划。
    • 为什么你的程序逻辑开发交付慢返工多?因为不具备某些业务场景的开发经验。
    • 为什么你的程序展现都是看上去不说人话?因为没有产品思维都是程序员逻辑的体现。

    最终,所有的这些不合理交织在一起,就是你能看到的一坨坨的代码!所以,要想把代码写好、写美,写到自己愿意反复欣赏,那么基本需要你有一定的:基础能力(数据结构、算法逻辑、设计模式)、应用能力(系统架构、开发经验)、拓展能力(产品思维),这三方面综合起来才能更好的开发程序。

    但可能杠精会喊,我就写个CRUD要什么逻辑、什么数据结构,还算法? 但写CRUD并不一定业务需求是CRUD,只是你的知识面和技术深度只能把它设计成CRUD,用ifelse和for循环在一个类里反复粘贴复制罢了。

    可能同样的需求交给别人手里,就会想的更多搭建的更加完善。就像:树上10只鸟开一枪还剩下几只,你会想到什么?比如:

    • 手抢是无声的吗?
    • 枪声大吗?
    • 这个城市打鸟犯不犯法?
    • 确定那只鸟被打死了?
    • 树上的鸟有没有聋子?
    • 有没有被关在笼子里或者绑在树上的鸟?
    • 旁边还有其他树吗?
    • 有残疾或者飞不动的鸟吗?
    • 有怀孕肚子里的鸟吗?
    • 打鸟的人眼睛花没花?
    • 保证是10只吗?
    • 有没有那种不怕死的鸟?
    • 会不会一枪打死两只或者更多?
    • 所有的鸟都可以自由活动飞离树以外吗?
    • 打死以后挂在树上还是掉下来了?

    所以,你还相信写程序只是简简单单的搞CRUD吗?接下来小傅哥再带着你搞几个例子看一看!

    二、代码就是对数学逻辑的具体实现

    数据结构:数组、链表、红黑树
    算法逻辑:哈希、扰动函数、负载因子、拉链寻址、

    其实我们所开发的业务程序,哪怕是CRUD也都是对数学逻辑的具体实现过程。只不过简单的业务有简单的数学逻辑、复杂的业务有复杂的数学逻辑。数学逻辑是对数据结构的使用,(例如:把大象装进冰箱分几步)合理的数据的结构有利于数据逻辑的实现和复杂程度。

    在我们常用的API中,HashMap 就是一个非常好的例子,既有非常好的数据结构的使用,也有强大的数学逻辑的实现。为此也让 HashMap 成为开发过程中非常常用的API,当然也成为面试过程中最常问的技术点。

    图 15-2 HashMap中的数据结构和数学逻辑

    重点,HashMap 中涉及的知识点非常多,包括数据结构的使用、数组、链表、红黑树,也包括算法逻辑的实现:哈希、扰动函数、负载因子、拉链寻址等等。而这些知识如果可以深入的搞清楚,是完全不需要死记硬背的,也不需要为了面试造火箭。就像如下问题:

    • HashMap 怎么来的?因为有非常多业务开发中需要key、value的形式存放获取数据。
    • 为什么要用哈希计算下标呢?因为哈希值求计算出的 key 具有低碰撞性。
    • 为什么还要加扰动函数呀?因为扰动函数可以让数据散列的均匀,如果HashMap中的数据都碰撞成短链表,就会大大降低HashMap的索引性能。
    • 为什么会有链表呢?因为无论如何都有会有节点碰撞的可能,碰撞后HashMap选择拉链寻址的方式存放数据。当然在 ThreadLocal 中采用的是斐波那契(Fibonacci)散列+开放寻址,感兴趣也可以看看。
    • 为什么链表会转换树呢?因为时间复杂度问题,链表的时间复杂度是O(n),越长越慢。
    • 为什么树是红黑树呢?红黑树具有平衡性,也就是黑色节点是平衡的,平衡带来的效果就是控制整体树高,让时间复杂度最终保持在O(logn),否则都是一丿的树就没意义了。
    • 为什么有个负载因子呢?负载因子决定HashMap的高矮胖瘦,负载你可以理解成一辆卡车能装多少货,装的越多这一趟赚的也阅读风险也越高,装的越少跑的越快赚的也少。所以选择了适当大小0.75。
    • 为什么JDK8优化了数据扩容时迁移?那不就是因为计算哈希值求下标耗费时间吗,已经找到了数学规律,直接迁移就可以了,提高性能。

    看到了吗? HashMap完全就是对数据结构的综合使用,以及对数学逻辑的完美结合,才让我们有了非常好用的HashMap。这些知识的学习就可以技术迁移到我们自己业务开发中,把有些业务开发优化到非常不错的性能体现上。同时你的代码也值得加薪!

    哈希下标

    图 15-2 中涉及到的下标位置存放的数据,不是胡乱写的。是按照 HashMap 中的计算逻辑找到的固定位置值。代码如下:

    for (int i = 1; i < 1000; i++) {
        String key = String.valueOf(i);
        int hash = key.hashCode() ^ (key.hashCode() >>> 16);
        int idx = (64 - 1) & hash;
        
        if (idx == 2) {
           // System.out.println(i + " Idx:" + idx);
        }
        if (idx == 62) {
            System.out.println(i + " Idx:" + idx);
        }
    }

    如果你需要英文的,那么可以跑10万单词的字典表。关于HashMap的内容小傅哥已经整理到面经手册中,链接:面经手册 • 拿大厂Offer

    三、得物(毒) 8位随机抽奖码设计

    1. 需求描述

    图 15-3 模仿得物(毒) APP抽奖码需求

    图 15-3 是我们模拟得物APP中关于抽奖码需求的样式图,核心技术点包括:

    1. 需要一个8位的随机码,全局唯一。
    2. 每个人可以获得多个这样的随机码,随机码阅读中奖概率越大。
    3. 随机码我们这里的设计与毒App的展现形式略有不同,组成包括:大写字母、小写字母和数字。

    在你没有看实现方案前,你可以先考虑下这样的唯一的随机码该怎样去生成。

    2. 实现方案

    2.1 基于Redis生成

    int codeId = RedisUtil.incr("codeUUID");
    String UUID = String.format("%08d", codeId);
    System.out.println(UUID);
    
    // 测试结果
    00000001
    00000002
    00000003
    • 评分:⭐
    • 方案:基于 Redis 的 incr 方法,全局自增从0开始,以上是伪代码。
    • 点评:以上方案不可用,除了并不一定能保证全局自增和可靠性外,有一个很大的问题是你的顺序自增,把APP有多少人参加活动的数据暴露了。

    2.2 随机数生成

    Random random = new Random();
    StringBuffer code = new StringBuffer();
    for (int i = 0; i < 8; i++) {
        int number = random.nextInt(3);
        switch (number) {
            case 0:
                code.append((char) (random.nextInt(26) + 65)); // 65 ~ 90
                break;
            case 1:
                code.append((char) (random.nextInt(26) + 97)); // 97 ~ 122
                break;
            case 2:
                code.append((char) (random.nextInt(9) + 48)); // 48 ~ 97
                break;
        }
    }
    System.out.println(code.toString());
    
    // 测试结果
    qvY0Fqrk
    8uyehK3H
    U7z2v4qK
    • 评分:⭐⭐
    • 方案:基于随机数生成8位随机码,相当于62^8次幂,有将近百万亿的随机数。
    • 点评:此方案在很多业务场景中都有使用,但这里的实现还有一个问题,就是随性后的不唯一性,虽然我们知道这么大体量很难出现两个相同的。但如果随着业务运营日积月累的使用,终究会有两个一样的随机数,只要出现就会是客诉。所以还需要保证唯一性,可以在随机数中加入年或者月的标记,按照这个体量落库用防重方式保证唯一。当然你还可以有其他的方式来保证唯一

    2.3 基于雪花算法

    final static char[] digits = { '0', '1', '3', '2', '4', '7', '6', '5', '8',
            'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
            '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
            'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
            'Z', '0', '1', };
            
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
        System.out.println(idWorker.nextId());
        long code = idWorker.nextId();
        char[] buf = new char[64];
        int charPos = 64;
        int radix = 1 << 6;
        long mask = radix - 1;
        do {
            buf[--charPos] = digits[(int) (code & mask)];
            code >>>= 6;
        } while (code != 0);
        System.out.println(new String(buf, charPos, (64 - charPos)));
    }
    
    // 测试结果
    uxdDQOG001
    uxd8Uoj001
    uxdERuG000
    • 评分:⭐⭐⭐
    • 方案:基于雪花算法的核心目的是,生成随机串的本身就是唯一值,那么就不需要考虑重复性。只需要将唯一值转换为对应64进制的字符串组合就可以了。
    • 点评:这里的思路很好,但有几个问题需要解决。首先是雪花算法的长度是18位,在转换为64位时会会有10位长的随机字符串组合,不满足要求。另外大写字母、小写字母和数字组合是62个,还缺少2个不满足64个,所以需要后面补充两位,但这两位生成的组合数需要废弃。那么,如果按照这个生成随机串且保证唯一的思路,就需要完善雪花算法,降低位数,在满足业务自身的情况下,控制生成长度。

    实现方案,终究不会一次就完美,还需要不断的优化完善。除此之外也会有很多其他的思路,例如电商生成订单号的方案也可以考虑设计,另外你以为这就完事了?当你已经工作多年,那么你每一天其实都在解决技术问题也是数学问题,产品的需求也更像是数学作业!加油数学老师!

    四、总结

    • 好的程序实现离不开数据结构的设计、逻辑算法的完善、设计模式的考量,再配合符合业务发展和程序设计的架构才能搭建出更加合理的程序。
    • 在学习的过程中不要刻意去背答案、背套路,那不是理科内容的学习方式。只有你更多的去实践、去验证,让懂了就是真的懂,才更加舒心!
    • 本篇又扯到了这,想问一句你是害怕35岁,还是害怕自己能力不及年龄增长?想学就把知识学透,你骗不了面试官,只能骗自己!

    五、系列推荐

    查看原文

    赞 1 收藏 0 评论 2

    小傅哥 发布了文章 · 2020-12-10

    手写线程池,对照学习ThreadPoolExecutor线程池实现原理!


    作者:小傅哥
    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    人看手机,机器学习!

    正好是2020年,看到这张图还是蛮有意思的。以前小时候总会看到一些科技电影,讲到机器人会怎样怎样,但没想到人似乎被娱乐化的东西,搞成了低头族、大肚子!

    当意识到这一点时,其实非常怀念小时候。放假的早上跑出去,喊上三五个伙伴,要不下河摸摸鱼、弹弹玻璃球、打打pia、跳跳房子!一天下来真的不会感觉累,但现在如果是放假的一天,你的娱乐安排,很多时候会让头很累!

    就像,你有试过学习一天英语头疼,还是刷一天抖音头疼吗?或者玩一天游戏与打一天球!如果你意识到了,那么争取放下一会手机,适当娱乐,锻炼保持个好身体!

    二、面试题

    谢飞机,小记!,上次吃亏在线程上,这次可能一次坑掉两次了!

    谢飞机:你问吧,我准备好了!!!

    面试官:嗯,线程池状态是如何设计存储的?

    谢飞机:这!下一个,下一个!

    面试官:Worker 的实现类,为什么不使用 ReentrantLock 来实现呢,而是自己继承AQS?

    谢飞机:我...!

    面试官:那你简述下,execute 的执行过程吧!

    谢飞机:再见!

    三、线程池讲解

    1. 先看个例子

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
    threadPoolExecutor.execute(() -> {
        System.out.println("Hi 线程池!");
    });
    threadPoolExecutor.shutdown();
    
    // Executors.newFixedThreadPool(10);
    // Executors.newCachedThreadPool();
    // Executors.newScheduledThreadPool(10);
    // Executors.newSingleThreadExecutor();

    这是一段用于创建线程池的例子,相信你已经用了很多次了。

    线程池的核心目的就是资源的利用,避免重复创建线程带来的资源消耗。因此引入一个池化技术的思想,避免重复创建、销毁带来的性能开销。

    那么,接下来我们就通过实践的方式分析下这个池子的构造,看看它是如何处理线程的。

    2. 手写一个线程池

    2.1 实现流程

    为了更好的理解和分析关于线程池的源码,我们先来按照线程池的思想,手写一个非常简单的线程池。

    其实很多时候一段功能代码的核心主逻辑可能并没有多复杂,但为了让核心流程顺利运行,就需要额外添加很多分支的辅助流程。就像我常说的,为了保护手才把擦屁屁纸弄那么大!

    图 21-1 线程池简化流程

    关于图 21-1,这个手写线程池的实现也非常简单,只会体现出核心流程,包括:

    1. 有n个一直在运行的线程,相当于我们创建线程池时允许的线程池大小。
    2. 把线程提交给线程池运行。
    3. 如果运行线程池已满,则把线程放入队列中。
    4. 最后当有空闲时,则获取队列中线程进行运行。

    2.2 实现代码

    public class ThreadPoolTrader implements Executor {
    
        private final AtomicInteger ctl = new AtomicInteger(0);
    
        private volatile int corePoolSize;
        private volatile int maximumPoolSize;
    
        private final BlockingQueue<Runnable> workQueue;
    
        public ThreadPoolTrader(int corePoolSize, int maximumPoolSize, BlockingQueue<Runnable> workQueue) {
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
        }
    
        @Override
        public void execute(Runnable command) {
            int c = ctl.get();
            if (c < corePoolSize) {
                if (!addWorker(command)) {
                    reject();
                }
                return;
            }
            if (!workQueue.offer(command)) {
                if (!addWorker(command)) {
                    reject();
                }
            }
        }
    
        private boolean addWorker(Runnable firstTask) {
            if (ctl.get() >= maximumPoolSize) return false;
    
            Worker worker = new Worker(firstTask);
            worker.thread.start();
            ctl.incrementAndGet();
            return true;
        }
    
        private final class Worker implements Runnable {
    
            final Thread thread;
            Runnable firstTask;
    
            public Worker(Runnable firstTask) {
                this.thread = new Thread(this);
                this.firstTask = firstTask;
            }
    
            @Override
            public void run() {
                Runnable task = firstTask;
                try {
                    while (task != null || (task = getTask()) != null) {
                        task.run();
                        if (ctl.get() > maximumPoolSize) {
                            break;
                        }
                        task = null;
                    }
                } finally {
                    ctl.decrementAndGet();
                }
            }
    
            private Runnable getTask() {
                for (; ; ) {
                    try {
                        System.out.println("workQueue.size:" + workQueue.size());
                        return workQueue.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        private void reject() {
            throw new RuntimeException("Error!ctl.count:" + ctl.get() + " workQueue.size:" + workQueue.size());
        }
    
        public static void main(String[] args) {
            ThreadPoolTrader threadPoolTrader = new ThreadPoolTrader(2, 2, new ArrayBlockingQueue<Runnable>(10));
    
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                threadPoolTrader.execute(() -> {
                    try {
                        Thread.sleep(1500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("任务编号:" + finalI);
                });
            }
        }
    
    }
    
    // 测试结果
    
    任务编号:1
    任务编号:0
    workQueue.size:8
    workQueue.size:8
    任务编号:3
    workQueue.size:6
    任务编号:2
    workQueue.size:5
    任务编号:5
    workQueue.size:4
    任务编号:4
    workQueue.size:3
    任务编号:7
    workQueue.size:2
    任务编号:6
    workQueue.size:1
    任务编号:8
    任务编号:9
    workQueue.size:0
    workQueue.size:0

    以上,关于线程池的实现还是非常简单的,从测试结果上已经可以把最核心的池化思想体现出来了。主要功能逻辑包括:

    • ctl,用于记录线程池中线程数量。
    • corePoolSizemaximumPoolSize,用于限制线程池容量。
    • workQueue,线程池队列,也就是那些还不能被及时运行的线程,会被装入到这个队列中。
    • execute,用于提交线程,这个是通用的接口方法。在这个方法里主要实现的就是,当前提交的线程是加入到worker、队列还是放弃。
    • addWorker,主要是类 Worker 的具体操作,创建并执行线程。这里还包括了 getTask() 方法,也就是从队列中不断的获取未被执行的线程。

    ,那么以上呢,就是这个简单线程池实现的具体体现。但如果深思熟虑就会发现这里需要很多完善,比如:线程池状态呢,不可能一直奔跑呀!?线程池的锁呢,不会有并发问题吗?线程池拒绝后的策略呢?,这些问题都没有在主流程解决,也正因为没有这些流程,所以上面的代码才更容易理解。

    接下来,我们就开始分析线程池的源码,与我们实现的简单线程池参考对比,会更加容易理解😄!

    3. 线程池源码分析

    3.1 线程池类关系图

    图 21-2 线程池类关系图

    以围绕核心类 ThreadPoolExecutor 的实现展开的类之间实现和继承关系,如图 21-2 线程池类关系图。

    • 接口 ExecutorExecutorService,定义线程池的基本方法。尤其是 execute(Runnable command) 提交线程池方法。
    • 抽象类 AbstractExecutorService,实现了基本通用的接口方法。
    • ThreadPoolExecutor,是整个线程池最核心的工具类方法,所有的其他类和接口,为围绕这个类来提供各自的功能。
    • Worker,是任务类,也就是最终执行的线程的方法。
    • RejectedExecutionHandler,是拒绝策略接口,有四个实现类;AbortPolicy(抛异常方式拒绝)DiscardPolicy(直接丢弃)DiscardOldestPolicy(丢弃存活时间最长的任务)CallerRunsPolicy(谁提交谁执行)
    • Executors,是用于创建我们常用的不同策略的线程池,newFixedThreadPoolnewCachedThreadPoolnewScheduledThreadPoolnewSingleThreadExecutor

    3.2 高3位与低29位

    图 22-3 线程状态,高3位与低29位

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    ThreadPoolExecutor 线程池实现类中,使用 AtomicInteger 类型的 ctl 记录线程池状态和线程池数量。在一个类型上记录多个值,它采用的分割数据区域,高3位记录状态,低29位存储线程数量,默认 RUNNING 状态,线程数为0个。

    3.2 线程池状态

    图 22-4 线程池状态流转

    图 22-4 是线程池中的状态流转关系,包括如下状态:

    • RUNNING:运行状态,接受新的任务并且处理队列中的任务。
    • SHUTDOWN:关闭状态(调用了shutdown方法)。不接受新任务,,但是要处理队列中的任务。
    • STOP:停止状态(调用了shutdownNow方法)。不接受新任务,也不处理队列中的任务,并且要中断正在处理的任务。
    • TIDYING:所有的任务都已终止了,workerCount为0,线程池进入该状态后会调 terminated() 方法进入TERMINATED 状态。
    • TERMINATED:终止状态,terminated() 方法调用结束后的状态。

    3.3 提交线程(execute)

    图 22-5 提交线程流程图

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

    在阅读这部分源码的时候,可以参考我们自己实现的线程池。其实最终的目的都是一样的,就是这段被提交的线程,启动执行加入队列决策策略,这三种方式。

    • ctl.get(),取的是记录线程状态和线程个数的值,最终需要使用方法 workerCountOf(),来获取当前线程数量。`workerCountOf 执行的是 c & CAPACITY 运算
    • 根据当前线程池中线程数量,与核心线程数 corePoolSize 做对比,小于则进行添加线程到任务执行队列。
    • 如果说此时线程数已满,那么则需要判断线程池是否为运行状态 isRunning(c)。如果是运行状态则把不能被执行的线程放入线程队列中。
    • 放入线程队列以后,还需要重新判断线程是否运行以及移除操作,如果非运行且移除,则进行拒绝策略。否则判断线程数量为0后添加新线程。
    • 最后就是再次尝试添加任务执行,此时方法 addWorker 的第二个入参是 false,最终会影响添加执行任务数量判断。如果添加失败则进行拒绝策略。

    3.5 添加执行任务(addWorker)

    图 22-6 添加执行任务逻辑流程

    private boolean addWorker(Runnable firstTask, boolean core)

    第一部分、增加线程数量

    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    第一部分、创建启动线程

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateOf(ctl.get());
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;

    添加执行任务的流程可以分为两块看,上面代码部分是用于记录线程数量、下面代码部分是在独占锁里创建执行线程并启动。这部分代码在不看锁、CAS等操作,那么就和我们最开始手写的线程池基本一样了

    • if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty())),判断当前线程池状态,是否为 SHUTDOWNSTOPTIDYINGTERMINATED中的一个。并且当前状态为 SHUTDOWN、且传入的任务为 null,同时队列不为空。那么就返回 false。
    • compareAndIncrementWorkerCount,CAS 操作,增加线程数量,成功就会跳出标记的循环体。
    • runStateOf(c) != rs,最后是线程池状态判断,决定是否循环。
    • 在线程池数量记录成功后,则需要进入加锁环节,创建执行线程,并记录状态。在最后如果判断没有启动成功,则需要执行 addWorkerFailed 方法,剔除到线程方法等操作。

    3.6 执行线程(runWorker)

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // 允许中断
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) 
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

    其实,有了手写线程池的基础,到这也就基本了解了,线程池在干嘛。到这最核心的点就是 task.run() 让线程跑起来。额外再附带一些其他流程如下;

    • beforeExecuteafterExecute,线程执行的前后做一些统计信息。
    • 另外这里的锁操作是 Worker 继承 AQS 自己实现的不可重入的独占锁。
    • processWorkerExit,如果你感兴趣,类似这样的方法也可以深入了解下。在线程退出时候workers做到一些移除处理以及完成任务数等,也非常有意思

    3.7 队列获取任务(getTask)

    如果你已经开始阅读源码,可以在 runWorker 方法中,看到这样一句循环代码 while (task != null || (task = getTask()) != null)。这与我们手写线程池中操作的方式是一样的,核心目的就是从队列中获取线程方法。

    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
            int wc = workerCountOf(c);
            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
    • getTask 方法从阻塞队列中获取等待被执行的任务,也就是一条条往出拿线程方法。
    • if (rs >= SHUTDOWN ...,判断线程是否关闭。
    • wc = workerCountOf(c),wc > corePoolSize,如果工作线程数超过核心线程数量 corePoolSize 并且 workQueue 不为空,则增加工作线程。但如果超时未获取到线程,则会把大于 corePoolSize 的线程销毁掉。
    • timed,是 allowCoreThreadTimeOut 得来的。最终 timed 为 true 时,则通过阻塞队列的poll方法进行超时控制。
    • 如果在 keepAliveTime 时间内没有获取到任务,则返回null。如果为false,则阻塞。

    四、总结

    • 这一章节并没有完全把线程池的所有知识点都介绍完,否则一篇内容会有些臃肿。在这一章节我们从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的,涉及到的知识点也几乎是我们以前介绍过的内容,包括:队列、CAS、AQS、重入锁、独占锁等内容。所以这些知识也基本是环环相扣的,最好有一些根基否则会有些不好理解。
    • 除了本章介绍的,我们还没有讲到线程的销毁过程、四种线程池方法的选择和使用、以及在CPU密集型任务IO 密集型任务时该怎么配置。另外在Spring中也有自己实现的线程池方法。这些知识点都非常贴近实际操作。
    • 好了,今天的内容先扯到这,后续的内容陆续完善。如果以上内容有错字、流程缺失、或者不好理解以及描述错误,欢迎留言。互相学习、互相进步。

    五、系列推荐

    查看原文

    赞 8 收藏 7 评论 0

    小傅哥 发布了文章 · 2020-12-07

    90%的程序员,都没用过多线程和锁,怎么成为架构师?


    作者:小傅哥

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    你只面向工作学习吗?

    如果说编程只是单纯的承接产品需求开发系统功能,那么基本可以把程序开发简单理解成按照需求PRD,定义属性创建方法调用展示,这三个步骤。

    尤其是在一些大公司中,会有易用的、完善的、标准的架构体系和运维服务,例如:RPC、MQ、Redis集群、分布式任务、配置中心、分库分表组件、网关等搭配出来的系统架构。也因此让程序员做到只关心业务功能开发

    让程序员只关心业务开发,有成熟的系统架构、有标准的开发流程、有通用的功能设计,对于团队效能提升来说是非常好的事。但一部分程序员正因为有这样的好事,让日复一日的岁月做着同样的事,最后成为工具人。

    如果是框架和中间件的存在,是了让程序员只关心业务开发。那为什么你面试的时候会被问到核心组件的设计和原理呢? 在这个年代,别放弃学习是你几乎唯一的生存途径。

    二、多线程和锁没用过?

    面试必问的多线程,甚至可能问的还挺深入,比如:AQS、CAS、CLH、MCS、锁升级、对象头等等。但在实际的业务开发中,你用到了吗?可能这也是大部分同学说,面试造火箭的地方!

    互联网应用中有些业务场景开发,确实很少能用到多线程,也几乎不需要你去加锁。即使你能用到多线程的地方也可以用其他更好的方式处理,就像你需要多个线程把数据落库,那么就可以使用异步MQ的方式,把压力分散到各个应用实例上去。而这一开发方式的演变,是因为现在的应用开发和部署都是基于分布式的思想,所以也就很少会有非得用线程来压榨单实例CPU。

    在基于RPC+MQ+数据库路由+网关,以及各类配合的组件下,构建出的分布式应用,在某些时候是改变了我们的开发模式的。可能原来我们需要大量使用多线程在单个实例下的开发思路,在使用分布式架构后,就需要转变这一思想,所以随时而来的使用多线程和锁的场景也会减少。

    图 14-1 分布式简化的应用部署

    图 14-1 分布式简化的应用部署

    ,也不是就没有多线程和锁的业务场景,就比如我们的核心组件中,数据库连接池、分布式任务中,都会涉及到多线程和锁的使用。也有一些类似商品秒杀的场景,同样需要使用到锁。

    那么,使用多线程为了更大限度的利用资源提升效率,加锁是为了在同一个资源有竞争的情况保证业务流程的正确性。就像:数据库连接池为了合理分配数据库资源、商品秒杀是为了库存的竞争。

    可是,在没有需要竞争和分配资源的情况下,一般并不会在分布式场景下使用到多线程。假如我们做一个用户资源单次计数的操作,那么原来的应用是单实例还是可以加锁累加计数的。但现在是分布式应用部署,也就是你可能这一时刻是A实例提供你的需求,当你再次刷新页面后可能访问到的就是B实例。这时候在想做一些实例上的累加,就没那么方便了。

    这也就是在分布式应用框架的应用中,让你能用到多线程和锁的地方并不多的原因。但如果你有需要去了解一些中间件或者核心组件的设计时,就需要了解相关的核心知识。

    很多纸上谈兵的技术,也就是你造轮子、造火箭、成为架构师的根基! 如果你还想奔着这条路能走的更远,就需要继续学习。

    三、你的成长阶段目标?

    图 14-2 你的成长阶段

    就编程开发这条道路而言,每一个成长阶段的目标都会有它随着带来的难以攻克的

    • 上学阶段,对突如其来的奇怪知识,想把它在自己电脑运行起来,就很难。
    • 工作1~3年,以前掌握的都是毛皮,接下来需要有深度的学习,而深入后都将与数学硬碰硬。
    • 工作3~5年,看以前理论性的知识也没那么难,但怎么实际要解决一些复杂项目,还是专心脑干。
    • 工作5~7年,薪资与职位都会成为这个阶段非常难以突破的瓶颈,积累不足、沉淀不够,现状不满!
    • 工作7~10年,以前觉得什么都难学,现在可能让你有空闲时间都难。并不一定年龄到了,本事就到了。

    随着年龄的增长,每一阶段都有难以跨越的难。而那些看上去突破了瓶颈,达到了你想要的高度的人。其实每一个阶段,他们都跑在前面。

    但就单纯的技术成长而言,其实理论知识并不难,只要你学就还能会,只是付出的时间成本不同罢了。但过了理论知识这一关后,接下来要面对的是创造能力,也就是为什么你感觉自己会了那么多技术内容,但是实际开发时却总感觉写不出好代码的阶段。

    会了核心技术但又写不出好代码,就很像是:会汉字但写不出诗词歌赋懂色彩但绘不出山河大川能蹦跳但舞不出摇曳生姿

    所以,多实战一些项目代码,多看一些设计模式,会让你更好的理解代码该怎么用,也就能提升突破当前的阶段屏障。😄推荐小傅哥的《重学Java设计模式》,公众号:bugstack虫洞栈,回复:设计模式,下载。

    四、怎么成长为架构师?

    图 14-3 架构师知识体系

    讲到架构师,其实真的挺难因为报名一个课程学习完就能成为架构师。架构师的成长更多的取决你们的研发组是否需要一个架构师,也同时需要你在这个岗位起到应有的作用。

    如果你还不是架构师,但想成为架构师。那么还取决于你的老板是否愿意把你培养成架构师,以及你自己的多方面能力是否具备。另外,并不一定高级开发就低于架构师。高级开发有时候比架构师做的事更专一、更核心。

    那么除了图 14-3 对于架构师的能力概况,有哪些具体的事项呢?

    1. 定得了规范、设计了架构。
    2. 有一定的技术深入和广度,改的了bug、处理得了事故。
    3. 带了了小组推进项目落地,也能协同其他组配合。
    4. 了解运营和业务规划,提前介入产品开发阶段。
    5. 懂得了业务和运营,了解数据指标和各项ROI。
    6. 架构更多的是经验和经历的结合,而不是一个单项内容的单一渠道。
    7. 不是没有架构师就没有架构,有时候是一个公司或者小组承接的项目并没有那么大,使用成型架构模式即可。
    8. 但如果有非常复杂的场景设计,都是十几个系统的分组安排开发,提供服务,支持几万秒杀,几十万日活,在扩展到上百万DAU,就需要有架构师来把控。
    9. 再比如:从下单、到交易、到支付、到结算、到活动、到玩法、怎么支持。这个体量的复杂度才需要有架构权衡。
    10. 没有绝对的对和绝对的错,只是什么时候更适合罢了。多学一些,别给自己设定边界,才更好突围!

    做好架构,远看是部门效率,近看是解决烂代码!很多时候的急,可能让整个工程烂掉。烂的越来越多,最终也会影响业务发展。那么这些烂代码都怎么来的呢?

    1. bug很多时候是接手了的烂代码或者别人的思路没有继续继承。
    2. 业务需求简单开始就写的没有扩展性,后面也不断的堆积。
    3. 没有很好的结构和命名、也从不格式化。
    4. 预期不到将来业务走向,设计不出合理的扩展性系统。
    5. 炫技大于整体规划和设计,一个新技能的引入,但缺少相应的匹配。
    6. 没有设计,功能都是流程式,需要啥就写ifelse。
    7. 总想一把梭,没关系的,心里有抱怨,部门有急功近利,不给你长时间的铺垫,没有有人带,写不出好东西。
    8. 组内缺少相应的流程规范和评审,设计评审、代码评审,也没与标杆项目可以参考。
    9. 懂几个jdk源码从不是写好代码的根本只是基本功。就像老木匠用斧子,新木匠用电锯,但做出来的东西,有的就好,有的就不好。
    10. 没有永远好的代码,如果像代码更好,就需要一直维护,一直改造。
    11. 没有业务对应的体量,不谈QPS、TPS、TP99、TP999,服务健康度,很多空谈都是耍流氓。

    ,来自于很多方面,而且这并不是你报名个课程就能学到的。业务、产品、研发,三方共同努力才能更好的减少烂的出现,而这些也是每一个研发都应该努力的方向,也几乎是你要成为架构师的必经之路。

    五、总结

    • 写了这么多主要是想帮助那些和我一样在这条路上持续拼搏的同好,可能大家都会在这些阶段迷茫过:上学时技术怎么学、求职时简历怎么写、工作时个人怎么成长等等。所以很多时候更多的仍然是自己的克制和自己的选择!
    • 2020年这已经是12月,有疫情的开始、也有口罩带的一年、有人股票发财、也有人还不起房贷、有人急躁没目标、也有人学了不少知识。总归如何,时间很快!
    • 你用剑、我用刀、都有目标、都很风烧! 继续加油!

    六、系列推荐


    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    查看原文

    赞 7 收藏 5 评论 2

    小傅哥 发布了文章 · 2020-12-03

    面经手册 · 第20篇《Thread 线程,状态转换、方法使用、原理分析》

    作者:小傅哥
    <br/>博客:https://bugstack.cn
    <br/>Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    考不常用的、考你不会的、考你忽略的,才是考试!

    大部分考试考的,基本都是不怎么用的。例外的咱们不说😄 就像你做程序开发,尤其在RPC+MQ+分库分表,其实很难出现让你用一个机器实例编写多线程压榨CPU性能。很多时候是扔出一个MQ,异步消费了。如果没有资源竞争,例如库表秒杀,那么其实你确实很难接触多并发编程以及锁的使用。

    但!凡有例外,比如你需要开发一个数据库路由中间件,那么就肯定会出现在一台应用实例上分配数据库资源池的情况,如果出现竞争就要合理分配资源。如此,类似这样的中间件开发,就会涉及到一些更核心底层的技术的应用。

    所以,有时候不是没用,而是你没有

    二、面试题

    谢飞机,小记! 线程我玩定了,面试也拦不住我,我说的!

    谢飞机:嘿,你好哇,我是谢飞机!

    面试官:好,今天电话面试,你准备好了?

    谢飞机:准备好了,嘿嘿!

    面试官:嗯,我看你简历里写了不少线程的东西,看来了解的不错。问你一个线程吧那就,线程之间状态是怎么转换的?

    谢飞机:扒拉扒拉,扒拉扒拉!

    面试官:嗯,还不错。那 yield 方法是怎么使用的。

    谢飞机:嗯!好像是让出CPU。具体的没怎么用过!

    面试官:做做测试,验证下,下次问你。

    三、Thread 状态关系

    Java 的线程状态描述在枚举类 java.lang.Thread.State 中,共包括如下五种状态:

    public enum State {
        NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
    }

    这五种状态描述了一个线程的生命周期,其实这种状态码的定义在我们日常的业务开发中,也经常出现。比如:一个活动的提交、审核、拒绝、修改、通过、运行、关闭等,是类似的。那么线程的状态是通过下图的方式进行流转的,如图 20-1

    图 20-1 线程状态流转

    • New:新创建的一个线程,处于等待状态。
    • Runnable:可运行状态,并不是已经运行,具体的线程调度各操作系统决定。在 Runnable 中包含了 ReadyRunning 两个状态,当线程调用了 start() 方法后,线程则处于就绪 Ready 状态,等待操作系统分配 CPU 时间片,分配后则进入 Running 运行状态。此外当调用 yield() 方法后,只是谦让的允许当前线程让出CPU,但具体让不让不一定,由操作系统决定。如果让了,那么当前线程则会处于 Ready 状态继续竞争CPU,直至执行。
    • Timed_waiting:指定时间内让出CPU资源,此时线程不会被执行,也不会被系统调度,直到等待时间到期后才会被执行。下列方法都可以触发:Thread.sleepObject.waitThread.joinLockSupport.parkNanosLockSupport.parkUntil
    • Wating:可被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过 synchronized 获得锁,调用 wait 方法进入等待状态。最后通过 notify、notifyall 唤醒。下列方法都可以触发:Object.waitThread.joinLockSupport.park
    • Blocked:当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如 synchronized 锁,先获得的先执行,没有获得的进入阻塞状态。
    • Terminated:这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常。

    这里参考枚举State 类的英文注释了解了每一个状态码的含义,接下来我们去尝试操作线程方法,把这些状态体现出来。

    四、Thread 状态测试

    1. NEW

    Thread thread = new Thread(() -> {
    });
    System.out.println(thread.getState());
    
    // NEW
    • 这个状态很简单,就是线程创建还没有启动时就是这个状态。

    2. RUNNABLE

    Thread thread = new Thread(() -> {
    });
    // 启动
    thread.start();
    System.out.println(thread.getState());
    
    // RUNNABLE
    • 创建的线程启动后 start(),就会进入 RUNNABLE 状态。但此时并不一定在执行,而是说这个线程已经就绪,可以竞争 CPU 资源。

    3. BLOCKED

    Object obj = new Object();
    new Thread(() -> {
        synchronized (obj) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    
    Thread thread = new Thread(() -> {
        synchronized (obj) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    
    thread.start();
    while (true) {
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
    
    // BLOCKED
    // BLOCKED
    // BLOCKED
    • 这段代码稍微有点长,主要是为了让两个线程发生锁竞争。
    • 第一个线程,synchronized 获取锁后休眠,不释放锁。
    • 第二个线程,synchronized 获取不到锁,会被挂起。
    • 那么最后的输出结果就会是,BLOCKED

    4. WAITING

    Object obj = new Object();
    Thread thread = new Thread(() -> {
        synchronized (obj) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    
    thread.start();
    
    while (true) {
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
    
    // WAITING
    // WAITING
    // WAITING
    • 只要在 synchronized 代码块或者修饰的方法中,调用 wait 方法,又没有被 notify 就会进入 WAITING 状态。
    • 另外 Thread.join 源码中也是调用的 wait 方法,所以也会让线程进入等待状态。

    5. Timed_waiting

    Object obj = new Object();
    Thread thread = new Thread(() -> {
        synchronized (obj) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    thread.start();
    
    while (true) {
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
    
    // TIMED_WAITING
    // TIMED_WAITING
    // TIMED_WAITING
    • 有了上面状态获取的对比,这个状态的获取就没什么难度了。只要改成 Thread.sleep(100000); 就可以了。

    6. Terminated

    Thread thread = new Thread(() -> {
    });
    thread.start();
    
    System.out.println(thread.getState());
    System.out.println(thread.getState());
    System.out.println(thread.getState());
    
    // RUNNABLE
    // TERMINATED
    // TERMINATED
    • 这个就比较简单了,只要一个线程运行完,它的生命周期结束了,就进入了 TERMINATED 状态。

    五、Thread 方法使用

    一般情况下 Thread 中最常用的方法就是 start 启动,除此之外一些其他方法可能在平常的开发中用的不多,但这些方法在一些框架中却经常出现。因此只了解它们的概念,但是却缺少一些实例来参考! 接下来我们就来做一些案例来验证这些方法,包括:yield、wait、notify、join。

    1. yield

    yield 方法让出CPU,但不一定,一定让出!。这种可能会用在一些同时启动的线程中,按照优先级保证重要线程的执行,也可以是其他一些特殊的业务场景(例如这个线程内容很耗时,又不那么重要,可以放在后面)。

    为了验证这个方法,我们做一个例子:启动50个线程进行,每个线程都进行1000次的加和计算。其中10个线程会执行让出CPU操作。那么,如果让出CPU那10个线程的计算加和时间都比较长,说明确实在进行让出操作。

    案例代码

    private static volatile Map<String, AtomicInteger> count = new ConcurrentHashMap<>();
    static class Y implements Runnable {
        private String name;
        private boolean isYield;
        public Y(String name, boolean isYield) {
            this.name = name;
            this.isYield = isYield;
        }
        @Override
        public void run() {
            long l = System.currentTimeMillis();
            for (int i = 0; i < 1000; i++) {
                if (isYield) Thread.yield();
                AtomicInteger atomicInteger = count.get(name);
                if (null == atomicInteger) {
                    count.put(name, new AtomicInteger(1));
                    continue;
                }
                atomicInteger.addAndGet(1);
                count.put(name, atomicInteger);
            }
            System.out.println("线程编号:" + name + " 执行完成耗时:" + (System.currentTimeMillis() - l) + " (毫秒)" + (isYield ? "让出CPU----------------------" : "不让CPU"));
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            if (i < 10) {
                new Thread(new Y(String.valueOf(i), true)).start();
                continue;
            }
            new Thread(new Y(String.valueOf(i), false)).start();
        }
    }

    测试结果

    线程编号:10 执行完成耗时:2 (毫秒)不让CPU
    线程编号:11 执行完成耗时:2 (毫秒)不让CPU
    线程编号:15 执行完成耗时:1 (毫秒)不让CPU
    线程编号:14 执行完成耗时:1 (毫秒)不让CPU
    线程编号:19 执行完成耗时:1 (毫秒)不让CPU
    线程编号:18 执行完成耗时:1 (毫秒)不让CPU
    线程编号:22 执行完成耗时:0 (毫秒)不让CPU
    线程编号:26 执行完成耗时:0 (毫秒)不让CPU
    线程编号:27 执行完成耗时:1 (毫秒)不让CPU
    线程编号:30 执行完成耗时:0 (毫秒)不让CPU
    线程编号:31 执行完成耗时:0 (毫秒)不让CPU
    线程编号:34 执行完成耗时:1 (毫秒)不让CPU
    线程编号:12 执行完成耗时:1 (毫秒)不让CPU
    线程编号:16 执行完成耗时:1 (毫秒)不让CPU
    线程编号:13 执行完成耗时:1 (毫秒)不让CPU
    线程编号:17 执行完成耗时:1 (毫秒)不让CPU
    线程编号:20 执行完成耗时:0 (毫秒)不让CPU
    线程编号:23 执行完成耗时:0 (毫秒)不让CPU
    线程编号:21 执行完成耗时:0 (毫秒)不让CPU
    线程编号:25 执行完成耗时:1 (毫秒)不让CPU
    线程编号:24 执行完成耗时:1 (毫秒)不让CPU
    线程编号:28 执行完成耗时:0 (毫秒)不让CPU
    线程编号:38 执行完成耗时:0 (毫秒)不让CPU
    线程编号:39 执行完成耗时:0 (毫秒)不让CPU
    线程编号:37 执行完成耗时:1 (毫秒)不让CPU
    线程编号:40 执行完成耗时:0 (毫秒)不让CPU
    线程编号:44 执行完成耗时:0 (毫秒)不让CPU
    线程编号:36 执行完成耗时:1 (毫秒)不让CPU
    线程编号:42 执行完成耗时:1 (毫秒)不让CPU
    线程编号:45 执行完成耗时:1 (毫秒)不让CPU
    线程编号:43 执行完成耗时:1 (毫秒)不让CPU
    线程编号:46 执行完成耗时:0 (毫秒)不让CPU
    线程编号:47 执行完成耗时:0 (毫秒)不让CPU
    线程编号:35 执行完成耗时:0 (毫秒)不让CPU
    线程编号:33 执行完成耗时:0 (毫秒)不让CPU
    线程编号:32 执行完成耗时:0 (毫秒)不让CPU
    线程编号:41 执行完成耗时:0 (毫秒)不让CPU
    线程编号:48 执行完成耗时:1 (毫秒)不让CPU
    线程编号:6 执行完成耗时:15 (毫秒)让出CPU----------------------
    线程编号:7 执行完成耗时:15 (毫秒)让出CPU----------------------
    线程编号:49 执行完成耗时:2 (毫秒)不让CPU
    线程编号:29 执行完成耗时:1 (毫秒)不让CPU
    线程编号:2 执行完成耗时:17 (毫秒)让出CPU----------------------
    线程编号:1 执行完成耗时:11 (毫秒)让出CPU----------------------
    线程编号:4 执行完成耗时:15 (毫秒)让出CPU----------------------
    线程编号:8 执行完成耗时:12 (毫秒)让出CPU----------------------
    线程编号:5 执行完成耗时:12 (毫秒)让出CPU----------------------
    线程编号:9 执行完成耗时:12 (毫秒)让出CPU----------------------
    线程编号:0 执行完成耗时:21 (毫秒)让出CPU----------------------
    线程编号:3 执行完成耗时:21 (毫秒)让出CPU----------------------
    • 从测试结果可以看到,那些让出 CPU 的,执行完计算已经在10毫秒以上,说明我们的测试是效果的。

    2. wait & notify

    wait 和 notify/nofityall,是一对方法,有一个等待,就会有一个叫醒,否则程序就夯在那不动了。关于这部分会使用到的 synchronized 在之前小傅哥有深入的源码分析,讲到它是怎么加锁在对象头的,如果你忘记了可以翻翻看 《synchronized 解毒,剖析源码深度分析!》

    接下来我们模拟鹿鼎记·丽春院,清倌喝茶吟诗聊风月日常。当有达官贵人来时,需要分配清倌给大老爷。中间会有一些等待、叫醒操作。只为让你更好的记住这样的案例,不要想歪喽。清倌人即是只卖艺欢场人,喊麦的。

    案例代码

    public class 丽春院 {
    
        public static void main(String[] args) {
            老鸨 鸨子 = new 老鸨();
    
            清倌 miss = new 清倌(鸨子);
            客官 guest = new 客官(鸨子);
    
            Thread t_miss = new Thread(miss);
            Thread t_guest = new Thread(guest);
    
            t_miss.start();
            t_guest.start();
        }
    
    }
    
    class 清倌 implements Runnable {
    
        老鸨 鸨子;
    
        public 清倌(老鸨 鸨子) {
            this.鸨子 = 鸨子;
        }
    
        @Override
        public void run() {
            int i = 1;
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                if (i == 1) {
                    try {
                        鸨子.在岗清倌("苍田野子", "500 日元");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    try {
                        鸨子.在岗清倌("花田岗子", "800 日元");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                i = (i + 1) % 2;
            }
        }
    
    }
    
    class 客官 implements Runnable {
    
        老鸨 鸨子;
    
        public 客官(老鸨 鸨子) {
            this.鸨子 = 鸨子;
        }
    
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                try {
                    鸨子.喝茶吟诗聊风月();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
    }
    
    class 老鸨 {
    
        private String 清倌 = null;
        private String price = null;
        private boolean 工作状态 = true;
    
        public synchronized void 在岗清倌(String 清倌, String price) throws InterruptedException {
            if (!工作状态)
                wait();//等待
            this.清倌 = 清倌;
            this.price = price;
            工作状态 = false;
            notify();//叫醒
        }
    
        public synchronized void 喝茶吟诗聊风月() throws InterruptedException {
            if (工作状态)
                wait();//等待
            System.out.println("聊风月:" + 清倌);
            System.out.println("茶水费:" + price);
            System.out.println("  " + "  " + "  " + "  " + "  " + "  " + "  " + "  " + "  " + "  " + 清倌 + "完事" + "准备 ... ...");
            System.out.println("****************************************");
            工作状态 = true;
            notify();//叫醒
        }
    
    }

    测试结果

    聊风月:苍田野子
    茶水费:500 日元
                        苍田野子完事准备 ... ...
    ****************************************
    聊风月:花田岗子
    茶水费:800 日元
                        花田岗子完事准备 ... ...
    ****************************************
    聊风月:苍田野子
    茶水费:500 日元
                        苍田野子完事准备 ... ...
    ****************************************
    
    ...
    • 效果效果主要体现 wait、notify,这两个方法的使用。我相信你一定能记住这个例子!

    3. join

    join 是两个线程的合并吗?不是的!

    join 是让线程进入 wait ,当线程执行完毕后,会在JVM源码中找到,它执行完毕后,其实执行notify,也就是 等待叫醒 操作。

    源码jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp

    void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
        // Notify waiters on thread object. This has to be done after exit() is called
        // on the thread (if the thread is the last thread in a daemon ThreadGroup the
        // group should have the destroyed bit set before waiters are notified).
        ensure_join(this);
    }
    static void ensure_join(JavaThread* thread) {
      // 叫醒
      java_lang_Thread::set_thread(threadObj(), NULL);
      lock.notify_all(thread);
    }

    好的,就是这里!lock.notify_all(thread),执行到这,就对上了。

    案例代码

    Thread thread = new Thread(() -> {
        System.out.println("thread before");
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("thread after");
    });
    thread.start();
    System.out.println("main begin!");
    thread.join();
    System.out.println("main end!");

    测试结果

    main begin!
    thread before
    thread after
    main end!
    
    Process finished with exit code 0

    首先join() 是一个synchronized方法, 里面调用了wait(),这个过程的目的是让持有这个同步锁的线程进入等待,那么谁持有了这个同步锁呢?答案是主线程,因为主线程调用了threadA.join()方法,相当于在threadA.join()代码这块写了一个同步代码块,谁去执行了这段代码呢,是主线程,所以主线程被wait()了。然后在子线程threadA执行完毕之后,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,会继续执行。

    • 这部分验证的主要体现就是加了 thread.join() 后,会影响到输出结果。如果不加,main end! 会优先 thread after 提前打印出来。
    • join() 是一个 synchronized 方法,里面调用了 wait() 方法,让持有当前同步锁的线程进入等待状态,也就是主线程。当子线程执行完毕后,我们从源码中可以看到 JVM 调用了 lock.notify_all(thread) 所以唤醒了主线程继续执行。

    六、总结

    • 线程状态和状态的转换也是面试中必问的问题,但除了面试是我们自己在开发中,如果真的使用线程,是非常有必要了解线程状态是如何转换的。模模糊糊的使用,总会觉得担心,那么你是个好程序员!
    • 线程的一些深入学习都是在调用本地方法,也就是需要了解到JVM层面,才能更加深刻的见到c++代码是如何实现这部分逻辑的。
    • 在使用线程的时候一定要让自己有一个类似多核的脑子,线程一起、生死由你!本章节就扯到这了,很多的知识都是为了整套内容体系的全面,为后续介绍其他知识打下根基。感谢!

    七、系列推荐

    查看原文

    赞 10 收藏 7 评论 4

    小傅哥 发布了文章 · 2020-11-30

    北漂码农的我,把在大城市过成了屯子一样舒服,哈哈哈哈哈!


    作者:小傅哥
    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    东北老家,很久没回去了!

    可能是写代码改变生活吧!😄

    一家人从东北来到京津冀后,我自己基本很久没回去过了。上段时间小学同学路过我家门口,拍了张照片发给我,看后确实很怀念,嗯!很怀念!后面放假了,再回去转一转!不吹牛的讲,东北的烤串、麻辣烫最香!

    接下来讲讲关于我是自己北漂的故事,是怎么一步步把大城市的生活,过成屯子!

    二、🚇地铁坐不动了!

    🚇地铁坐不动了!

    在北京这个大城市,如果能过的像在县城里那样,还真的是挺舒服的!

    我可能本身不是一个特别喜欢大城市的人,尤其市中心那种上下班时:道路的嘈杂人流的拥挤还有通勤路途的遥远。有时候看似不太远的路,40公里、50公里,但放在我的老家可能就已经出门跑到榆树或者舒兰了!

    刚毕业上班时,我陆续租住过:立水桥房山长阳大瓦窑,找房子的核心目标就是便宜,哪里便宜就住哪里。最远的时候,单程上班差不多要2个小时(好在那时候6点就下班了),早年手机也没那么多流量,只能下载个电影充饥无聊!不敢快进,怕路上不够看!

    但从15年进入互联网企业后,就不行了,加班也多、下班也晚了、🛌睡眠也不够。就很难抗住每天早上6点起来坐地铁去,所以随着公司搬家,立马就搬家到公司身边了。从此过上没羞没臊的日子,18分钟走路上班!

    三、🚶走路上班很幸福!

    🚶走路上班很幸福!

    自从可以走路上班后,我膨胀了还买了一个电动车🛵,骑车上班3~5分钟就到了。周边的城乡世纪广场、南海子公园、马驹桥等等,几乎都可以抵达,但有时候回来也就没电了。

    更主要的是通勤时间短、通勤容易以后,就有了很多自己的时间。早上起床洗漱完也就7点多,再看看书、写写东西,或者刷一会视频还是蛮幸福的。有时候即使晚上加班9点、10点回来,也不会觉得特别累,如果回来的再早点基本就又有自己的时间可以搞点东西了。

    为了能把这份快乐延续下去,我还把第一个窝安在了廊坊(北京买不起),距离上班总部40公里。平时如果不加班也可以坐班车回家,还可以选择坐公交车,如果打车的话差不多50分钟可以到家。哎、可惜没有京牌,也没有在早期选择一个电动车牌。所以有的时候选择还是很重要的!

    不想生活只是活着,就还是需要奔着有目标的方向选择!

    四、🏃北漂落户天津!

    这可能是很多留不下北京,又回不去家乡的东北人,最佳选择吧。

    刚毕业时还真有机会拿北京户口,但被我放弃了!哈哈哈哈哈,我选择了多要几百块钱住宿补助!

    从决定落户到拿到户口本,用了将近3年时间。从17年找中介办理集体户口,准迁证、调档、迁入、户籍开通、拿集体户口个人页、办理新身份证、买房、等待交房、开不动产证明、办理个人户籍,终于在2020年,拿到了这个小红本!

    天津小红本

    虽然,落户了天津,但还不知道什么时候才会去天津!其实还是挺期待以后可以安静的在天津生活,做一些自己想做的事情。比如,开个码农会所,哈哈哈哈哈!

    五、🤔认知范围决定生活!

    • 如果我数学不好,没选择软件工程
    • 如果我毕业就业,没选择北京北漂
    • 如果我第一份工作,就想好了要拿北京户口
    • 如果我没选择从传统行业跳槽到互联网
    • 如果我放弃油车摇号,排队电动车
    • 等等...

    你现在的生活,基本是由你的认知范围决定,你的认知范围又是由你的知识储备支撑的。

    这就像我们夜晚都开车在高速上,虽然路一样宽,但你的灯不那么亮,他的灯亮。那么他看到的就多、看到的就远,他也就有更多的时间提前做出反应。而你是不可能已经错过了下高速的路口!

    有句话说(13年·我说的),人生其实没有选择,因为有些选项只是摆设!

    人生其实没有选择,因为有些选项只是摆设!

    之所以哪些看似更好的选择是摆设,是因为我们知识储备不足,所以视觉盲区就会很大。这就像你好似很费力的给人家讲一个道理,但换来的是喋喋不休的争吵。就像蚂蚱问孔子一年有三季,孔子说:三季一样。没有共同的认知,就没有必要争吵。

    沉淀、积累、破局,几乎是我们普通人突破赛道的唯一途径!

    😄好了,本期就扯到这咯!可能你我都有类似的人生经历,如果能给你一些借鉴,感谢点个赞、留个言,这样指不定某天我们就在天津碰面了!

    六、系列推荐

    查看原文

    赞 6 收藏 0 评论 4

    小傅哥 发布了文章 · 2020-11-26

    Thread.start() ,它是怎么让线程启动的呢?


    作者:小傅哥
    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    一、前言

    有句话:正因为你优秀,所以难以卓越!

    刚开始听这句话还在上学,既不卓越、也不优秀,甚至可能还有点笨!但突然从某次爬到班级的前几名后,开始喜欢上了这种感觉,原来前面的风景是如此灿烂😜!

    优秀和卓越差的不是一个等级,当你感觉自己优秀后,还能保持空瓶的心态开始,才能逐步的像卓越迈进,并漫漫长!

    是不小时候更容易学会更多的知识,但越大越笨了!人可能很容易被自己的年纪大了,当成长者。却很少能保持一个低姿态谦卑的心态,不断的学习。所以最后,放不下自己,也拾不起能力。

    喜欢一句话,蓝是天的颜色、红是火的象征,我不学大海抄袭天的蓝、也不学晚霞模拟火的红。我就是我,生命是我的、命运是我的健身也是你的、学习也是你的,只要你有一个好心态,自然会走到前面卓越那里!

    二、面试题

    谢飞机,小记!码德,年轻人写代码好猖狂,不遵守规范还喷我,你要耗子尾汁!谢飞机骂骂咧咧的下班后,找面试官聊心得。

    谢飞机:我感觉天天就像活在粪堆,代码都是乱糟糟,我有心无力!

    面试官:怎么,想跳槽了?

    谢飞机:想去写代码有规范的公司,想提升!

    面试官:嗯!确实,有些大公司的代码质量要好一些。但是你也要自身能力强的。

    谢飞机:是的,我一直在努力学习!准备跑路!

    面试官:那我顺便考你个题,看看你进大厂的几率大不。嗯... Java 线程如何启动的?

    谢飞机:如何启动的?start 启动的!

    面试官:还有吗?

    谢飞机:嗯...,没了!

    面试官:嗯,可能会与不会这一个题并不会让你代码有多牛、有多好,但是你的技术栈深度和广度,决定你的编程职业生涯是否有一条康庄大道。还是要多努力!

    三、线程启动分析

    new Thread(() -> {
        // todo
    }).start();

    咳咳,Java 的线程创建和启动非常简单,但如果问一个线程是怎么启动起来的往往并不清楚,甚至不知道为什么启动时是调用start(),而不是调用run()方法呢?

    那么,为了让大家有一个更直观的认知,我们先站在上帝视角。把这段 Java 的线程代码,到 JDK 方法使用,以及 JVM 的相应处理过程,展示给大家,以方便我们后续逐步分析。

    图 19-1 线程启动分析

    以上,就是一个线程启动的整体过程分析,会涉及到如下知识点:

    • 线程的启动会涉及到本地方法(JNI)的调用,也就是那部分 C++ 编写的代码。
    • JVM 的实现中会有不同操作系统对线程的统一处理,比如:Win、Linux、Unix。
    • 线程的启动会涉及到线程的生命周期状态(RUNNABLE),以及唤醒操作,所以最终会有回调操作。也就是调用我们的 run() 方法

    接下来,我们就开始逐步分析每一步源码的执行内容,从而了解线程启动过程。

    四、线程启动过程

    1. Thread start UML 图

    图 19-2 Thread start UML 图

    如图 19-2 是线程的启动过程时序图,整体的链路较长,会涉及到 JVM 的操作。核心源码如下:

    1. Thread.chttps://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/java.base/share/native/libjava/Thread.c
    2. jvm.cpphttps://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/prims/jvm.cpp
    3. thread.cpphttps://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp
    4. os.cpphttps://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/os.hpp
    5. os_linux.cpphttps://github.com/JetBrains/jdk8u_hotspot/blob/master/src/os/linux/vm/os_linux.cpp
    6. os_windows.cpphttps://github.com/JetBrains/jdk8u_hotspot/blob/master/src/os/windows/vm/os_windows.cpp
    7. vmSymbols.hpphttps://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/classfile/vmSymbols.hpp

    2. Java 层面 Thread 启动

    2.1 start() 方法

    new Thread(() -> {
        // todo
    }).start();
    
    // JDK 源码
    public synchronized void start() {
    
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
    
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {}
        }
    }
    • 线程启动方法 start(),在它的方法英文注释中已经把核心内容描述出来。Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread. 这段话的意思是:由 JVM 调用此线程的 run 方法,使线程开始执行。其实这就是一个 JVM 的回调过程,下文源码分析中会讲到
    • 另外 start() 是一个 synchronized 方法,但为了避免多次调用,在方法中会由线程状态判断。threadStatus != 0
    • group.add(this),是把当前线程加入到线程组,ThreadGroup。
    • start0(),是一个本地方法,通过 JNI 方式调用执行。这一步的操作才是启动线程的核心步骤。

    2.2 start0() 本地方法

    // 本地方法 start0
    private native void start0();
    
    // 注册本地方法
    public class Thread implements Runnable {
        /* Make sure registerNatives is the first thing <clinit> does. */
        private static native void registerNatives();
        static {
            registerNatives();
        }
        // ...
    }    
    • start0(),是一个本地方法,用于启动线程。
    • registerNatives(),这个方法是用于注册线程执行过程中需要的一些本地方法,比如:start0isAliveyieldsleepinterrupt0等。

    registerNatives,本地方法定义在 Thread.c 中,以下是定义的核心源码:

    static JNINativeMethod methods[] = {
        {"start0",           "()V",        (void *)&JVM_StartThread},
        {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
        {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
        {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
        {"resume0",          "()V",        (void *)&JVM_ResumeThread},
        {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
        {"yield",            "()V",        (void *)&JVM_Yield},
        {"sleep",            "(J)V",       (void *)&JVM_Sleep},
        {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
        {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
        {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
        {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
        {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
        {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
    };

    3. JVM 创建线程

    3.1 JVM_StartThread

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/prims/jvm.cpp

    JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
      JVMWrapper("JVM_StartThread");
      JavaThread *native_thread = NULL;
      
      // 创建线程
      native_thread = new JavaThread(&thread_entry, sz);
      // 启动线程
      Thread::start(native_thread);
    
    JVM_END
    • 这部分代码比较多,但核心内容主要是创建线程启动线程,另外 &thread_entry 也是一个方法,如下:

    thread_entry,线程入口

    static void thread_entry(JavaThread* thread, TRAPS) {
      HandleMark hm(THREAD);
      Handle obj(THREAD, thread->threadObj());
      JavaValue result(T_VOID);
      JavaCalls::call_virtual(&result,
                              obj,
                              KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                              vmSymbols::run_method_name(),
                              vmSymbols::void_method_signature(),
                              THREAD);
    }

    重点,在创建线程引入这个线程入口的方法时,thread_entry 中包括了 Java 的回调函数 JavaCalls::call_virtual。这个回调函数会由 JVM 调用。

    vmSymbols::run_method_name(),就是那个被回调的方法,源码如下:

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/classfile/vmSymbols.hpp

    #define VM_SYMBOLS_DO(template, do_alias)
    template(run_method_name, "run") 
    • 这个 run 就是我们的 Java 程序中会被调用的 run 方法。接下来我们继续按照代码执行链路,寻找到这个被回调的方法在什么时候调用的。

    3.2 JavaThread

    native_thread = new JavaThread(&thread_entry, sz);

    接下来,我们继续看 JavaThread 的源码执行内容。

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp

    JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
      Thread()
    #if INCLUDE_ALL_GCS
      , _satb_mark_queue(&_satb_mark_queue_set),
      _dirty_card_queue(&_dirty_card_queue_set)
    #endif // INCLUDE_ALL_GCS
    {
      if (TraceThreadEvents) {
        tty->print_cr("creating thread %p", this);
      }
      initialize();
      _jni_attach_state = _not_attaching_via_jni;
      set_entry_point(entry_point);
      // Create the native thread itself.
      // %note runtime_23
      os::ThreadType thr_type = os::java_thread;
      thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :os::java_thread;
      os::create_thread(this, thr_type, stack_sz);
    }
    • ThreadFunction entry_point,就是我们上面的 thread_entry 方法。
    • size_t stack_sz,表示进程中已有的线程个数。
    • 这两个参数,都会传递给 os::create_thread 方法,用于创建线程使用。

    3.3 os::create_thread

    源码

    众所周知,JVM 是个啥!,所以它的 OS 服务实现,Liunx 还有 Windows 等,都会实现线程的创建逻辑。这有点像适配器模式

    os_linux -> os::create_thread

    bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
      assert(thread->osthread() == NULL, "caller responsible");
    
      // Allocate the OSThread object
      OSThread* osthread = new OSThread(NULL, NULL);
      // Initial state is ALLOCATED but not INITIALIZED
      osthread->set_state(ALLOCATED);
      
      pthread_t tid;
      int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
    
      return true;
    }
    • osthread->set_state(ALLOCATED),初始化已分配的状态,但此时并没有初始化。
    • pthread_create,是类Unix操作系统(Unix、Linux、Mac OS X等)的创建线程的函数。
    • java_start,重点关注类,是实际创建线程的方法。

    3.4 java_start

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/os/linux/vm/os_linux.cpp

    static void *java_start(Thread *thread) {
    
      // 线程ID
      int pid = os::current_process_id();
    
      // 设置线程
      ThreadLocalStorage::set_thread(thread);
    
      // 设置线程状态:INITIALIZED 初始化完成
      osthread->set_state(INITIALIZED);
      
      // 唤醒所有线程
      sync->notify_all();
    
     // 循环,初始化状态,则一致等待 wait
     while (osthread->get_state() == INITIALIZED) {
        sync->wait(Mutex::_no_safepoint_check_flag);
     }
    
      // 等待唤醒后,执行 run 方法
      thread->run();
    
      return 0;
    }
    • JVM 设置线程状态,INITIALIZED 初始化完成。
    • sync->notify_all(),唤醒所有线程。
    • osthread->get_state() == INITIALIZED,while 循环等待
    • thread->run(),是等待线程唤醒后,也就是状态变更后,才能执行到。这在我们的线程执行UML图中,也有所体现

    4. JVM 启动线程

    JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
      JVMWrapper("JVM_StartThread");
      JavaThread *native_thread = NULL;
      
      // 创建线程
      native_thread = new JavaThread(&thread_entry, sz);
      // 启动线程
      Thread::start(native_thread);
    
    JVM_END
    • JVM_StartThread 中有两步,创建(new JavaThread)、启动(Thread::start)。创建的过程聊完了,接下来我们聊启动。

    4.1 Thread::start

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp

    void Thread::start(Thread* thread) {
      trace("start", thread);
    
      if (!DisableStartThread) {
        if (thread->is_Java_thread()) {
          java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                              java_lang_Thread::RUNNABLE);
        }
        // 不同的 OS 会有不同的启动代码逻辑
        os::start_thread(thread);
      }
    }
    • 如果没有禁用线程 DisableStartThread 并且是 Java 线程 thread->is_Java_thread(),那么设置线程状态为 RUNNABLE
    • os::start_thread(thread),调用线程启动方法。不同的 OS 会有不同的启动代码逻辑

    4.2 os::start_thread(thread)

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/os.hpp

    void os::start_thread(Thread* thread) {
      // guard suspend/resume
      MutexLockerEx ml(thread->SR_lock(), Mutex::_no_safepoint_check_flag);
      OSThread* osthread = thread->osthread();
      osthread->set_state(RUNNABLE);
      pd_start_thread(thread);
    }
    • osthread->set_state(RUNNABLE),设置线程状态 RUNNABLE
    • pd_start_thread(thread),启动线程,这个就由各个 OS 实现类,实现各自系统的启动方法了。比如,windows系统和Linux系统的代码是完全不同的。

    4.3 pd_start_thread(thread)

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/os/linux/vm/os_linux.cpp

    void os::pd_start_thread(Thread* thread) {
      OSThread * osthread = thread->osthread();
      assert(osthread->get_state() != INITIALIZED, "just checking");
      Monitor* sync_with_child = osthread->startThread_lock();
      MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
      sync_with_child->notify();
    }
    • 这部分代码 notify() 最关键,它可以唤醒线程。
    • 线程唤醒后,3.4 中的 thread->run(); 就可以继续执行了。

    5. JVM 线程回调

    5.1 thread->run()[JavaThread::run()]

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp

    // The first routine called by a new Java thread
    void JavaThread::run() {
      // ... 初始化线程操作
      
      thread_main_inner();
    }
    • os_linux.cpp 类中的 java_start 里的 thread->run(),最终调用的就是 thread.cpp 的 JavaThread::run() 方法。
    • 这部分还需要继续往下看,thread_main_inner(); 方法。

    5.2 thread_main_inner

    源码https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp

    void JavaThread::thread_main_inner() {
    
      if (!this->has_pending_exception() &&
          !java_lang_Thread::is_stillborn(this->threadObj())) {
        {
          ResourceMark rm(this);
          this->set_native_thread_name(this->get_thread_name());
        }
        HandleMark hm(this);
        this->entry_point()(this, this);
      }
    
      DTRACE_THREAD_PROBE(stop, this);
    
      this->exit(false);
      delete this;
    }
    • 这里有你熟悉的设置的线程名称,this->set_native_thread_name(this->get_thread_name())
    • this->entry_point(),实际调用的就是 3.1 中的 thread_entry 方法。
    • thread_entry,方法最终会调用到 JavaCalls::call_virtual 里的vmSymbols::run_method_name()。也就是 run() 方法,至此线程启动完成。终于串回来了!

    五、总结

    • 线程的启动过程涉及到了 JVM 的参与,所以如果没有认真了解过,确实很难从一个本地方法了解的如此透彻。
    • 整个源码分析可以结合着代码调用UML时序图进行学习,基本核心过程包括:Java 创建线程和启动调用本地方法 start0()JVM 中 JVM_StartThread 的创建和启动设置线程状态等待被唤醒根据不同的OS启动线程并唤醒最后回调 run() 方法启动 Java 线程
    • 有时候可能只是一步很简单的方法,也会有它的深入之处,当真的懂了以后,就不用死记硬背。如果需要获得以上高清大图,可以添加小傅哥微信(fustack),备注:Thread大图

    六、系列推荐

    查看原文

    赞 5 收藏 4 评论 0