一、JVM内存模型

常见jvm内存模型,主要分为堆区,本地方法栈,虚拟机栈,程序计数器,和方法区。如下图所示: image.png

(1)程序计数器

每个线程都会有自己私有的程序计数器(PC)。可以看作是当前线程所执行的字节码的行号指示器。 也可以理解为下一条将要执行的指令的地址或者行号。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程上下文切换,线程恢复时,都要依赖PC.

  • 如果线程正在执行的是一个Java方法,PC值为正在执行的虚拟机字节码指令的地址
  • 如果线程正在执行的是Native方法,PC值为空(未定义)

(2)虚拟机栈(VM Stack)

简介

VM Stack也是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。 **在《java虚拟机规范》一书中对这部分的描述如下:**栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在 Java 虚拟机栈( §2.5.5)之中,每一个栈帧都有自己的局部变量表( Local Variables, §2.6.1)、操作数栈( OperandStack, §2.6.2)和指向当前方法所属的类的运行时常量池( §2.5.5)的引用。 VM-Stack 说白了,VM Stack是一个栈,也是一块内存区域。 所以,他是有大小的。虽然有大小,但是一般而言,各种虚拟机的实现都支持动态扩展这部分内存。

  • 如果线程请求的栈深度太大,则抛出StackOverflowError
  • 如果动态扩展时没有足够的大小,则抛出OutOfMemoryError以下代码肯定会导致StackOverflowError:

StackOverflowError

public static void method() {
    method();
}

public static void main(String[] args) {
    method();
}

Exception in thread "main" java.lang.StackOverflowError
    at xxx.xxx.xxx.method(JavaVMStackSOF.java:10)
复制代码

(3)本地方法栈

Java 虚拟机实现可能会使用到传统的栈(通常称之为“ C Stacks”)来支持 native 方法( 指使用 Java 以外的其他语言编写的方法)的执行,这个栈就是本地方法栈( Native MethodStack)。 VM Stack是为执行java方法服务的,此处的Native Method Stack是为执行本地方法服务的。 此处的本地方法指定是和具体的底层操作系统层面相关的接口调用了(这部分太高高级了,不想深究……)。 《java虚拟机规范》中没有对这部分做具体的规定。所以就由VM的实现者自由发挥了。 有的虚拟机(比如HotSpot)将VM Stack和Native Method Stack合二为一,所以VM的另一种内存区域图就如下面所示了: image.png

(4)方法区

方法区是由所有线程共享的内存区域。 方法区存储的大致内容如下:

  • 每一个类的结构信息
    • 运行时常量池( Runtime Constant Pool)
    • 字段和方法数据
    • 构造函数和普通方法的字节码内容
  • 类、实例、接口初始化时用到的特殊方法

以下是本人对《java虚拟机规范》一书中对方法区的介绍的总结:

  • 在虚拟机启动的时候被创建
  • 虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集
  • 不限定实现方法区的内存位置和编译代码的管理策略
  • 容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
  • 方法区在实际内存空间中可以是不连续的
  • Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段
    • 对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段
  • 如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常

(5)堆

简介

在 Java 虚拟机中,堆( Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。 以下是本人对《java虚拟机规范》一书中对Java堆的介绍的总结:

  • 在虚拟机启动的时候就被创建
  • 是所有线程共享的内存区域
  • 存储了被自动内存管理系统所管理的各种对象
  • Java 堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩
  • Java 堆所使用的内存不需要保证是连续的
  • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常
  • 实现者应当提供给程序员或者最终用户调节 Java 堆初始容量的手段
  • 所有的对象实例以及数组都要在堆上分配
  • 至于堆内存的详细情况,将在后续的GC相关文章中介绍。

堆内存中的OutOfMemoryError以下示例代码肯定导致堆内存溢出:

public static void main(String[] args) {
    ArrayList<Integer> list = Lists.newArrayList();
    while (true) {
        list.add(1);
    }
}
复制代码

无限制的往list中添加元素,无论你的堆内存分配的多大,都会有溢出的时候。

java.lang.OutOfMemoryError: Java heap space
复制代码

二、内存优化工具

(1)LeakCanary

介绍

leakCanary这是一个集成方便, 使用便捷,配置超级简单的框架,实现的功能却是极为强大的线下内存检测工具。 screenshot-2.0.png

如何使用

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
复制代码

在项目集成之后,在Android Studio Logcat日志通过筛选LeakCanary可以看到如下日志,标志LeakCanary已经安装成功,并且已经启动。 image.png

内存泄漏

我们经常会用到很多单例的工具类,往往这些单例也是经常容易发生内存泄漏的地方。下面我们模拟一下单例工具类造成内存泄漏的情况,封装一个ToastUtils的单例类,内部持有context的引用,这样在页面销毁之后依然持有context的引用,造成无法销毁,从而造成内存泄漏。

object ToastUtils {
    private var context: Context? = null

    fun toast(context: Context, text: String) {
        this.context =context
        Toast.makeText(context, text, Toast.LENGTH_LONG)
    }
}
复制代码

在控制台和手机上都可以看到内存泄漏的信息。如下所示: image.pngimage.png 手机上可以明显看到内存泄漏的列表,通过点击item即可看到详细的堆栈信息