您的位置:首页 > 编程语言 > Java开发

Java内存区域及溢出

2016-08-23 16:20 316 查看
Java内存管理是Java的特色,也是码码时容易遇到的坑。搞清楚内存管理的实现有助于我们写出更好的代码。

本着博主一贯博采众长的原则,博主找了不少博文,发现在Java内存管理方面内容都很接近,大体都是《深入理解Java虚拟机》第二章的主要内容。本文就在此书章节的基础上,从浅到深把Java内存管理梳理一遍,争取能有更清晰的展现,自己的理解不敢有,现在还没到那个水平,等以后吧……

粗看堆栈

Java的运行时数据区域,简单的分类就是堆和栈,堆中存放类的对象以及数组,栈中存放局部变量、对象引用等。堆和栈他们各自的特性又是啥呢?参考《Think in Java》的讲解:

栈的存储速度快,仅次于寄存器,且数据可以共享但是存在栈中的数据的生存期必须是确定的,缺乏灵活性。

堆存储可以动态分配内存,分配和清理比栈耗时,但是存储的数据编译器无需管他存活多久,灵活性好。正好堆中的对象要由垃圾回收器负责回收,因此大小和生命周期不需要确定

一般我们就是理解到这个粒度的Java堆栈,可能再了解下常量池。差不多就是新手的知识范畴。 JAVA中的数据存储(堆及堆栈),大家可以参考这篇文章的几个例子加深下理解。

细看堆栈

相对于上述对内存管理的浅显理解,JVM总是带着一层天生的高大上光环。我们先看一张结构图(某种意义上,虚框1的部分对应于上文的堆,虚框2对应于上文的栈概念,看了下文会知道这样理解不准确,但为了由粗到细的过度不妨先这样认为):



Java虚拟机运行时数据区大概分为这几个部分:

程序计数器(PC):学过计算机体系结构的同学大概都知道它大概是干嘛用的,在JVM中,PC指向当前线程执行字节码的行号。每个线程都有自己独立的PC,互不影响,独立存储。图中用白色矩形框标识为线程私有。

虚拟机栈:基本包含了上文对栈的粗糙理解,但不紧紧如此。每个方法执行时会创建一个栈帧用于存储局部变量表、操作数、动态链接、方法出口等(可以理解成执行一次方法用到的相关信息)。同样虚拟机栈也是线程私有的。

本地方法栈:它和虚拟机栈的区别就在于,虚拟机栈为执行Java方法服务,而本地方法栈为虚拟机调用Native方法服务。同虚拟机栈,它也是线程私有的。

堆:堆是JVM管理的内存中最大的一块,它唯一的目的就是存放对象实例:所有的对象和素组都在堆上分配。从内存回收的角度看,堆可以再被分为新生代、老年代。

新生代又可以被分为三个区:Eden空间、From Survivor空间、To Survivor空间。程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区的对象占用空间满了时,此区存活的对象又被复制到另外一个Survivor区,当这个Survivor区也满的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到年老代。

年老代存放的是上面年轻代复制过来的对象,也就是在年轻代中还存活的对象,并且区满了复制过来的。一般来说,年老代中的对象生命周期都比较长。

无所谓如何划分堆,里面存放的都是对象,进一步划分是为了更好的分配、回收内存。

方法区:存储JVM加载的类信息、常量、静态变量、即时编译后产生的代码等数据。有一种JVM的实现将方法区作为永久代和堆实现到了一起,如图



方法区/永久代没必要实现gc,可能在对常量池的回收和类型的卸载方面有一定必要,但是往往效果堪忧。

上述五个区域除了程序计数器之外,都有可能发生内存溢出。对可能发生溢出区域的测试可以通过产生需要使用该区的方法不断填充它,直到溢出。比如在堆上不断创建对象,直到堆溢出;在虚拟机栈上不断递归调用,使得方法的嵌套层数超过了最大深度发生StackOverflowError或者OutOfMemoryError(当栈无法扩展时)。甚至是运行大量的类去填充方法区,使得其溢出。

总结

从粗略的堆栈,到细分JVM运行数据区为五大类,每大类存放什么样的数据,什么情况下会内存溢出做了一个说明。在对结构有了了解之后,下篇将阐述gc的机制,知道如何解决OOM的问题。

很惭愧,做了一点微小的贡献!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 内存管理