JVM内存结构
在实际工作中,对于有一定经验的程序员来说,jvm的调优是必须掌握的知识,在了解各种命令和调优手段之前,首先必须的清楚jvm的内存结构,因为jvm调优必须要求开放人员对内存结构有一定的了解,对于OutOfMemoryError等异常知道原因,下面我们就来介绍一下jvm的内存结构。
先来看一张图,了解下jvm内存结构的整体布局
jvm的内存结构主要分为3大块:堆内存,方法区和栈,堆内存是jvm中最大的一块由年轻代和老年代组成。
年轻代分为Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;老年代是通过调整年轻代和堆内存来间接控制的。
方法区是存储类的信息,常量和静态变量,线程共享的,为了和堆内存区分,称为Non-Heap(非堆)。
栈分为java虚拟机栈和本地方法栈,用于方法的执行;每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
下面通过一张图来了解参数控制各内存区域的大小
控制参数
-
-Xms设置堆的最小空间大小。
-
-Xmx设置堆的最大空间大小。
-
-Xmn设置年轻代的空间的大小
-
-XX:NewSize设置新生代最小空间大小。
-
-XX:MaxNewSize设置新生代最大空间大小。
-
-XX:PermSize设置永久代最小空间大小。
-
-XX:MaxPermSize设置永久代最大空间大小。
-
-Xss设置每个线程的堆栈大小。
-Xmn 和 -XX:Newsize
前者是设置堆中新生代大小。后者是设置新生代初始大小
-Xmn 是将NewSize与MaxNewSize设为一致。
下面对各个区域详细介绍下
Java堆(Heap)
是jvm管理内存最大的一块,是线程共享的,虚拟机启动创建时,唯一目的是方法对象实例,几乎所有的对象实例都在这里分配内存。
java堆是垃圾收集器管理的主要区域,因此也被称为GC堆,垃圾回收采用的分代收集算法,所以堆分为年轻代,老年代,年轻代还可以细分为Eden空间、From Survivor空间、To Survivor空间等;根据java虚拟机规范规定,java堆可以是物理上不连续的内存空间,逻辑上连续就行;通过-Xmx和-Xms控制。
new创建的对象和数组,是存放在堆中的,用完之后靠垃圾回收机制不定期自动清除。
如果堆中没有内存完成分配,或者不可扩展,就会抛出OutOfMemoryError异常。
方法区(Method Area)
与堆一样,是线程共享的内存区域,它用于存储已经被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据
当方法区无法满足内存分配时,将抛出OutOfMemoryError异常。
JVM栈(JVM Stacks)
线程私有,生命周期和线程相同,虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用至执行完毕的过程,就是一个栈帧在虚拟机中从入栈到出栈的过程;栈中的数据用完就消失。
局部变量存放编译器可知的各种基本数据类型(byte,int,boolean,char,short,float,double,long),对象引用。
java虚拟机规范中,如果请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;动态扩展时没有足够的内存也会抛出这个异常。(用递归的时候需要注意下,如果可能,使用尾递归)
哪儿的OutOfMemoryError
Exception in thread “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>. Out of swap space?
原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。
Exception in thread “main”: java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)
原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现
下面讲解下堆和栈的区别
栈是运行单位,堆是存储单位
堆(对象):
引用类型的变量,其内存分配在堆上或者常量池(字符串常量,基本数据类型常量),需要通过new等方式创建。
堆内存的主要作用是存放运行时new创建的对象。(主要用于存放对象,存取速度慢,可以运行时动态分配内存,生存期不需要提前确定)。
栈(基本数据类型变量,对象的引用变量):
基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放。
栈内存的主要作用是存放基本数据类型和引用变量。栈的内存管理是通过栈的"后进先出"模式来实现的。(主要用来执行程序,存取速度快,大小和生存期必须确定,缺乏灵活性)
来看看如何创建对象、创建对象的过程:
创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。但对象不是完全由构造器来负责创建的,实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了---这些是在构造器执行之前就完成的,也就是说,当系统开始执行构造器的执行体之前,系统已经创建了一个对象,只是这对象还不能被外部程序访问,只能在构造器中通过this来引用。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,通常还会赋给另外一个引用类型的变量,从而让外部程序可以访问。(当然可以通过设置构造器的访问权限private,阻止其他类创建该类的实例)
下图是堆和栈关联的图
下面来看段代码:
public static void main(String[] args) {
Test t = new Test();
Test t1 = new Test();
System.out.println(t == t1);
t = t1;
System.out.println(t == t1);
}
输出:false true
原因是因为t和t1变量是分别new的,对象在堆中分配的内存地址是不一样的,地址指向t,t1后也是不一样的,然后将t1赋值给t后,说白了是把t1对应的堆中的内存地址指向t,这样t对应的堆中的对象就没有引用,垃圾回收器会自动回收;t的地址变为t1,所以t和t1后面才会相等。
注:以上有些内容来自于网上,自己搜集整理,加上自己的理解。
下一篇: 数据结构——平衡二叉树