1、JVM内存结构、JMM内存模型、Java对象模型的区别?
这3个概念很容易混淆,这里做一下简单分析,本文仅对JVM内存结构做详细介绍,JMM内存模型和Java对象模型仅做简单介绍,后面的章节再做详细介绍。总的来说,这三个概念如下:
- JVM内存结构:和Java虚拟机的运行时区域有关;
- Java内存模型:和Java的并发编程有关;
- Java对象模型:和Java对象在虚拟机中的表现形式有关。
1.1 JVM内存结构
本文重点介绍的内容,JVM内存结构由Java虚拟机规范定义,描述的是Java程序在执行过程中,JVM管理的不同的数据区域,每个区域存储特定类型的数据,具体见第2节。1.2 JMM内存模型
JMM
全称:Java Memory Model
,JMM
内存模型并不像Java
内存结构那样是真实存储在的,JMM
只是一个抽象的概念。Java内存结构中的堆区和方法区是多个线程共享的数据区域,即多个线程可能同时操作保存在堆或者方法区中的同一个数据。Java
的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM
就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM
定义了一些语法集,这些语法集映射到Java
语言中就是volatile
、synchronized
等关键字。
在JMM
中,我们把多个线程间通信的共享内存称之为主内存,而在并发编程中多个线程都维护了一个自己的本地内存(这是个抽象概念),本地内存中保存的数据是主内存中的数据拷贝。而JMM
主要是控制本地内存和主内存之间的数据交互的,如下图所示:1.3 Java对象模型
Java
对象在JVM
中的存储也是有一定的结构的,而这个关于Java
对象自身的存储模型称之为Java
对象模型。HotSpot
虚拟机中,设计了一个OOP-Klass Model
,OOP(Ordinary Object Pointer)
指的是普通对象指针,而Klass
用来描述对象实例的具体类型。
在OOP-Klass Model
模型中,每一个Java
类在被JVM
加载的时候,JVM
会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM
层表示该Java
类。当我们在Java
代码中使用new
关键字创建一个对象的时候,JVM
会创建一个instanceOopDesc
对象,这个对象中包含了对象头以及实例数据。OOP-Klass
模型如下图所示:
2、JVM内存结构
Java
代码是运行在虚拟机上的,而虚拟机在执行Java
程序的过程中会把管理的内存划分为若干个不同的数据区域,这些区域存储的数据类型不同,都有各自的用途。其中有些区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。在Java
虚拟机规范中,定义了5种运行时数据区,分别是Java
堆、方法区、虚拟机栈区、本地方法栈区、程序计数器,其中Java
堆和方法区是线程共享的,剩下的3个区域是每个线程各自维护的。
JVM内存结构如下图所示:
2.1 Java堆(Heap)
对于大多数应用来说,Java
堆(Java Heap
)是Java
虚拟机所管理的内存中最大的一块。Java
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
根据Java
虚拟机规范的规定,Java
堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。Java
堆既可以实现成固定大小,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx
和-Xms
控制)。Java
堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC
堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java
堆中还可以细分为:新生代和老年代,其中新生代再细致一点划分还可以分成Eden
空间、From Survivor
空间、To Survivor
空间等,具体如下图所示:
我们可以通过JVM
启动参数来控制堆空间分配的区间,通过以下3个参数控制:
**-Xms**
:JVM
启动时申请的初始Heap
值,默认为操作系统物理内存的1/64;**-Xmx**
:JVM
可申请的最大Heap
值,默认值为物理内存的1/4;**-Xmn**
:设置新生代的内存大小,-Xmn
是将NewSize
与MaxNewSize
设为一致,我们也可以分别设置这两个参数。
如果在Java
堆中没有内存完成实例分配, 并且堆也无法再扩展时, Java
虚拟机将会抛出OutOfMemoryError
异常。
关于新生代中Eden
、From Survivor
、To Survivor
区的划分见JVM GC那篇文章:
2.2 方法区(Method Area)
方法区(Method Area
)与Java
堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、字符串常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap
(非堆),目的应该是与Java
堆区分开来。
类信息包括:
- 类型的完整有效名;
- 类的修饰符(public、abstract、final);
- 域(Field)信息;
- 方法(Method)信息;
- 除常量外的所有静态(static)变量。
运行时常量池(Runtime Constant Pool
)也是方法区的一部分,Class
文件中除了有类的版本、 字段、 方法、 接口等描述信息外, 还有一项信息是常量池表(Constant Pool Table
), 用于存放编译期生成的各种字面量与符号引用, 这部分内容将在类加载后存放到方法区的运行时常量池中。 常量池的作用是为了避免频繁的创建和销毁对象而影响系统性能,其实现了常量对象(比如常量字符串)的共享。
2.2.1 永久代(Permanent Generation)
永久代和方法区的关系是什么呢?方法区是Java虚拟机规范中定义的一种逻辑概念,而永久代是对方法区的实现。
在 JDK1.8
之前,方法区也被称作为永久代(Permanent Generation
),JDK1.8
之后移除了永久代。根据官方描述,永久代主要存储了三种数据:
- 类元数据(Class Metadata):即类的版本、字段、方法、接口等信息;
- Interned Strings:字符串常量池中的引用指向的字符串对象;
- 类静态变量。
移除永久代后,Interned Strings和类静态变量被移到了堆中,类元数据被移到了元空间MetaSpace里。
2.2.2 元空间(MetaSpace)
元空间在JDK1.8移除永久代后被引入,用来代替永久代,本质和永久代类似,都是对方法区的实现。元空间和永久代的最大区别在于:元空间并不在虚拟机中,而是使用的本地内存(Native Memory)。元空间主要用于存储类元数据。
我们也可以通过设置参数来控制 Metaspace
的空间大小,主要有以下几个命令:
**-XX:MetaspaceSize**
:分配给类元数据空间(以字节计)的初始大小。MetaspaceSize
的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大;**-XX:MaxMetaspaceSize**
: 分配给类元数据空间的最大值,超过此值就会触发**Full GC**
,此值默认没有限制,但应取决于系统内存的大小,JVM
会动态地改变此值。**-XX:MinMetaspaceFreeRatio**
:表示一次GC
以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。**-XX:MaxMetaspaceFreeRatio**
:表示一次GC
以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。
为什么采用元空间来取代永久代?
- 为了解决永久代的OOM问题,元数据和class对象存放在永久代中,容易出现性能问题和内存溢出;
- 类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,太小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。而存放在元空间(即本地内存)中,存储空间相对充足,不用太过关心分配的内存大小;
-
2.3 程序计数器(Program Counter Register)
程序计数器(
Program Counter Register
)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。程序计数器是唯一一个在Java
虚拟机规范中没有规定任何OutOfMemoryError
情况的内存空间。2.4 Java虚拟机栈(JVM Stacks)
Java
虚拟机栈(Java Virtual Machine Stacks
)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java
方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame
)的数据结构,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法从被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,过程如下图所示:
虚拟机栈帧中我们更多关心的是存储在栈帧中的局部变量表,局部变量表里主要存放基本类型的数据,以及对象的引用。
我们实际开发过程中程序抛异常时都会打印堆栈信息,里面会有很多方法的层层调用,这部分信息就是Java虚拟机栈的信息。在Java虚拟机规范中,对Java虚拟机栈区域规定了两类异常状况: 如果线程请求的栈深度大于虚拟机允许的深度,将抛出
StackOverflowError
异常(比如递归调用方法时没有终止返回条件无限递归);- 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,将抛出
OutOfMemoryError
异常。注意:HotSpot
虚拟机的栈容量是不可以动态扩展的,因此在HotSpot
虚拟机中是不会由于虚拟机栈无法扩展而导致OutOfMemoryError
异常的,只要线程申请栈空间成功就不会出现OOM
,但是如果申请失败仍然是会有OOM
异常的。2.5 本地方法栈(Native Method Stacks)
本地方法栈(Native Method Stacks
)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java
方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native
方法服务。所谓Native
方法,是指jre
类库中,底层调用的非Java
实现的方法,比如c
等实现的方法,举个例子:JUC
包下的原子类,底层的unsafe
类中的方法就是c
实现的。2.6 其他
2.6.1 字符串常量池
HotSpot VM里的字符串常量池(StringTable)是一个哈希表,全局仅有一份,被所有的类共享。字符串常量池存放的不是字符串对象实例本身,而是字符串对象的引用。关于指向的字符串对象实例,JDK 6及之前是存放在永久代里,从JDK 7之后存放在堆里。
字符串常量池没有存放在永久代、也没存放在方法区、更没有存放在堆里,字符串常量池是存放在本地内存(Native Memory,即你物理机的8g、16g内存)里。
字符串常量池的作用是避免字符串常量对象反复创建和销毁而影响性能,实现对象的共享。2.6.2 直接内存
除了上面介绍的虚拟机运行时的数据区以外,还有一部分内存也被频繁使用,它不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它是直接内存。直接内存的分配不受Java堆大小的限制,但是会受到服务器总的物理内存的限制。
在JDK 1.4中引入的NIO中,引入了一种基于Channel和Buffer的I/O方式,他可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象来操作这块直接内存。2.7 各种OutOfMemoryError异常
Java
内存结构中抛出OutOfMemoryError
异常的场景有如下:
原因:对象不能被分配到堆内存中。Exception inthread “main”:java.lang.OutOfMemoryError:Java heap space
Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space
原因:类或者方法不能被加载到永久代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库。
Exception in thread “main”: java.lang.OutOfMemoryError :Requested array size exceeds VM limit
原因:创建的数组大于堆内存的空间。
Exception in thread “main”: java.lang.OutOfMemoryError: request <size>bytes for<reason>.Outof swap space?
原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。
Exception in thread “main”: java.lang.OutOfMemoryError: <reason> stack trace>(Nativemethod)
原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现。
参考
jvm系列(二):JVM内存结构
5 分钟给你讲明白JVM内存结构 和 Java内存模型 和 Java对象模型
这一次,我终于弄清楚了JVM 内存结构
深入理解java虚拟机——java内存结构之虚拟机栈(JVM Stack)线程私有区域如何调度方法
JVM中的堆的新生代、老年代、永久代详解
面试必问的 JVM 运行时数据区,你懂了吗?