1、线上系统突然挂掉

大家作为一个Java程序员,平时开发系统,测试系统,上线部署系统,为了公司拼命的加班,任劳任怨的干活,然后。。。平时最害怕的是个什么事情?想必不用我说,大家自己也知道了,就是出事故!很多大公司管事故叫做Case,如果系统一旦出一个事故,比如线上核心系统突然宕机不可用,然后导致几个小时内用户无法下订单,进而导致公司损失几百万,甚至几千万。
或者公司的某个单点登录系统突然不可用,所有用户无法登录APP,也导致无法下单。或者公司的缓存集群突然全面故障,然后导致公司的全部系统一起瘫痪。或者因为某个明星突然出轨,结果导致流量集中访问某台服务器,直接把数据库搞挂了。
凡此种种,都是重大的Case。一旦有Case,就会有程序员被拉出去祭天,这是网上常见的一个段子,是不是?其实祭天不至于那么夸张,但是一旦出了事故,总有人得去承担这个责任,去分析这个事故为什么会发生,谁的责任,后续如何改进。
所以,自己负责的线上系统,或者负责维护的缓存集群,或者负责维护的数据库集群,突然莫名其妙挂掉,不可用,导致公司核心业务流程彻底中断,这个就是程序员平时最害怕的事情。

2、最常遇到的故障:系统OOM

那么作为咱们Java程序员而言,先不考虑自己系统外部依赖的缓存、消息队列、数据库等等东西挂掉,就我们自己系统本身而言,最常见的挂掉的原因是什么?其实就是系统OOM,也就是所谓的内存溢出!其实说白了,也非常非常的简单,一句话形容,你的JVM内存就这么点,结果你拼命的往里面塞东西,结果内存塞不下了,不就直接溢出了吗?
一旦你的系统代码不停的往JVM内存里塞入大量的东西,JVM实在是放不下之后,JVM就会告诉你,OutOfMemory,内存溢出了,我实在放不下那么多东西了,我就直接瘫痪不能工作了。通常而言,内存溢出这个问题可能对你的系统是毁灭性的打击,他代表你的JVM内存不足以支撑你的代码的运行。
所以一旦发生这个情况,就会导致你的系统直接停止运转,甚至会导致你的JVM进程直接崩溃掉,进程都没了!这个时候对于线上看起来的场景就是,用户突然发现很奇怪,为什么点击APP、点击网页,都没反应了呢?然后大量的投诉和反馈给到客服,客服直接转移投诉给到运营,运营会直接反馈给技术人员。这个时候技术人员往往得知这个消息会直接目瞪口呆,最害怕的事情发生了,自己负责的线上系统居然挂掉了,今年的年终奖。。。也许是泡汤了,弄不好还得提前出去找工作去了。。。

3、运行一个Java系统就是运行一个JVM进程

首先的话呢,大家得先搞明白一个事情,就是我们平时说启动一个Java系统,其实本质就是启动一个JVM进程。
第一步就是这份“.java”源代码文件必须先编译成一个“.class”字节码文件,这个字节码文件才是可以运行的,如下图所示。接着对于这种编译好的字节码文件,比如HelloWorld.class,如果里面包含了main方法,接下来我们就可以用“java命令”来在命令行执行这个字节码文件了。实际上一旦你执行“java命令”,相当于就会启动一个JVM进程。这个JVM进程就会负责去执行你写好的那些代码,如下图所示。
所以首先要清楚第一点,运行一个Java系统,本质上就是启动一个JVM进程,这个JVM进程负责来执行你写好的一大堆代码。只要你的Java系统中包含一个main方法,接着JVM进程就会从你指定的这个main方法入手,开始执行你写的代码。

4、到底执行哪些代码:JVM得加载你写的类

大家都知道,Java是一个面向对象的语言,所以最最基本的代码组成单元就是一个一个的类,平时我们说写Java代码,不就是写一个一个的类吗?是不是。然后在一个一个的类里我们会定义各种变量,方法,数据结构,通过if else之类的语法,写出来各种各样的系统业务逻辑,这就是所谓的编程了。
所以JVM既然要执行你写的代码,首先当然得把你写好的类加载到内存里来啊!所以JVM的内存区域里大家都知道,有一块区域叫做永久代,当然JDK 1.8以后都叫做Metaspace了,我们也用最新的说法好了。这块内存区域就是用来存放你系统里的各种类的信息的,包括JDK自身内置的一些类的信息,都在这块区域里。
JVM有类加载器和一套类加载的机制,我们在专栏最开始的时候都说过了,这里不再赘述,他会负责把我们写好的类从编译好的“.class”字节码文件里加载到内存里来,如下图。
好,那么既然有这么一块Metaspace区域是用来存放类信息的,那是不是有可能在这个Metaspace区域里就会发生OOM?没错,是有这种可能的。
解决方案:分析GC日志了解系统是如何在多次GC之后无奈内存溢出的,分析内存快照了解是什么东西占据了太多的内存,然后区代码中找原因即可。

5、Java虚拟机栈:让线程执行各种方法

大家都知道,我们写好的那些Java代码虽然是一个一个的类,但是其实核心的代码逻辑一般都是封装在类里面的各种方法中的。比如JVM已经加载了我们写好的HelloWorld类到内存里了,接着怎么执行他里面的代码呢?Java语言中的一个通用的规则,就是一个JVM进程总是从main方法开始执行的,所以我们既然在HelloWorld中写了一个main()方法,那么当然得执行这个方法中的代码了。
但是等一等,JVM进程里的谁去执行main()方法的代码?其实我们所有的方法执行,都必须依赖JVM进程中的某个线程去执行,你可以理解为线程才是执行我们写的代码的核心主体。JVM进程启动之后默认就会有一个main线程,这个main线程就是专门负责执行main()方法的。
现在又有一个问题了,在main()方法里定义了一个局部变量,“message”,那么大家回忆一下,这些方法里的局部变量可能会有很多,那么这些局部变量是放在哪里的呢?很简单,每个线程都有一个自己的虚拟机栈,就是所谓的栈内存。然后这个线程只要执行一个方法,就会为方法创建一个栈桢,将栈桢放入自己的虚拟机栈里去,然后在这个栈桢里放入方法中定义的各种局部变量,如下图所示。好,现在问题来了,大家如果还记得之前我们讲过的一个参数,应该都知道,我们是可以设置JVM中每个线程的虚拟机栈的内存大小的,一般是设置为1MB。
那么既然每个线程的虚拟机栈的内存大小是固定的,是否可能会发生虚拟机栈的内存溢出?没错,所以第二块可能发生OOM的区域,就是每个线程的虚拟机栈内存。
解决方案:将异常日志写入本地日志文件,在日志中看异常信息,找到对应报错的方法,直接去代码中定位问题。

6、堆内存:放我们创建的各种对象

最后我们知道,我们写好的代码里,特别在一些方法中,可能会频繁的创建各种各样的对象,这些对象都是放在堆内存里的,如下图所示。而且我们通过之前的学习,也都知道了一点,通常我们在JVM中分配给堆内存的空间其实一般是固定的。
既然如此,我们还不停在堆内存里创建对象,是不是说明,堆内存也有可能会发生内存溢出?没错,第三块可能发生内存溢出的区域,就是堆内存空间!
解决方案:一个是必须在JVM参数中加入自动导出内存快照,一个是到线上看一下日志文件里的报错,如果是堆溢出,立马用MAT分析内存快照。MAT分析的时候,先看占用内存最多的对象是谁,然后分析那个线程的调用栈,接着就可以看到是哪个方法引发的内存溢出了。接着优化代码即可。