内容比较浅显,主要是整体结构的梳理,错误之处,欢迎指出!
java内存管理.png1、JVM的内部结构
JVM(Java Virtual Machine)是内存的管理者。首先看JVM的内部结构图
jvm.png jvm.gif
如图所示,JVM主要包括两个子系统和两个组件。两个子系统分别是Class loader(类加载器)子系统和Execution engine(执行引擎) 子系统;两个组件分别是Runtime data area (运行时数据区域)组件和Native interface(本地接口)组件。
各部分的作用如下:
-
Class loader
根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。Java程序员可以extends java.lang.ClassLoader类来写自己的Class loader。 -
Execution engine
执行classes中的指令。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。 -
Native interface
与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。 -
Runtime Data Area
这就是我们常说的JVM的内存。
2、关于Runtime Data Area
我们关心的有:
-
堆
-
堆唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
-
堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC堆。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
-
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
-
方法区
-
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-
Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价。
-
这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
-
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
-
运行时常量池在方法区之中
JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。
既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
关于常量池请参考:Java中几种常量池的区分和Java常量池理解与总结 -
栈
Java虚拟机栈也是线程私有的 ,它的生命周期与线程相同。栈是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链表、方法出口信息等。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 -
局部变量表中存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
-
虚拟机只会直接对Java stack执行两种操作:以帧为单位的压栈或出栈
-
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
-
程序计数器
线程私有,是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。
各线程之间计数器互不影响,独立存储。
如果线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。 -
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。比如NIO中引入了一种基于通道和缓冲区的I/O方法,它可以使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
该内存的分配不收Java堆大小的限制。但肯定会受到本机总内存大小以及处理器寻址空间的限制。 -
一个有助理解的例子
引用自:Java堆.栈和常量池 笔记
public class Test{
public static void main(String args[]){
int date = 9;
//date为局部变量,基本类型,引用和值都存在栈中。
Test test = new Test();
//test为对象引用,存在栈中,对象(new Test())存在堆中。
test.change(date);
//方法中i为局部变量,引用和值存在栈中。
//当方法change执行完成后,i就会从栈中消失。
BirthDate d1= new BirthDate(7,7,1970);
//d1为对象引用,存在栈中,对象(new BirthDate())存在堆中;
//其中d,m,y为局部变量存储在栈中,
//且它们的类型为基础类型,因此它们的数据也存储在栈中;
//day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。
//当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
//main方法执行完之后,date变量,test,d1引用将从栈中消失,
//new Test(),new BirthDate()对象实例将等待垃圾回收。
}
public void change(int i){
i = 1234;
}
}
class BirthDate {
private int day;
private int month;
private int year;
public BirthDate(int d, int m, int y) {
day = d;
month = m;
year = y;
}
省略get,set方法………
}
一个关于final的小问题:final只对引用的"值"(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。
3、Java对象的分配和释放
Java中,内存管理是JVM自动进行的,无需人为干涉。创建对象或者变量时, JVM会自动分配内存。当JVM发现某些对象不再需要的时候,就会对该对象占用的内存进行重分配(释放)操作,而且使得分配出来的内存能够提供给所需要的对象。对象的释放则是由垃圾回收机制决定和执行的(监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等)。
4、内存溢出和内存泄漏
4.1 内存溢出
引用自:Java 内存溢出的常见情况和处理方式和java中三种常见内存溢出错误的处理方法
- 定义
内存不够,java程序所需要的内存远远超出了JVM所承受大小,就叫内存溢出,错误信息为:java.lang.OutOfMemoryError。 - 内存溢出的常见原因
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小;
- 解决方法
- 增加jvm的内存大小(但要适当,Heap太大会导致GC时间变长。GC不会经常发生,但是一旦被触发,那么JVM会被冻结很久。)
主要方法是:(a)在执行某个class文件时候,使用java -Xmx256M aa.class
来设置运行aa.class
时jvm所允许占用的最大内存为256M。(b)对tomcat容器或resin容器,在启动时对jvm设置内存限度。 - 优化程序,释放垃圾。
主要包括避免死循环,应该及时释放种资源:内存, 数据库的各种连接,防止一次载入太多的数据。及时地释放没用的对象,释放内存空间,主要从以下几个方面进行考虑:
(a)检查代码中是否有死循环或递归调用。
(b)检查是否有大循环重复产生新对象实体。
(c)检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
(d)检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
4.2 内存泄漏
- 定义
在对象明明已经不需要的时候,还仍然保留着这个对象和它的访问方式(对象游离)。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连(也就是说仍存在该内存对象的引用);其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
内存泄漏是引起内存溢出的一种因素。 - 常见情况
可以概括为:长生命周期的对象持有短生命周期对象的引用。
(a)静态集合类引起内存泄露;
(b)当集合里面的对象属性被修改后,再调用remove()方法时不起作用;
(c)各种监听器在完成功能后没有删除或取消注册;
(d)资源未关闭:各种连接比如数据库连接、网络连接、IO连接没有关闭。要及时主动调用各自的close()方法去关闭资源。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
(e)内部类和外部模块等的不当引用
(f)不正确使用单例模式,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。
这篇文章讲的很好,易于理解:JAVA 内存泄露详解(原因、例子及解决)
5、垃圾回收机制
- 定义
垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露。 - 何时发生GC
一般是在CPU空闲或空间不足时自动进行垃圾回收,而程序员无法精确控制垃圾回收的时机和顺序等。
垃圾回收器如发现一个对象不能被任何活线程访问时,他将认为该对象符合删除条件,就将其加入回收队列,但不是立即销毁对象,何时销毁并释放内存是无法预知的。垃圾回收不能强制执行,然而Java提供了一些方法(如:System.gc()方法),允许请求JVM执行垃圾回收,而不是要求,虚拟机会尽其所能满足请求,但是不能保证JVM从内存中删除所有不用的对象。
网友评论