JVM内存管理概述

1.概述

java不在需要开发人员显示的分配内存和回收内存,而是由JVM自动管理内存的分配和回收(又称为垃圾回收-GC),这简化了编程难度,但同时可能使得程序员在不知不觉中浪费了很多内存,导致JVM花费很多时间进行垃圾回收。另外还有可能由于不清楚JVM的内存分配和回收机制造成内存泄露。最终导致JVM内存不够用。

2.内存区域分类

在JVM规范中,将内存空间分为:方法区(METHOD AREA)、(HEAP)、本地方法栈(NATIV METHOD STACK)、PC寄存器(PROGRAM COUNTER REGISTER)、JAVA栈(JAVA STACK)。

JVM内存管理概述

1.方法区(全局共享)

方法去是堆的一个组成部分,他主要存储的是运行时的常量池、静态字段信息、方法信息、构造方法、普通函数的字节码以及一些特殊方法。当虚拟机装载某个类文件时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。由于运行类该类信息会直接加载到内存,所以都是相对较早进入内存中的。方法区的堆和和常规对象存储堆有所区别。部分虚拟机规范不强制要求自动实现内存管理系统,即便有也是放置在永久代的堆中。

运行时常量池:在JVM规范中是这样定义运行时常量池这个数据结构的:Runtime Constant Pool代表运行时每个class文件的常量表。它包含几种常量:编译期的数字常量、方法和域的引用(在运行时解析)。它的功能类似于传统编程语言的符号表,尽管它包含的数据比典型的符号表要丰富得多。每个Runtime Constant Pool都是在JVM的Method area中分配的,每个Class或者Interface的Constant Pool都是在JVM创建class或接口时创建的。它是属于方法区的一部分,所以它的存储也受方法区的规范约束,如果常量池无法分配,同样会抛出OutOfMemoryError。

2.堆(全局共享)

Java堆是java虚拟机所管理内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动创建。此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在此分配内存。Java堆可以处理物理上不连续的内存空间,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMenoryError异常。因此Java堆是垃圾收集管理器(GC)的主要区域。JAVA堆中的内存释放是不受开发人员控制的,完全由JAVA虚拟机一手操办。对于JAVA虚拟机如何实现垃圾搜集器,JAVA虚拟机规范没有明确的规定,也正因如此,我们平时使用的JAVA虚拟机中提供了许多种垃圾搜集器,它们采用不同的算法以及实现方式,已满足多方面的性能需求。

JVM内存管理概述

年轻代(Young Generation)
大多数情况下Java程序中新建的对象都是从新生代分配内存的(注意,大对象可以直接在老年代分配)。新生代由Eden Space和两块相同大小的Survivor Space(通常又称为S0和S1或From和To)构成。而Eden区采用的复制回收算法,此算法在存活对象比例很少的情况下非常高效。当年轻代需要回收时会触发Minor GC(也称作Young GC)。

老年代(Old Generation)
老年代用于存放在年轻代中经多次垃圾回收仍然存活的对象,可以理解为比较老一点的对象,例如缓存对象;新建的对象也有可能在老年代上直接分配内存,这主要有两种情况:一种为大对象,另一种为大的数组对象,且数组对象中无引用外部对象。当老年代满了的时候就需要对老年代进行垃圾回收,老年代的垃圾回收称作Major GC(也称作Full GC)。

内存堆分配配置:

-Xmx :来表示堆的最大大小。默认为物理内存的1/4但小于1GB。

-Xms :来表示堆的最小大小。默认为物理内存的1/64但小于1GB。

-XX:MinHeapFreeRatio=”比例” 当空余堆内存大于”比例”(默认70%)时,JVM会减小到-Xms制定的大小。

-XX:MaxHeapFreeRatio=”比例” 当空余堆内存小于“比例”(默认40%)时,JVM会增大Heap到-Xmx指定的大小。

为了避免频繁的调整Heap的大小。通常将-Xms和-Xmx大小设为一致。

-Xmn :参数来指定新生代的大小。

-XX:SurvivorRadio 调整Eden Space和Survivor Space大小。

-Xss :每个线程可使用的内存大小。

和 -Xms两个选项来控制大小,Xmx,表示初始大小。
-Xmm参数来指定新生代的大小。

3.本地方法栈(线程独有)

本地方法栈是一个传统的栈,它用来支持native方法的执行。如果JAVA虚拟机是使用的其它语言实现指令集解释器的时候,也会用到本地方法栈。如果前面这两种都未发生,也就是说如果JAVA虚拟机不依赖于本地方法栈,而且JAVA虚拟机也不支持native方法,则不需要本地方法栈。而如果需要的话,则本地方法栈也是随每一个线程的启动而创建的。

4.PC寄存器(线程独有)

程序计数器是一个比较小的内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

5.JAVA虚拟机栈(线程独有):

JAVA虚拟机栈是在创建线程的同时创建的,用于存储栈帧,JAVA虚拟机栈也是线程独有的。

栈帧:简单点说,可以解释为是一个方法运行时,临时数据的存储区域,具体点说,它里面包括了数据和部分的过程结果,与此同时,它又肩负着处理方法返回值、动态链接以及异常分派的任务。栈帧是随着方法的创建而创建,随着方法的结束而销毁,如果方法抛出异常,也算方法结束。然而在每一个栈帧中,都有着自己的局部变量表以及操作数栈以及对当前类的运行时常量池的引用。

局部变量表:它是一个方法局部变量的列表,是在编译时期就写入了class文件当中。简单的理解,可以将它理解为一个对象数组,而里面按照索引0到length-1分别对应于每一个局部变量,特别的,如果是实例方法的局部变量表,第0个局部变量会是一个指向当前实例的引用,也就是this关键字,其余的局部变量则从索引1开始。

操作数栈:它是一个后进先出(LIFO)栈,而它的长度也是在编译时期就写入了class文件当中,是固定的。它的作用就是提供字节码指令操作变量计算的空间,比如简单的,对于int a=9这句话来说,就需要先将9压入操作数栈,再将9赋给a这个变量。