JVM内存区域
JVM内存区域
一 方法区
1 什么是方法区
方法区,也称非堆(Non-Heap),又是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field等元数据对象、static-final常量、static变量、jit编译器编译后的代码等数据,。另外,方法区包含了一个特殊的区域“运行时常量池”
方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
方法区用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
在老版jdk,方法区也被称为永久代。
不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。
在jdk1.8中,方法区已经不存在
2 方法区的实现
方法区的实现,虚拟机规范中并未明确规定,目前有2种比较主流的实现方式:
(1)HotSpot虚拟机1.7-:在JDK1.6及之前版本,HotSpot使用“永久代(permanent generation)”的概念作为实现,即将GC分代收集扩展至方法区。这种实现比较偷懒,可以不必为方法区编写专门的内存管理,但带来的后果是容易碰到内存溢出的问题(因为永久代有-XX:MaxPermSize的上限)。在JDK1.7+之后,HotSpot逐渐改变方法区的实现方式,如1.7版本移除了方法区中的字符串常量池。
(2)HotSpot虚拟机1.8+:1.8版本中移除了方法区并使用metaspace(元数据空间)作为替代实现。metaspace占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。但这不意味着我们不对方法区进行限制,如果方法区无限膨胀,最终会导致系统崩溃。
我们思考一个问题,为什么使用“永久代”并将GC分代收集扩展至方法区这种实现方式不好,会导致OOM?首先要明白方法区的内存回收目标是什么,方法区存储了类的元数据信息和各种常量,它的内存回收目标理应当是对这些类型的卸载和常量的回收。但由于这些数据被类的实例引用,卸载条件变得复杂且严格,回收不当会导致堆中的类实例失去元数据信息和常量信息。因此,回收方法区内存不是一件简单高效的事情,往往GC在做无用功。另外随着应用规模的变大,各种框架的引入,尤其是使用了字节码生成技术的框架,会导致方法区内存占用越来越大,最终OOM。
3 去永久代的原因有:
(1)字符串存在永久代中,容易出现性能问题和内存溢出。
(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
二 堆
1 什么是堆
堆是用于存放对象的内存区域。因此,它是垃圾收集器(GC)管理的主要目标。其具有以下特点:
- 堆在逻辑上划分为“新生代”和“老年代”。由于JAVA中的对象大部分是朝生夕灭,还有一小部分能够长期的驻留在内存中,为了对这两种对象进行最有效的回收,将堆划分为新生代和老年代,并且执行不同的回收策略。不同的垃圾收集器对这2个逻辑区域的回收机制不尽相同,在后续的章节中我们将详细的讲解。
- 堆占用的内存并不要求物理连续,只需要逻辑连续即可。
- 堆一般实现成可扩展内存大小,使用“-Xms”与“-Xmx”控制堆的最小与最大内存,扩展动作交由虚拟机执行。但由于该行为比较消耗性能,因此一般将堆的最大最小内存设为相等。
- 堆是所有线程共享的内存区域,因此每个线程都可以拿到堆上的同一个对象。
- 堆的生命周期是随着虚拟机的启动而创建。
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
是 Java 虚拟机所管理的内存中最大的一块,也被称为 “GC堆”,是被所有线程共享的一块内存区域,在虚拟机启动时被创建
堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。
- 唯一目的就是储存对象实例和数组(JDK7 已把字符串常量池和类静态变量移动到 Java 堆),几乎所有的对象实例都会存储在堆中分配。随着 JIT 编译器发展,逃逸分析、栈上分配、标量替换等优化技术导致并不是所有对象都会在堆上分配。
- Java 堆是垃圾收集器管理的主要区域。堆内存分为新生代 (Young) 和老年代 (Old) ,新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。
-
新生代(Young): 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。新生代又可细分为Eden空间、From Survivor空间、To Survivor空间,默认比例为8:1:1。它们的具体作用将在下一篇文章讲解GC时介绍。
新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,
新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。
-
老年代(Tenured/Old):在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
用于存放新生代中经过多次垃圾回收仍然存活的对象
-
永久代(Perm):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
2 堆异常
当堆无法分配对象内存且无法再扩展时,会抛出OutOfMemoryError异常。
一般来说,堆无法分配对象时会进行一次GC,如果GC后仍然无法分配对象,才会报内存耗尽的错误。
3 新生代的GC:
新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未 使用的空间中.
对应于新生代:就是在Eden和其中一个Survivor,复制到另一个之间Survivor空间中,然后清理掉原来就是在Eden和其中一个Survivor中的对象。
新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代。
4 老年代的GC:
旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行 GC(SerialMSC)、并行GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进一步深入研究
堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),
即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小
三 程序计数器
1 什么是程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
- 记录的是正在执行的虚拟机字节码指令的地址,可以看成是当前线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器,各条线程的程序计数器互不影响,独立存储,这类内存区域成为“线程私有的内存”。
- 此内存区域是唯一在虚拟机规范中没有OutOfMemoryError的情况的区域
2 程序计数器的特点
1.线程隔离性,每个线程工作时都有属于自己的独立计数器。
2.执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址。
3.执行native本地方法时,程序计数器的值为空(Undefined)。
4.程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
5.程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。
3 程序计数器作用
Java程序从源文件创建到程序运行要经过两大步骤:
1. 源文件由编译器编译成字节码(ByteCode)。
2. 字节码由java虚拟机解释运行。
字节码解释器工作时就是通过改变这个 程序计数器 的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
四 虚拟机栈( JVM 执行java 方法)
1 什么是虚拟机栈
虚拟机栈是线程私有的,它的生命周期与线程相同。
Java虚拟机栈描述的是Java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧,它用于存储局部变量、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
每一条Java虚拟机都有自己私有的Java虚拟机栈,这个栈与线程同时创建,用于存储栈帧。
2 Java虚拟机栈的作用,就是用于存储局部变量与一些过程结果的地方。
另外,它在方法调用和返回中也扮演了重要的角色。因为除了栈帧的出栈和入栈之外,Java虚拟机栈不会再受其他因素的影响。
Java 虚拟机规范允许 Java 虚拟机栈被实现成固定大小的或者是根据计算动态扩展和收缩的。
如果采用固定大小的 Java 虚拟机栈设计,那每一条线程的 Java 虚拟机栈容量应当在线程创建的时候独立地选定。
Java 虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩 Java 虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
3 栈中的内存区域是连续的,有大小限制的,如果超过了就会抛出栈溢出的异常StackOverflowError。
在每个方法执行的时候,都会创建一个个栈帧,用于保存局部变量,操作数栈、动态链表等信息。
每次方法的调用都会对应着一个栈帧,因此可以解释有我们在写递归程序的时候不小心报栈溢出的异常,因为栈时有限的,方法调用太多次导致栈帧存满,所以溢出。
4 启动线程的的时候,Java虚拟机会为该线程创建新的Java虚拟机栈。
Java虚拟机栈将线程的状态存储在栈中。
5 Java虚拟机在Java虚拟机栈中只执行两个操作:入栈和出栈。
当线程调用Java方法时,虚拟机会创建一个新栈帧并将其压入当前线程的Java虚拟机栈中。
然后这个新栈帧就成为了当前栈帧。
当方法执行的时,使用该栈帧来存储参数、局部变量、中间计算和其他数据。
方法可以两种方式(之一)完成;正常完成、异常完成。
无论是正常还是突然,Java虚拟机都会弹出并丢弃当前方法的栈帧;然后,先前方法的栈帧变为当前帧
五 本地方法栈(JVM 执行本地方法) HotSpot
1.Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法(一般非Java实现的方法)的调用
2.本地方法栈,也是线程私有的。
3.允许被实现成固定或者是可动态拓展的内存大小。(和Java虚拟机栈在内存溢出方面情况是相同的)
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么java虚拟机将会抛出一个OutOfMemoryError异常。
4.本地方法是使用C语言实现的
5.它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
-
Java 方法调用
- 本地方法栈是一个后入先出(Last In First Out)栈。
- 由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
- 本地方法栈会抛出 StackOverflowError 和 OutOfMemoryError 异常。
6.当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限
本地方法可以通过本地方法接口来 访问虚拟机内部的运行时数据区
它甚至可以直接使用本地处理器中的寄存器
直接从本地内存的堆中分配任意数量的内存
7.并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
-
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
➢本地方法可以通过本地方法接来访问虚拟机内部的运行时数据区。
➢它甚至可以直接使用本地处理器中的寄存器
➢直接从本地内存的堆中分配任意数量的内存。
-
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
-
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
六 直接内存(Direct Memory )
堆外内存,又被称为直接内存。这部分内存不是由jvm管理和回收的。需要我们手动的回收。
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
-
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
-
配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常
直接内存(堆外内存)与堆内存比较
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
- 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
直接内存的优点:
1:对于频繁的io操作,我们需要不断把内存中的对象复制到直接内存。然后由操作系统直接写入磁盘或者读出磁盘。
这时候用到直接内存就减少了堆的内外内存来回复制的操作。
2:我们在运行程序的过程中可能需要新建大量对象,对于一些声明周期比较短的对象,可以采用对象池的方式。但
是对于一些生命周期较长的对象来说,不需要频繁调用gc,为了节省gc的开销,直接内存是必备之选。
3:扩大程序运行的内存,由于jvm申请的内存有限,这时候可以通过堆外内存来扩大内存。
使用对外内存的原因:
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
- 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
posted on 2020-08-21 17:24 shumeigang 阅读(178) 评论(0) 收藏 举报
浙公网安备 33010602011771号