《深入理解java虚拟机》——java内存区域与内存溢出异常

2019-11-21  本文已影响0人  李die喋

我是很喜欢用java语言编写代码的。从开始学习到现在其实也是在一步步体会java语言的各方面,开始看深入理解java虚拟机这本书觉得java虚拟机的内部感觉就像是一个操作系统,也可以说是个计算机。想要深入的理解我觉得需要先从整体去看。为什么需要java虚拟机,java虚拟机到底是个什么?我会先从这个方向开始理解。

JDK:java程序设计语言、java虚拟机、java API类库三部分。

JRE:支持java程序运行的标准环境。

java虚拟机实现了java语言与平台的无关性。意思就是java语言在不同的平台(操作系统等环境)运行都是可以的,因为java虚拟机将java源文件编译成class字节码文件,class字节码在java虚拟机中被解释成机器码。

JVM定义了控制java代码解释执行和具体实现的五种规格,他们是:

jvm指令系统同其他计算机的指令系统及其相似。java指令也是由操作码和操作数两部分组成。操作码为8位二进制数,操作数紧随操作码之后,其长度根据需要而不同。当操作数的长度大于8位时,会被分为两个以上的字节存放。它编码的方式和intel采用的方式不同,jvm是低字节放在高位,高位放在低字节中。java的8位操作码的长度使jvm最多有256条指令,java1.6及以上版本与使用了160多种操作码。

java类的实例所需的存储空间是在堆上分配的。

jvm有两类存储区:常量缓冲池和方法区。

常量缓冲池:用于存储类名称、方法和字段名称以及串常量。

方法区:用于存储java方法的字节码。

这两种存储区域具体实现方式在jvm规格中没有具体说明,也就是说java应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。

有一个类比的例子我觉得很形象:

如果把Java原程序想象成我们的C++原程序,Java原程序编译后生成的字节码就相当于C++原程序编译后的80x86的机器码(二进制程序文件),JVM虚拟机相当于80x86计算机系统,Java解释器相当于80x86CPU。在80x86CPU上运行的是机器码,在Java解释器上运行的是Java字节码。

了解到这里感觉jvm和最近学习的微机原理好像有异曲同工之妙。计算机在处理信息的时候也需要寄存器去存储数据或者存储指令地址等,有他的存储单元和运算单元。

运行时数据区区域

书里说java提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界的问题。在android中如果对某些对象的使用不当的话就会出现内存泄漏,为此就带着这个问题去看看,java虚拟机内存的各个区域,为什么避免了这些问题。。。

java虚拟机所管理的内存将会包含以下几个运行时数据区域:

1. 程序计数器

程序计数器是一块较小的内存空间,没有规定OOM情况的区域且是线程私有的,有两个主要功能

字节码解释器通过改变程序计数器的值来选取下一条将要执行的字节码指令(是跳转 循环还是。。。)

在操作系统中进程切换执行也需要记录进程当前执行的状态。这里的作用是可以类比的。因为java虚拟机的多线程是通过线程的轮流切换并分配处理器执行时间的方式实现的,因此需要记录当前线程运行的状态以便于下次从上次的运行状态开始执行。

如果线程正在执行的是java方法,计数器记录正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,计数器值为空。

2. java虚拟机栈

线程私有,且与线程的生命周期相同。描述的是java方法执行的内存模型

有一个需要了解的概念栈帧:是方法运行时的基础数据结构。

每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用到执行,就是一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表

局部变量表存放了编译期可知的各种数据基本类型、对象引用类型和returnAddress类型(指向了一条字节码指令的地址)。

有两个特点:

两种异常情况:

3. 本地方法栈

本地方法栈为虚拟机使用到的native方法服务,虚拟机栈为虚拟机执行的java方法服务。

本地方法区域会抛出StackOverFlowError异常和OutOfMemoryError异常。

4. java堆

java堆是java虚拟机所管理的最大的一块内存(对大多数应用),是被所有线程共享的一块内存区域,在虚拟机启动时创建。内存区域的唯一目的就是存放对象实例
几个特点:

无论如何划分,无论哪个区域,存储的都是对象实例。划分目的就是为了更好的回收内存或者更快的分配内存。

5. 方法区

几个特点:

6. 运行时常量池

几个特点:

Class文件有类的版本、字段、方法、接口等描述信息,还有一个就是常量池。

java虚拟机对class文件的每一部分都有严格的格式要求,每个字节用于存储什么样的数据都必须符合规范才可以被虚拟机认可、装载和执行。

java语言并不要求常量一定只要编译期才能产生,也就是并不是在class文件常量池中的内容才能进入方法区的运行时常量池。

7. 直接内存

对象的创建

对象的内存布局

对象的访问定位

这里看了很久的时间,发现其实做一个大纲就很好理解并且把他们串起来。


image

OutOfMemoryError异常

在android的日常开发中就很可能在写代码的过程中遇到OOM的问题。我遇到过OOM的场景就是在加载Bitmap的时候还有就是多线程的情况下,线程开的太多以至于在线程池中都无法挽救。

现在就先从底层来分析一下OOM出现的情况:

1. java堆溢出

java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。

在android可以通过下面的方法获得堆的大小,也就是每个程序可使用的内存的上限:

ActivityManager manager = (Activity)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();//结果以MB为单位返回

有一个东西叫GC Roots到底是什么呢?

有一个算法叫做根搜索算法。是JVM用来*判断对象是否存活的算法,此算法的基本思路就是通过一系列的GC Roots对象作为起始点,从这些结点往下搜索,当一个对象和GC Roots不可达时,则该对象是无用的。

image
从上面的图可以看到5、6、7都到达不了GC Roots,所以会被回收掉。

可以作为GC Roots的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native)中引用的对象

java堆内存的OOM异常是实际中经常会遇到的情况。当出现堆内存溢出时,为了解决,一般的手段是先通过内存映像分析工具对dump出来的堆转储快照进行分析。重点是确认内存中的对象是内存泄漏还是内存溢出。

虚拟机栈和本地方法栈溢出

方法区和运行时常量池溢出

就是在这两部分中的数据超过他们的范围就会溢出。有一个很有意思的例子

public class Test{
    public static void main(String[] args){
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);//true-jdk1.7以后 之前为false
        
        //在这之前java这个字符串在常量池中存在
        String str2 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str2.intern() == str2);//false 因为在new StringBuilder之前str2已经有指向常量池中的引用了
    }
}

首先去看了下intern这个方法是干嘛的,原来是返回字符串的对象。String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

产生注释中结果不同的原因是:

本机直接内存溢出

直接内存容量可通过-XX:MaxDirectMemorySize指定,如果不指定则默认与java堆最大值一样。

分配本机内存可以用两种方式:

用DirectByteBuffer

用过java NIO的话应该都用过ByteBuffer,用来作为消息的缓冲区,它其实是在直接内存开辟了一块空间。(看了虚拟机才知道 之前真的是蒙着头用啊 哈哈哈)

ByteBuffer BUFFER = ByteBuffer.allocateDirect(1*1024*1024);

获取UnSafe实例进行内存分配

public class DirectMemory{
    public static void main(String[] args){
        Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessiable(true);
        Unsafe unsafe = (Unsafe)unsafeField.get(null);
        unsafe.allocateMemory(1*1024*1024);
    }
}

在jdk1.4中新加入了NIO类,引入了一种基于通道(channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这可以提高性能,因为避免了在java堆和native堆中来回复制数据。

所以直接内存区域的溢出,发生在可能忽略了分配直接内存的大小,在项目中使用了NIO的时候发生了OOM可以看是不是直接内存溢出的原因。

参考文章

百度知道 统领全文的作用

GC Roots 例子

java堆还是本地内存

上一篇 下一篇

猜你喜欢

热点阅读