作为 Java 程序员而言,先不考虑自己系统外部依赖的缓存、消息队列、数据库等等东西挂掉,就我们自己系统本身而言,最常见的挂掉的原因是什么?
其实就是系统OOM,也就是所谓的内存溢出!
什么是内存溢出?在哪些区域会发生内存溢出?
运行一个 Java 系统就是运行一个JVM进程
首先的话呢,大家得先搞明白一个事情,就是我们平时说启动一个Java系统,其实本质就是启动一个JVM进程。
咱们就用最最基本的情况来给大家演示一下好了,比如说下面的一段代码,是每个Java初学者都会写的一段代码:
那么大家知道,当你在Eclipse或者Intellij IDEA中写好这个代码,然后通过IDE来运行这个代码的时候,会发生哪些事情吗?
首先,我们专栏最早的几篇文章就给大家说过,我们写好的代码他都是后缀为“.java”的源代码,这个代码是不能运行的。
所以第一步就是这份“.java”源代码文件必须先编译成一个“.class”字节码文件,这个字节码文件才是可以运行的,如下图所示。
接着对于这种编译好的字节码文件,比如HelloWorld.class,如果里面包含了main方法,接下来我们就可以用“java命令”来在命令行执行这个字节码文件了
实际上一旦你执行“java命令”,相当于就会启动一个JVM进程。这个JVM进程就会负责去执行你写好的那些代码,如下图所示。
所以首先要清楚第一点,运行一个Java系统,本质上就是启动一个JVM进程,这个JVM进程负责来执行你写好的一大堆代码。只要你的Java系统中包含一个main方法,接着JVM进程就会从你指定的这个main方法入手,开始执行你写的代码。
到底执行哪些代码:JVM得加载你写的类
Java是一个面向对象的语言,所以最最基本的代码组成单元就是一个一个的类,平时我们说写Java代码,不就是写一个一个的类吗?是不是。
然后在一个一个的类里我们会定义各种变量,方法,数据结构,通过if else之类的语法,写出来各种各样的系统业务逻辑,这就是所谓的编程了。
所以JVM既然要执行你写的代码,首先当然得把你写好的类加载到内存里来啊!
所以JVM的内存区域里大家都知道,有一块区域叫做永久代,当然JDK 1.8以后都叫做Metaspace了,我们也用最新的说法好了。
这块内存区域就是用来存放你系统里的各种类的信息的,包括JDK自身内置的一些类的信息,都在这块区域里。
JVM有类加载器和一套类加载的机制,我们在专栏最开始的时候都说过了,这里不再赘述,他会负责把我们写好的类从编译好的“.class”字节码文件里加载到内存里来,如下图。
好,那么既然有这么一块Metaspace区域是用来存放类信息的,那是不是有可能在这个Metaspace区域里就会发生OOM?
没错,是有这种可能的。
Java虚拟机栈:让线程执行各种方法
大家都知道,我们写好的那些Java代码虽然是一个一个的类,但是其实核心的代码逻辑一般都是封装在类里面的各种方法中的
比如JVM已经加载了我们写好的HelloWorld类到内存里了,接着怎么执行他里面的代码呢?
Java语言中的一个通用的规则,就是一个JVM进程总是从main方法开始执行的,所以我们既然在HelloWorld中写了一个main()方法,那么当然得执行这个方法中的代码了。
但是等一等,JVM进程里的谁去执行main()方法的代码?
其实我们所有的方法执行,都必须依赖JVM进程中的某个线程去执行,你可以理解为线程才是执行我们写的代码的核心主体。
JVM进程启动之后默认就会有一个main线程,这个main线程就是专门负责执行main()方法的。
大家如下图所示。
现在又有一个问题了,在main()方法里定义了一个局部变量,“message”,那么大家回忆一下,这些方法里的局部变量可能会有很多,那么这些局部变量是放在哪里的呢?
很简单,每个线程都有一个自己的虚拟机栈,就是所谓的栈内存。
然后这个线程只要执行一个方法,就会为方法创建一个栈桢,将栈桢放入自己的虚拟机栈里去,然后在这个栈桢里放入方法中定义的各种局部变量,如下图所示
好,现在问题来了,大家如果还记得之前我们讲过的一个参数,应该都知道,我们是可以设置JVM中每个线程的虚拟机栈的内存大小的,一般是设置为1MB。
那么既然每个线程的虚拟机栈的内存大小是固定的,是否可能会发生虚拟机栈的内存溢出?
没错,所以第二块可能发生OOM的区域,就是每个线程的虚拟机栈内存。
堆内存:放我们创建的各种对象
最后我们知道,我们写好的代码里,特别在一些方法中,可能会频繁的创建各种各样的对象,这些对象都是放在堆内存里的,如下图所示。
而且我们通过之前的学习,也都知道了一点,通常我们在JVM中分配给堆内存的空间其实一般是固定的
既然如此,我们还不停在堆内存里创建对象,是不是说明,堆内存也有可能会发生内存溢出?
没错,第三块可能发生内存溢出的区域,就是堆内存空间!