1.Java虚拟机内存管理
1.1.运行时数据区[Runtime Data Area]
1.1.1.线程共享区
1.1.1.1.Java堆[heap]
1.1.1.1.1.新生代、老年代、Eden区域分配
1.1.1.1.2.年轻代(Young Generation)
1.1.1.1.3.老年代(Old Generation)
1.1.1.2.方法区[Method Area]
1.1.2.线程独占区
1.1.2.1.虚拟机栈[VM Stack]
1.1.2.2.本地方法栈[Native Method stack]
1.1.2.3.程序计数器[Program Counter Register]
1.1.3.执行引擎
1.Java虚拟机内存管理
JVM = 类加载器(classloader)+执行引擎(execution engine)+运行时数据区域(runtime data area)
再如网上的一个图:
方法区和堆是所有线程共享的:
1.1.运行时数据区[Runtime Data Area]
Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
1.1.1.线程共享区
1.1.1.1.Java堆[heap]
存放对象实例。
垃圾收集器管理的主要区域。
新生代,老年代,Eden空间。
为java内存管理的最大一块儿区域:
堆不够出现:OutOfMemory
主要的参数:-Xmx -Xms
被所有线程共享,在虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例都在这里分配内存。对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做**”GC堆”**。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代又有Eden空间、From Survivor空间、To Survivor空间三部分。Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError异常。
对于堆区大小,可以通过参数-Xms和-Xmx来控制,-Xms为JVM启动时申请的最小heap内存,默认为物理内存的1/64但小于1GB;-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1GB,默认当剩余堆空间小于40%时,JVM会增大Heap到-Xmx大小,可通过-XX:MinHeapFreeRadio参数来控制这个比例;当空余堆内存大于70%时,JVM会减少Heap大小到-Xms指定大小,可通过-XX:MaxHeapFreeRatio来指定这个比例。对于系统而言,为了避免在运行期间频繁的调整Heap大小,我们通常将-Xms和-Xmx设置成一样。
1.1.1.1.1.新生代、老年代、Eden区域分配
Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分为三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。
堆的内存模型大致为:
从图中可以看出:堆大小=新生代+老年代。其中,堆的大小可以通过参数-Xms、-Xmx来指定。
以JDK1.6为例:
默认的,新生代(Young)与老年代(Old)的比例的值为1:2(该值可以通过-XX:NewRatio来指定),即:新生代(Young)=1/3的堆空间大小。老年代(Old)=2/3的堆空间大小。其中,新生代(Young)被细分为Eden和两个Survivor区域,这两个Survivor区域分别被命名为from和to,以示区分。Eden空间不足的时候,会把存活的对象转移到Survivor中。
新生代大小可以由-Xmn来控制,默认的Eden: from: to = 8:1:1(可以通过参数-XX:SurvivorRatio来设定),即:Eden = 8/10的新生代空间大小,from = to = 1/10的新生代空间大小。
JVM每次只会使用Eden和其中的一块Survivor区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。
因此,新生代实际可用的内存空间为9/10 (即90%)的新生代空间。
1.1.1.1.2.年轻代(Young Generation)
对象在被创建时,内存首先是在年轻代进行分配(注意,大对象可以直接在老年代分配)。当年轻代需要回收时会触发Minor GC(也称作Young GC)。
年轻代由Eden Space和两块相同大小的Survivor Space(又称S0和S1)构成,可通过-Xmn参数来调整新生代大小,也可通过-XX:SurvivorRatio来调整Eden Space和Survivor Space大小。不同的GC方式会按不同的方式来按比值划分Eden Space和Survivor Space,有些GC方式还会根据运行状况来动态调整Eden、S0、S1的大小。
年轻代的Eden区内存是连续的,所以其分配会非常快;同样Eden区的回收也非常快(因为大部分情况下Eden区对象存活时间非常短,而Eden区采用的复制回收算法,此算法在存活比例很少的情况下非常高效,后面会详细介绍)。
如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:java Heap Space异常。
1.1.1.1.3.老年代(Old Generation)
老年代用于存放在年轻代中经多次垃圾回收仍然存活的对象,可以理解为比较老一点的对象,例如缓存对象;新建的对象也有可能在老年代上直接分配内存,这主要有两种情况:一种为大对象,可以通过启动参数设置-XX:PretenureSizeThreshold=1024,表示超过多大时就不在年轻代分配,而是直接在老年代分配。此参数在年轻代采用Parallel Scavenge GC时无效,因为其会根据运行情况自己决定什么对象直接在老年代上分配内存;另一种为大的数组对象,且数组对象中无引用外部对象。
当老年代满了的时候就需要对老年代进行垃圾回收,老年代的垃圾回收称作Major GC(也称作Full GC)。
老年代所占用的内存大小为-Xmx对应的值减去-Xmn对应的值。
1.1.1.2.方法区[Method Area]
方法区存放了要加载的类的信息(如类名,修饰符)、运行时常量池、已被虚拟机加载的类信息、final定义的常量、属性(类中的field)和方法信息、静态变量、即时编译后的代码等数据。
当开发人员调用类对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是全局共享的,在一定条件下它也会被GC。当方法区使用的内存超过它允许的大小时,就会抛出OutOfMemory:PermGen Space异常。
在Hotspot虚拟机中,这块区域对应的是Permanent Generation(持久代),一般的,方法区上执行的垃圾收集是很少的,因此方法区又被称为持久代的原因之一,但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。在方法区上进行垃圾收集,条件苛刻而且相当困难,关于其回后面再介绍。
**运行时常量池(Runtime Constant Pool)**是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量,比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址。
JVM方法区的相关参数,最小值:–XX:PermSize;最大值–XX:MaxPermSize。
JVM用永久代(Permanet Generation)来存放方法区。可以通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。
和Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError异常。对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot虚拟机把它当成永久代(Permanent Generation)来进行垃圾回收。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫”非堆”。运行时常量池(Runtime Constant Pool)运行时常量池是方法区的一部分。Class文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。除了在编译期生成的常量,还允许动态生成,例如 String 类的intern()。这部分常量也会被放入运行时常量池。
注:在JDK1.7之前,HotSpot使用永久代实现方法区;HotSpot使用 GC分代实现方法区带来了很大便利;从JDK1.7开始HotSpot开始移除永久代。其中符号引用(Symbols)被移动到Native Heap中,字符串常量和类引用被移动到Java Heap中。在 JDK1.8 中,永久代已完全被元空间(Meatspace)所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。在JDK 1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native堆中来回复制数据。
1.1.2.线程独占区
1.1.2.1.虚拟机栈[VM Stack]
线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型),它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和return Address类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。该区域可能抛出以下异常:
当线程请求的栈深度超过最大值,会抛出StackOverflowError异常;栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError异常。
存放方法运行时所需的数据,成为栈帧。
- 虚拟机栈描述的是Java方法执行的动态内存模型。
- 栈帧
每个方法执行,都会创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表,操作数栈,动态链接,方法出口等。
方法执行的时候入栈,方法执行完成时出栈:
局部变量表
- 存放编译器可知的各种基本数据类型,引用类型,return Address类型。
- 局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小。
大小
- StackOverflowError
- OutOfMemory
案例:
package com.toto.jvm;
public class StackTest {
private void test() {
System.out.println("方法执行...");
test();
}
public static void main(String[] args) {
new StackTest().test();
}
}
方法执行...
方法执行...
方法执行...
方法执行...
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.ext.DoubleByte$Encoder.encodeLoop(Unknown Source)
at java.nio.charset.CharsetEncoder.encode(Unknown Source)
at sun.nio.cs.StreamEncoder.implWrite(Unknown Source)
at sun.nio.cs.StreamEncoder.write(Unknown Source)
at java.io.OutputStreamWriter.write(Unknown Source)
at java.io.BufferedWriter.flushBuffer(Unknown Source)
at java.io.PrintStream.write(Unknown Source)
at java.io.PrintStream.print(Unknown Source)
at java.io.PrintStream.println(Unknown Source)
at com.toto.jvm.StackTest.test(StackTest.java:6)
at com.toto.jvm.StackTest.test(StackTest.java:7)
虚拟机栈占用的是操作系统内存,每个线程都对应着一个虚拟机栈,它是线程私有的,而且分配非常高效。一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
1.1.2.2.本地方法栈[Native Method stack]
HotSport虚拟机不区分,虚拟机栈和本地方法栈。
虚拟机栈为虚拟机执行Java方法服务。
本地方法栈为虚拟机执行native方法服务。
为JVM所调用到的Native即本地方法服务。
与虚拟机栈非常相似,其区别不过是虚拟机栈执行Java方法(也就是字节码)服务,而本地方法栈则是为了虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
本地方法栈用于支持native方法的执行,存储了每个native方法调用的状态。本地方法栈和虚拟机方法栈运行机制一致,它们唯一的区别就是,虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。
1.1.2.3.程序计数器[Program Counter Register]
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
程序计数器处于线程独占区。
如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个计数器的值为undefined。
此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
行号计数器如下:
记录当前线程所执行到的字节码的行号。
线程私有,它的生命周期与线程相同。可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、跳转、异常处理、线程恢复(多线程切换)等基础功能。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(undefined)。程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以此区域不会出现OutOfMemoryError的情况。
程序计数器是一个比较小的内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
1.1.3.执行引擎
即时编译器[JITCompiler]
垃圾收集[Garbage Collection]
1.1.4.本地库接口[java Native Interface]
1.1.5.本地方法库
1.1.6.类加载器子系统[classloader subsystem]
转载:https://blog.csdn.net/toto1297488504/article/details/117390513