JVM1.7内存模型
JVM1.8内存模型
JVM数据区异常
运行区域 | 异常 | 主要原因 |
虚拟机栈和本地方法栈 | StackOverflowErrorOutOfMemoryError | StackOverflowError:线程请求的栈深度大于虚拟机所允许的最大深度; OutOfMemoryError:虚拟机在扩展栈时无法申请足够的内存空间 |
程序计数器 | 无 | 无 |
堆 | OutOfMemoryError | 对象数量到达最大堆的容量,内存泄漏、内存溢出 |
方法区和运行时常量池 | OutOfMemoryError | 反射,动态代理:CGLib、JSP、OSGI |
最后在说两个概念:
内存泄露(Memory Leak):程序在申请内存后,对象没有被GC所回收,它始终占用内存,内存泄漏的堆积最终会造成内存溢出。
内存溢出(Memory Overflow):程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出。
JVM1.7与1.8的区别
一、方法区变化
1.8同1.7比,最大的差别就是:元数据区取代了永久代
- 就是JDK8没有了PermSize相关的参数配置了。
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现
- 元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存
1)方法区与永久代的区别?
方法区只是JVM规范定义,而永久代为具体的实现,元空间也是方法区在jdk1.8中的一种实现。
2)为什么废除永久代?
- 官方文档:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
- PermGen很难调整,PermGen中类的元数据信息在每次FullGC的时候可能被收集,但成绩很难令人满意。
- 而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class总数,常量池的大小,方法的大小等。
- 并且永久代内存经常不够用发生内存泄露。
JVM内存模型解说
程序计数器
当前线程所执行的字节码地址指令存放与指示;
本地方法栈
为虚拟机使用到native(操作系统)方法服务;
java虚拟机栈
又称java栈,Java栈是Java方法执行的内存模型。每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。栈也是线程私有的
1)、局部变量表
就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
2)、操作数栈
想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
3)、指向运行时常量池的引用
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
4)、方法返回地址
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。也就解释了栈是线程私有的。
当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。在这个区域规定了两种异常状况:
如果线程请求的栈深入大于虚拟机所允许的深度,将抛出StackOverFlowError异常!
如果虚拟机栈可以动态扩展,当扩展到无法申请内存到足够的内存,就会抛出OutOfMemoryError异常!
Java堆
堆是jvm内存管理的最大的一块区域,此内存区域的唯一目的就是存放对象的实例,所有对象实例与数组都要在堆上分配内存。它也是垃圾收集器的主要管理区域。java对可以处于物理上不连续的空间,只要逻辑上是连续的即可。线程共享的区域。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出OutOfMemoryError异常。
为了支持垃圾收集,堆被分为三个部分:
- 年轻代 : 常常又被划分为Eden区和Survivor(From Survivor To Survivor)区(Eden空间、From Survivor空间、To Survivor空间(空间分配比例是8:1:1)
- 老年代
- 永久代 (jdk 8已移除永久代,下面会讲解)
(1) 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
(2) Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
(3) TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
(4) 所有新创建的Object 都将会存储在新生代Yong Generation中。如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到OldGeneration。新的Object总是创建在Eden Space。
这些知识在后面学习GC和内存调优方面非常重要。
方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的.class文件等。方法区是堆的一个逻辑部分,为了区分Java堆,它还有一个别名Non-Heap(非堆)。相对而言,GC对于这个区域的收集是很少出现的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
在Java 7及之前版本,我们也习惯称方法区它为“永久代”(Permanent Generation),更确切来说,应该是“HotSpot使用永久代实现了方法区”!