文章目录
- 一、 Java 虚拟机内存模型
- 二、 程序计数器 ( 线程私有区 )
- 三、 虚拟机栈 ( 线程私有区 )
- 四、 本地方法栈 ( 线程私有区 )
- 五、 方法区 ( 共享数据区 )
- 1. 方法区
- 2. 运行时常量池
- 六、 堆区 ( 共享数据区 )
- 七、 内存溢出类型
- 八、 引用计数算法回收内存
- 九、 可达性分析算法回收内存
一、 Java 虚拟机内存模型
Java 内存优化 , 首当其冲就是处理 Java 内存泄漏问题 , 这是 Java 程序最主要的内存问题 , 大量的内存泄漏会导致内存溢出 ;
Java 虚拟机内存机制 : Java 虚拟机中内存分为两部分 , 线程私有部分 , 共享数据区 ;
① 共享数据区 : 方法区 ( Method Area ) , 堆区 ( Heap Area ) ; 其中方法区中包含常量池 ;
② 线程私有数据区 : 程序计数器 ( PC ) , 虚拟机栈 ( VM Stack ) , 本地方法栈 ( Native Method Stack ) ;
这是 Java 虚拟机规范定义的内存分区 , 但是具体的厂家实现可能不完全一致 , 如 Sun JDK , Open JDK 等 ;
Android 中的 Java 虚拟机 跟上述 Java 规范有很大不同 ;
二、 程序计数器 ( 线程私有区 )
程序计数器 :
① 作用 : 该内存空间很小 , 主要用于指示执行的代码行 , 程序计数器指向的代码行 , 就是下一行将要执行的代码 ;
② 线程切换运行 : Java 多线程是抢占式执行的 , 经常出现线程
执行时 , 切换到线程
, 如果线程
执行完毕回到线程
, 这里就需要记住线程
之前执行到哪了 , 这就需要用到线程私有的数据区的程序计数器 ( PC ) ;
③ 执行 Java 代码 : 线程执行 Java 代码时 , 程序计数器记录的是虚拟机字节码地址 ;
④ 执行 Native C/C 代码 : 线程执行 native 代码时 , 程序计数器记录的 值是空值 null ;
程序计数器 区域没有定义 内存溢出 异常 , 这个区域很小 ;
三、 虚拟机栈 ( 线程私有区 )
1. 虚拟机栈 ( VM Stack ) : 其生命周期与线程相同 , 描述的是 Java 方法执行的内存模型 , 该区域就是栈区 , 与堆区相对应 ;
2. 虚拟机栈中保存的数据 :
- 局部变量表
- 操作栈
- 方法返回地址
- 动态链接
- 额外附加信息
四、 本地方法栈 ( 线程私有区 )
本地方法栈 ( Native Method Stack ) : 这是 Native 层 C/C 提供的栈内存空间 , 该内存的类型与虚拟机栈内存类型一样 , 只是语言不同 , 一个 Java 方法的额栈 , 一个是 C/C 方法的栈 ;
Hotspot VM 虚拟机中 , 虚拟机栈 与 本地方法栈是一块内存 , 二者合二为一 ;
五、 方法区 ( 共享数据区 )
1. 方法区
方法区 : 存储以下内容 ;
- 类信息 , 如 ClassLoader 加载的 Class
- 常量 , 存放在运行时常量池中 , 该常量池也是方法区的一部分 ;
- 静态变量 , static 变量
- 即时编译器( JIT compiler ) 编译后的代码
不同的虚拟机 , 实现不同 ;
该区域一般不进行 GC 垃圾回收 ;
2. 运行时常量池
运行时常量池 :
- 编译中的 Java 常量 ( public static final )
- 字符串常量 ( String )
- final 修饰的常量 ;
- 符号引用 , 如 类或接口完整名称 ( 带包名 ) , 字段名 , 方法名 , 描述符 ;
六、 堆区 ( 共享数据区 )
Java 堆区 :
① 最大区域 : 该内存区是 Java 虚拟机管理的内存中最大的部分 , 是垃圾回收算法 GC 的主要操作区域 ;
② 内存溢出 : OOM ( OutOfMemory ) 内存溢出就是该区域内存被全部占用 , 无法为新的内存申请更多空间 ;
七、 内存溢出类型
内存溢出 :
① 栈内存溢出 : 在 Java 的栈区内存溢出 , 就是 StackOverflowException 栈溢出异常 , 在递归的时候 , 如果没有控制好 , 就会报该异常 ;
② 堆内存溢出 : 在 Java 堆内存中的溢出 , 就是 OutOfMemoryError 堆内存溢出 , 在加载大量数据到内存时 , 会出现该异常 ;
八、 引用计数算法回收内存
引用计数是早期的 GC 回收 Java 对象机制 , 有一定弊端 ;
1. 引用计数简介 : 使用对象的引用计数 , 确定 Java 对象是否存活 , 确定是否应该被回收 ;
2. 引用计数垃圾回收算法示例说明 :
① 创建对象 : 创建一个
类型对象
, 此时引用计数为 0 , 如果不将其赋值给一个变量 , 那么很快就会被回收 ;
② 变量
赋值 : 创建一个
类型对象
, 将对象
其 赋值 给变量
, 此时该对象
引用计数为
;
③ 变量
赋值 : 创建一个
类型对象
, 将对象
其 赋值 给变量
, 此时该对象
引用计数为
;
④
引用
: 变量
中有
类型成员变量 , 将
赋值 给该成员变量 , 此时对象
引用计数变成
;
⑤
引用
: 变量
中有
类型成员变量 , 将
赋值 给该成员变量 , 此时对象
引用计数变成
;
此时即使把
两个变量都设置成 null , 每个变量的引用计数都减一 , 也无法将引用计数减为
, 该对象永远无法回收 ;
引用计数弊端 : 如果两个变量之间互相引用 , 引用计数永远不能变为
;
九、 可达性分析算法回收内存
1. 可达性分析算法 : 以 GC Root 为分析的起点 , 查找对象的引用 , 如果找到一个对象 , 无法被 GC Root 直接或间接引用到 , 那么该对象就可以被回收了 ;
2. GC Root 对象 : GC Root 是一个对象 , 可以是如下对象 ;
- 虚拟机栈正在运行的引用
- 静态属性
- 常量
- JNI 中的对象
GC Root 就是不会被回收的那些的变量 , Android 中就是 Application , 单例类 , 运行中的 Activity 等 ;
3. 第一次扫描回调 finalize 方法 : 对象经过可达性分析后 , 发现没有引用链可以达到 GC Root , 此时就会调用该对象的 finalize() 方法进行标记 , 开发者可以实现该方法 , 进行一些逻辑处理 :
- ① 释放资源 : 可以执行一些资源释放方法 , 一面出现内存泄漏 ;
- ② 引用自救 : 将对象赋值给指定变量 , 这样可以避免被 GC 回收内存 ;
4. 可达性分析中对对象的两次扫描 : 可达性分析时 , 需要对指定对象标记两次 , 第一次被标记时会调用该对象 finalize() 方法 , 相当于判了死缓 , 此时可以通过添加引用的方式自救 , 如果没有进行任何干预 , 第
次扫描到该对象还没有到 GCRoot 的引用链 , 此时不会调用 finalize() 方法 , 直接就被回收了 ;