Java 内存基础知识
主要介绍 Java 内存相关的基础知识,为 LeakCanary 原理分析做准备。
JVM 内存结构
JVM 在运行时内存区分为不同的区域,每个区域承担着不同的功能。
线程共享区域
1. 方法区
存放类信息(class)、常量、静态变量、JIT 编译器编译后的代码等数据。
存放常量值
2. JVM 堆
存放对象实例和数组,GC 回收的主要区域。
堆区不存放基本类型和对象引用,只存放对象本身。
存放对象实例
线程私有区域
3. 程序计数器
主要代表当前线程所执行的字节码行号指示器。
字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
线程字节码行号指示器
4. JVM 栈
与线程同时创建,代表 java 方法执行的内存模型。
每个方法执行时都会创建一个 栈桢 来存储方法的的局部变量表、操作数栈、运行时常量池的引用、返回值、返回地址等信息。
每个方法从调用到结束对应一个 栈桢 在 JVM 栈 中的入栈和出栈过程。
存放局部变量
5. Native 栈
Native 方法有关。
native
实例
说一堆概念不理解没用,看个简单的例子:
private static int key = 0; // 0
public static void main(String args[]){
int a = 3; // 1
Bean bean = new Bean(4); // 2
}
class Bean {
private int num;
public Bean(int n) {
this.num = n;
}
}
注释 0 处,值在方法区。
引用呢?求解...
注释 1 处,int a = 3
,基础数据类型,引用和值都存放在 JVM 栈中,方法执行完毕将从栈中消失。
注释 2 处,Bean bean = new Bean(4)
,其中 :
- bean 为对象引用,存放在栈中,对象则存放在堆中;
- n = 4,n 为局部变量,存放在栈中。基础数据类型,所以值也存在栈中。当 Bean 构造函数执行完毕,n 将从栈中消失;
- num = 4,num 为成员变量,由于 bean 对象存放堆中,所以它的 引用和值都存放在堆里。
方法执行完毕后,a 的引用和值、bean 引用将从栈中消失,bean 对象则等待 GC 回收。
jdk 1.8 后有所变化,暂不讨论;另外 String 创建对象的问题可以参考 此文章。
Java 内存模型
Java 内存模型是 Java 层对内存的抽象,目的是解决多线程共享内存情况下的原子性、可见性和有序性问题。
Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存的是变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
volatile、synchronized、final、concurren 包等就是 Java 内存模型底层封装后提供给程序员的关键字。
关于原子性、内存可见性、有序性,可以参考 另一篇博客。
引用分类
强、软(SoftReference)、弱(WeakReference)、虚(PhantomReference,ReferenceQueue)四种引用。
强引用
被强引用的对象永远不会被垃圾回收,内存不足时 OOM。
软引用
内存不足时才进行垃圾回收的引用方式。
可以用作缓存。
弱引用
只要 GC 就回收的引用方式。
可以用于子线程引用 context、handler 回调、缓存等场景。
如网络回调成功后引用 context 执行刷新页面等操作,如果网络回调时间过长,页面关闭,context 被工作线程持有则导致内存泄漏。此时使用弱引用包装 context,则 context 会在页面关闭、GC 触发时及时回收。
其它例子可以参考 知乎 上关于 ThreadLocal 中使用弱引用的描述,也很形象。
形象的说:
弱引用的对象尚在(尚有强、软引用)则用、不在则忽略,不限制对象本身的"去留"。
而软引用仅仅在内存不足时才肯放对象"离去"。
虚引用
这个引用可以在对象被收集器回收时收到一个系统通知。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中,所以通过检查该队列里面的内容就知道一个对象是不是已经可以被回收了。
[TOC]