JAVA虚拟机在执行JAVA程序的时候,会把它管理的内存分成若干不同的数据区域,每个区域都有各自的用途。目前大致把JVM内存模型划分为五个区域:程序计数器,虚拟机栈,本地方法栈,堆和方法区。
程序计数器
程序计数器(ProgramCounterRegister)是当前线程所执行的字节码的行号指示器。这句话理解起来有点拗口,打个比方,我看书看到一半的时候突然接到领导电话”XX啊,线上那个项目出BUG了,赶紧来公司加个班解决!“,这时书没看完啊怎么办?我会在当前页夹个书签,以便下次再看的时候接着上次看的地方往下读。而程序计数器就是这样一个作用。我们的CPU多线程处理能力有限,常规的CPU也就是4核8线程,表示同一时间段最多能同时处理8个线程。而我们的程序往往是几百上千个线程在跑。所以不得不采用线程之间来回切换的形式来执行程序。程序计数器就是线程的”书签“,用来记录当前线程执行到方法的哪一步,以便下次线程切回来的时候从上次执行的内存地址继续执行。程序计数器是线程私有的,各个线程之间的计数器互不影响,独立存储。程序计数器也是唯一在Java 虚拟机规范中没有规定任何OutOfMemoryError 的区域。
ps:JVM还有个东西叫方法计数器,是用来记录方法执行次数的,用于JIT。当方法执行次数达到阈值的时候,JVM会判定该方法为热点方法,从而将该方法编译为机器码,从而提高执行效率,两者概念别搞混淆了。
虚拟机栈
虚拟机栈(Java Virtual Machine Stacks)与程序计数器一样,也是线程私有的,它的生命周期与线程相同。我们JAVA程序中的所有线程都被它管理。线程是什么?网上这种概念一找一大堆,我的理解很简单,线程就是方法的执行者,java程序中所有方法只能被线程执行。一个用户请求过来就创建了一个线程,一直到请求回应这个线程生命也走到了尽头。该请求在我们的服务端执行了哪些操作都是在这个线程中实现的,线程每执行一个方法就会创建一个栈帧,栈帧用来存储当前方法的局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从被调用至返回的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度(如循环嵌套/死循环),将抛出StackOverflowError 异常;如果虚拟机栈无法申请到足够的内存时会抛出OutOfMemoryError 异常。ps:普通线程大概消耗1M左右的内存,如果项目中线程数过多也会导致在该区域内存溢出,抛出OutOfMemoryError 异常。所以项目中能用线程池就用线程池限制和维护线程数量。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈非常类似,只不过虚拟机栈为虚拟机执行Java 方法服务,而本地方法栈则是为虚拟机使用到的native 方法服务。我们看JDK源码的时候经常看到有的方法前面有native修饰,这些都是本地方法,由非JAVA语言实现。
示例如下
package java.lang; public class Object{ private static native void registerNatives(); ... ... public final native Class<?> getClass(); public native int hashCode(); protected native Object clone() throws CloneNotSupportedException; public final native void notify(); ... }
与 虚拟机栈类似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError 异常。以前项目中遇到过这种场景,遍历获取服务器某个目录下面所有文件夹和文件的时候抛出OutOfMemoryError 异常,提示是native方法报错。这时候别被native误导,native方法轻易不抛异常,就算抛异常也是我们打开方式有误,结合场景推测应该是有大文件在该目录下从而导致内存溢出,然后果然找到了大文件。
Java 堆
JAVA堆(Heap)是JVM内存管理中区域最大一块,也是GC垃圾回收器最活跃的区域,我们所有对象的生命周期都在堆里面。由于堆里面所有数据都是对虚拟机栈中所有线程共享,所以会造成并发编程的时候线程不安全的问题,这个我们先不讨论。现代GC主要采用的是分代回收的策略,将我们的堆主要划分为新生代(Eden区、From Survivor区和To Survivor区)和老年代。新生代就像炼狱,里面的对象朝生暮死,每分每秒都在煎熬,熬不下去就game over被扔到GC的销毁队列挨个销毁,熬下去了就跑到老年代去颐享天年。JVM默认配置一个对象如果经历了15次GC回收都还存活的话,就转移到老年代,特殊的大对象(如数组)除外。根据Java 虚拟机规范的规定,当JAVA堆无法满足内存分配需求时,将会抛出OutOfMemoryError 异常。ps:老年代不会轻易GC,但是老年代空间有限的情况下如果空间满了,则会促使GC来次大扫除--FULL GC,FULL GC是非常影响性能的,因为在执行FULL GC的时候,其他所有线程都不得不停下来等待,也就是所谓的STOP THE WORLD,一个好的JVM配置,基本不会出现 FULL GC的情况。
方法区
方法区(Method Area)存放虚拟机加载的类信息,静态变量,常量等数据。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。同堆一样,方法区也是对所有线程共享的。 JDK1.8之前,大家习惯于把方法区称之为”永久代“,这是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展到了方法区,但是为了跟堆区分出来又取了一个别名叫非堆。1.8以后用”元空间“取代了永久代的概念,元空间不再是jvm内存的一部分,而是直接在于本机内存中。而将常量池移到堆中。
希望这篇文章能给大家一些提示。