前言:凡事有利有弊,java有虚拟机自动管理内存,不用像C++一样自己去处理对象的内存依赖,但是这样的话一旦不了解java虚拟机内存如何工作的,一旦出现内存泄漏等内存问题就会比较难以排查,本章就是介绍java虚拟机内存。
一.运行时数据区域
java虚拟机在执行java程序的过程中会把它所管理的内存划分成若干个不同的数据区域。每个区域都有各自的用途,以及创建销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。如下图:
1.程序计数器:是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。笔者的理解是计数器为某些java基础功能提供了类似于标识的作用,好比当这标识是1的时候你去做什么,标识变为0的时候你去做什么 ;
举个运用的例子如:可重入锁的实现原理是为每一个锁设置一个关联计数及线程对象,初始计数为0,当一个线程获取该对象时,jvm为计数器增加为1,再次获取则继续往上增加,当该线程退出一次同步代码块,则计数器减去1,当计数器为0时,即释放锁。
因为他是类似于一个标识,所以他也就类似于多线程的线程安全,它对每个线程都有独立的计数器,各个线程互相不影响,独立存储,我们称为“线程私有”的内存,此内存区域是唯一一个不存在内存溢出的情况的区域。
2.java虚拟机栈:主要用来存储局部变量表、操作数栈、动态链接、方法出口等,每个方法在执行时都会创建一个栈,每一个方法从调用到执行完成的过程,就对应这一个栈帧在虚拟机中入栈到出栈的过程。因此它也是“线程私有”的内存。
3.本地方法栈:本地方法栈和虚拟机方法栈是一个道理,区别是虚拟机栈为虚拟机执行java方法(也就是字节码服务),本地方法栈则是虚拟机使用到的native 方法服务,同理也会抛出内存溢出异常(outOfmemoryError).
4.java堆:是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块区域,在虚拟机启动时创建。
作用:存放对象实例。同时也是垃圾回收器管理的主要区域,称为“GC堆”,其实为了便于记忆,也可以俗称“垃圾堆”。就像我们的磁盘空间一样,如果在堆中没有足够的内存完成示实例分配,并且堆也无法扩展时,会抛出内存溢出异常。
5.方法区:方法区和java堆一样是线程各个区域的共享区域,属于堆的一块逻辑上的区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。需要注意的是JDK8后,永久代被移到与一个堆不相连的本地内存区域,也即就是元空间由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
7.运行时常量池:运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。
8.直接内存:内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。使用未公开的Unsafe和NIO包下ByteBuffer来创建堆外内存。
二、对象的创建
过程如图所示:
参考文档:https://blog.youkuaiyun.com/cb_lcl/article/details/81078664
1.对象的内存布局
对象在内存中的布局可以分为3个:对象头、实例数据、对象填充。
对象头中存储了对象运行时的一些数据
实例数据存储对象的实例有关的一些有效信息
对象填充为了保证对象大小的8的倍数
参考文档:https://blog.youkuaiyun.com/geloin/article/details/82916939
2.对象的访问定位
一句话:通过栈帧中局部变量表所存储的对象引用来对堆内存中的对象实例进行访问或操作的!
1.句柄
java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。如图2-2:
2.直接访问对象
reference直接指向了对象类型数据,那么java堆对象分布中就必须考虑如何放置访问类型数据的相关信息,
reference存储的直接就时对象地址。如图:2-3
参考文档:https://www.cnblogs.com/YYfish/p/6722258.html
三、实战:outOfMemoryError异常
因为在java虚拟机规范的描述中,除了虚拟机计数器外,虚拟机内存的其他几个运行时区域都有发生outOfMemoryError异常的可能,本节将会通过若干示例来验证异常发生的场景,初步介绍几个内存相关的基本虚拟机参数。