JVM内存模型包括:类加载器、执行引擎、本地方法库、运行时数据区
1.类加载器
JVM中类加载器会把 Java 代码转换成字节码,主要使用双亲委派机制实现类的加载,加载机制为:当前程序类-->扩展程序类-->根加载器(rz.jar)
类加载器分类:
- 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
- 扩展类加载器(Extension ClassLoader):负责加载libext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
- 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型:
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
2.执行引擎
class文件经过加载后会在方法区形成一个该类的一些数据结构(或者说是Java虚拟机规定的格式),然后执行引擎会在方法区中找到main方法code属性中的指令,并且去执行它。执行引擎会维护一个虚拟机栈,虚拟机栈中有栈帧,栈帧中维护者操作数栈、局部变量表和指向该栈帧所属的方法在运行时常量池中的引用
3.本地方法库
- 由于java早期出现是为了优化c++的一些冗余弊端,所以为了和c或者c++能够相通,java中使用了native关键字来定义一些不是使用java实现的方法,也就是现在的本地方法库。
- native:凡是带有native关键字的,说明java的作用范围达不到了,会去调用底层C语言的库!会进入本地方法栈,调用本地方法接口java nation implment(JNI)
- JNI的作用:扩展java的使用,融合不同的编程语言为java所用,最初:C、C++
4.运行时数据区(重点)
首先来看一个简单程序代码:
public class JVMModel { public void math(){ int a = 1; int b =2; int c = a+b; } public static void main(String[] args){ JVMModel jvmModel = new JVMModel(); jvmModel.math(); } }
运行时数据区执行内存模型为下图所示:
下面结合上边程序和模型图进行JVM内存模型分析
(1)程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
java虚拟机的多线程是通过线程流切换并分配CPU的时间片的方式实现的,因此在任何时刻一个处理器(如果是多核处理器,则只有一个核)都只会处理一个线程,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间计数器互相不影响,独立存储,因此这类内存区域为“线程私有”的内存。
由此可总结出程序计数器的作用;
(1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
(2)在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次执行位置。
*由于程序计数器是用来记录执行链地址的,所有生命周期随着线程是一致的,也就不存在OOM(OutOfMemoryError内存溢出)的区域了。
(2)栈(线程栈)
java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是java方法执行的内存模型,java虚拟机栈是由一个个栈帧组成,线程在执行一个方法时,便会向栈中放入一个栈帧,每个栈帧都拥有局部变量表、操作数栈、动态链接、方法出口信息。局部变量表主要存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)和对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其它与此对象相关的位置)。
Java虚拟机会出现以下两种异常:
(1)StackOverFlowError:若Java虚拟机的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度,就会抛出StackOverFlowError;
(2)OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈的内存用完了,无法再动态扩展了,此时就会抛出OutOfMemoryError;
*Java虚拟机栈也是线程私有的,其生命周期是随着线程的创建而创建,随着线程死亡而死亡。
(3)本地方法栈
本地方法栈的起源是由于java出现早期,java很多方法都是使用C编写的(Java是基于C发展的,过渡期),所以在JVM内存中针对这部分方法开辟了本地方法栈。
和虚拟机所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、出口信息。
*本地方法栈同样也会出现StackOverFlowError和OutOfMemoryError异常
(4)堆
堆是java虚拟机中所管理的内存中最大的一块,java堆是所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。几乎所有的对象实例以及数组都在这里分配内存(目前由于编译器的优化,对象在堆上分配已经没有那么绝对了。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:其中新生代又分为:Eden空间、From Survivor、To Survivor空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。从内存分配的角度来看,线程共享的java堆中可能会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
如图所示,JVM内存主要由新生代、老年代、永久代构成。
① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。
② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。
③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
*在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
(5) 方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
*相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
(6) 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
(7)直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。