概述
java内存管理相比于C和C++自己管理内存方便了很多,不用自己手动去管理和释放内存,不必为每一个对象去做free和delete操作,正因为java程序员将内存管理交给了java虚拟机,一旦出现了内存泄露和内存溢出的问题,排查问题成为一件很难的事情。
2.1 java运行时数据区域
2.1.1 程序计数器
程序计数器是线程私有的一块很小的内存区域,主要保存的是当前线程所执行的字节码文件的行号指示器,字节码解析器实现分支,循环,跳转,异常处理,线程恢复等基础功能都是通过程序计数器实现的。
由于java多线程的实现是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时候,一个处理器只会执行一条指令,为了线程切换后还会恢复到正确的位置,每条线程都会有一个自己的‘计数器’。
2.2.2 java虚拟机栈
与程序计数器一样,java虚拟机栈也是线程私有的,生命周期根线程一样,虚拟机展描述的是java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态连接,方法的出口等信息。每个方法重调用的开始到调用的结束对应这在一个栈帧的入栈到出栈的过程。
2.2.3 本地方法栈
本地方法栈跟java虚拟栈的作用非常相似,java虚拟机栈主要是为了java虚拟机执行的方法服务的也就是为了字节码服务的,而本地方法栈主要是为了Native方法服务的,虚拟机规范中对本地方法栈的使用语言和数据结构没有具体的规定,不同的虚拟机可以自由的实现它,有的虚拟机如HotSpot就将本地方法栈和虚拟机栈合二为一了。与虚拟机一样本地房发展也会抛出StackOverFlowError和OutOfMemoryError异常
2.2.4 java堆
java堆是java虚拟机管理的最大一块内存,java堆也是所有线程共享的一块内存区域,在Java虚拟机启动时创建。几乎所有的对象实例都存放在java堆中。java堆是垃圾收集器主要管理的内存区域,所以很多时候被看做GC堆,现在的来及算法一般都才用分代算法,java堆还可以细分为:老年代和新生代,也可以细致的分为Eden空间,From Survivor空间,To Survivor空间,从内存分配的角度来说线程共享的内存也可以分为线程私有的内存空间比如说:ThreadLocal修饰的对象 。
2.2.5 方法区
方法区和java堆一样都是线程共享的,方法区主要存储的是类的信息,比如说:
类信息,常量,静态变量,即时编译器编译后的代码等数据,方法区也可以叫做
No Heap(非堆) ,HotSpot称呼方法区为永生代,现在jdk1.8 已将将永生代移除了java堆中,将永生代放在直接内存中,永生代不会出现内存分配不了的错误。
2.2.6 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本,字段,方法,接口等描述信息和符合引用,这部分内如在类加载进入方法去的运行时的常量池放入。
java虚拟机对Class 文件的每一部分的格式进行了严格的规定,每一个字节用于春初哪个数据都必须符合规范上的要求才会被虚拟机认可,装载和执行。
常量池对于Class文件常量池的另外一个重要的特征就是具有动态性,java语言不要求常量一定在编译器才能产生,也就是并非预置Class文件中的常量池的内容才能进入运行时常量池,运行时也可以将常量放入常量池,比如说String类intern()方法。
当常量池无法放入数据的时候会报出OutOfMemoryError异常。
2.2.7 直接内存
并不是虚拟机内存的一部分,但是这部分内存被频繁使用也可能导致OutOfMemoryError ,在jdk1.4加入的NIO引入了异常基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆以外的内存区域,通过一个存储在java堆中的DirectByteBuffer对象作为这块区域的内存引用。这样能在一些场景中显著的提高性能,因为避免了java堆和Native中来回的内存复制。
2.3.1 对象的创建
当虚拟机遇到一条new指令,如下步骤:
- 首先去常量池去检查是否能定位到一个类的符号引用。
- 并检查这个符号引用所代表的类是否已经被加载,解析和初始化过。
- 如果这个类没有被初始化过就执行相应类的加载过程。
虚拟机进行分配内存。对象所需的内存大小在类的加载过程完成后就可以确定,为对象分配内存相当于从java堆内存中划分出一块区域出来,假设java堆中的内存决定正规的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放这一个指针作为分界点的指示器,那么分配内存仅仅是将那个指针向空闲的地方挪动一段与对象大小相等的距离就可以,这种分配方式指针碰撞。如果java堆内存并不是规整的,使用和空闲的内存相互交错,就不能进行进行指针碰撞了,虚拟机必须维护一张列表,当尽心该内存分配的时候需要从列表中找到一块足够大的空间划分给对象,这个分配方式叫做空闲列表。java堆才用哪中方式根据所才用的垃圾收集器而决定的,CMS基于Mark-Sweep算法就是才用空闲列表。
在并发的情况下进行分配内存的两种解决方案
1.对分配空间的动作进行同步处理---实际上虚拟机才用CAS配上失败的重试的方式保证更新的操作都是原子性
- 把内存分配动作按照线程规划在不同的空间之中进行,每个线程在java堆中分配一块小的内存区域称为本地线程分配缓冲区
哪个线程要分配内存就在线程的TLAB上分配,只用TLAB用完并重新分配时,才需要同步锁定,虚拟机是否使用TLAB,通过-XX:+/-UserTLAB参数决定
内存分配结束后
需要将分配到内存取用的空间初始化为零值(不包括对象头),如果TLAB,这个工作可以提前到TLAB分配时进行,这样可以保证对象的实例字段在java代码中可以不赋值就可以直接使用,程序能访问到这些字段的零值。
虚拟机要对对象进行设置,这个对象属于哪个类,如何才能找到类的元数据信息,对象的哈希码,对象的GC分带信息,这些信息存储在对象头中。
以上步骤都执行完成后,从虚拟机的角度来说一个对象已经产生了,但从java的角度来说对象还没有进行init()方法,所有字段还为零,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后还会执行init()方法,把对象按照程序员工的意愿进行初始化,这样一个对象才可以产生出来。
参考代码C++
-- bytecodeInterpreter.cpp
2.3.2 对象中内存的布局
对象在内存中的存储布局可以分为3块区域:对象头(Header),实例 数据(Instance Data)和对齐填充(Padding):
对象头分为两部分信息:
1.第一部分存储对象自身运行时的数据,
如:哈希码,GC分代年龄,锁状态标志,线程持有的锁等,这部分数据在32位系统是32bit,在64位系统是64bit,官方叫做Mark Word,被设计为非固定长度的数据结构,以便在极小空间存储更多的数据。如果对象未被锁定的状态下,32bit空间中25bit存储对象哈希码,4bit存储对象分代年龄,2bit用于存储锁的标志,1bit固定为0,其他状态(轻量级锁定,重量级锁定,GC标志,可偏向)对象存储内容如下表:
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码,对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录 | 11 | GC标志 |
偏向线程ID,偏向时间戳,对象分代年龄 | 01 | 可偏向 |
第二部分存储类型指针
类型指针,及对象指向类元数据的指针,虚拟机通过这个指针来确定对象属于哪个类的实例,如果是java数组还需要存储数组的长度
2.实例数据是存储对象的有效信息
也就是代码定义的各种类型的字段内容,无论是从父亲继承还是子类中定义的,都需要被记录起来,存储的顺序收到虚拟机分配策略参数和字段在java源码中的定义影响。
HotSpot默认的分配策略是longs/doubles,ints,short/chars,bytes/booleans,oops,相同宽度的字段总是分配到一起
3.第三部分 填充区域
填充对齐区域并不是必须存在的由于HotSpot VM的自动内存管理系统对象其实地址必须是8字节的整数倍,就是对象的大小必须是8字节的倍数,当实例数据没有对齐的时候需要对齐填充数据补全。
2.3.3 对象的访问位置
两种方式一种使用句柄访问和直接指针访问
使用句柄访问需要在java堆内存划分一块内存作为句柄池,reference存储的就是对象的句柄地址,而句柄包含了对象示例数据与类型数据各自的具体地址信息
使用直接指针访问,reference中存储的就是对象的地址HotSpot采用的是第二种对象访问方式。
2.4 OutOfMemoryError异常
除了程序计数器外,虚拟机几个运行时区域都有可能发生OutOfMemoryError出现异常,
2.4.1 JAVA堆溢出
Java堆存储着对象实例,并且保证GC Roots对象之间可能到达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到对的容量限制后就出现内存溢出异常。
2.4.2 虚拟机栈和本地方法栈
由于HotSpot虚拟机并不区分虚拟机栈和本地方法栈,所以设置-Xoss参数(设置本地方法栈大小)存在,但实际无效,栈容量只能由-Xss参数设定,关于虚拟机栈和本地方法栈,在虚拟机规定中描述了两种异常:
1.如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常。
2.如果虚拟机在扩展栈时无法申请到足够的内存空间则抛出OutOfMemoryError异常。
3.使用-Xss参数减少栈内存容量。结果抛出StackOverflowError异常,异常出现时输出堆栈深度相应缩小。
4.定义了大量的本地变量,增加大方法帧中本地变量表的长度结果抛出StackOverflowError异常时输出堆栈深度相应缩小。
2.4.3 方法区和运行时常量池溢出
运行时常量池属于方法区的一部分,jdk1.8将方法区划分为本地内存。
2.4.4 本机直接内存溢出
DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定跟Java堆最大值一样。