第一部分 内存模型
我们常说Java内存模型(Java Memory Model, JMM)指的是Java运行时(Java runtime)内存模型,Java源码通过javac 翻译成字节码,由即时编译器(JIT)编译执行。因为字节码是静态代码,需要加载到内存才能成为可以动态运行的对象。运行时内存数据区大体上被分为5个区域、两大类型, 如下图。
线程私有(隔离)数据区
1、程序计数器,记录正在执行的虚拟机字节码的地址;
2、虚拟机栈:方法执行时创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法一个栈帧,互不干扰。
3、本地方法栈:虚拟机的Native方法执行的内存区;
线程共享数据区:
1、Java堆:存放对象的区域;
2、方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据;
3、常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。
第二部分 模型详情
运行时内存分为五大块区域,Java内存总体结构图如下:
2.1 程序计数器
当前线程所执行的字节码行号指示器。每个线程都有自己计数器,是私有内存空间,各个线程之间计数器互不影响,独立存储。
当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)。
2.2 虚拟机栈
虚拟机栈,生命周期与线程相同,是Java方法执行的内存模型。每个方法(不包含native方法)执行的同时都会创建一个栈帧结构,方法执行过程,对应着虚拟机栈的入栈到出栈的过程。
2.2.1 栈帧(Stack Frame)结构
栈帧是用于支持虚拟机进行方法执行的数据结构,是属性运行时数据区的虚拟机站的栈元素。
2.2.2 栈帧
局部变量表
一组变量存储空间, 容量以slot为最小单位。操作栈(stack大小,编译期确定),操作栈元素的数据类型必须与字节码指令序列严格匹配
动态连接
指向运行时常量池中该栈帧所属方法的引用,为了 动态连接使用。前面的解析过程其实是静态解析;对于运行期转化为直接引用,称为动态解析。
方法返回地址
正常退出,执行引擎遇到方法返回的字节码,将返回值传递给调用者
异常退出,遇到Exception,并且方法未捕捉异常,那么不会有任何返回值。
额外附加信息
虚拟机规范没有明确规定,由具体虚拟机实现。
Java虚拟机规范规定该区域有两种异常:
StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出
OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出
2.3 本地方法栈
本地方法栈则为虚拟机使用到的Native方法提供内存空间,而前面讲的虚拟机栈式为Java方法提供内存空间。
异常(Exception):Java虚拟机规范规定该区域可抛出StackOverFlowError和OutOfMemoryError。
2.4 Java堆
Java堆,是Java虚拟机管理的最大的一块内存,里面存放的是几乎所有的对象实例和数组数据。
从内存回收角度,Java堆被分为新生代和老年代;这样划分的好处是为了更快的回收内存;
从内存分配角度,Java堆可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);这样划分的好处是为了更快的分配内存;
对象创建的过程是在堆上分配着实例对象,那么对象实例的具体结构如下:
对于填充数据不是一定存在的,仅仅是为了字节对齐。HotSpot VM的自动内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例数据不是8的倍数,便需要填充数据来保证8字节的对齐。该功能类似于高速缓存行的对齐。
堆的结构
首先堆可以划分为新生代和老年代,
然后新生代又可以划分为一个Eden区和两个Survivor(幸存)区。
按照规定,新对象会首先分配在Eden中(如果对象过大,比如大数组,将会直接放到老年代)。在GC中,Eden中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过minor GC的次数),会被移动到老年代。新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
垃圾回收
垃圾回收的意义
分代收集。即在新生代和老生代使用不同的收集方式。在垃圾收集上,目标主要有:加大系统吞吐量(减少总垃圾收集的资源消耗);
减少最大STW(Stop-The-World)时间;减少总STW时间。不同的系统需要不同的达成目标。而分代这一里程碑式的进步首先极大减少了STW,然后可以自由组合来达到预定目标。
可达性检测
引用计数:一种在jdk1.2之前被使用的垃圾收集算法,我们需要了解其思想。其主要思想就是维护一个counter,当counter为0的时候认为对象没有引用,可以被回收。缺点是无法处理循环引用。
根搜算法:思想是从gc root根据引用关系来遍历整个堆并作标记,称之为mark,
之后回收掉未被mark的对象,好处是解决了循环依赖这种,
这里的gc root主要指:
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象
整理策略
复制:主要用在新生代的回收上,通过from区和to区的来回拷贝。需要特定的结构(也就是Young区现在的结构)来支持,对于新生成的对象来说,频繁的去复制可以最快的找到那些不用的对象并回收掉空间。所以说在JVM里YGC一定承担了最大量的垃圾清除任务。
标记清除/标记整理:主要用在老生代回收上,通过根搜的标记然后清除或者整理掉不需要的对象。
2.5 方法区
方法区主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。GC在该区域出现的比较少。异常(Exception):Java虚拟机规范规定该区域可抛出OutOfMemoryError。
2.6 运行时常量池
运行时常量池也是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。运行时常量池除了编译期产生的Class文件的常量池,还可以在运行期间,将新的常量加入常量池,
第三部分 Java中的内存泄露问题
虽然Java拥有垃圾回收机制,但同样会出现内存泄露问题,比如下面提到的几种情况:
(1)、 例如 HashMap、Vector 等集合类的静态使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
private static Vector vector = new Vector();
public void TestGc() {
for (int i = 0; i < 1000; i++) {
Object object = new Object();
vector.add(object);
object = null;
}
}
上面的代码中,虚拟机栈中保存者 Vector 对象的引用 vector 和 Object 对象的引用 object 。在 for 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 object 引用置空。问题是虽然我们将 object 引用置空,但当发生垃圾回收时,我们创建的 Object 对象也不能够被回收。因为垃圾回收在跟踪代码栈中的引用时会发现 vector 引用,而继续往下跟踪就会发现vector引用指向的内存空间中又存在指向 Object 对象的引用。也就是说,尽管object引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。
(2)、非静态内部类持有外部类的应用,容易导致内存泄漏
(3)、 各种资源连接包括数据库连接、网络连接、IO连接等没有显式调用close关闭,不被GC回收导致内存泄露。
(4)、监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。