对于C,C++的程序员来说,内存是始终要维护的。而在JAVA中,程序员把这个权力交给了JAVA虚拟机,所以JAVA不容易出现内存泄露和溢出的问题。但是我们也要理解JAVA虚拟机是如何使用内存的,才能在发生内存错误的时候高效地排除错误,修正问题。
概念模型 :它代表了所有虚拟机的统一外观, 但各款具体的Java虚拟机并不一定要完全照着概念模型的定义来进行设计, 可能会通过一些更高效率的等价方式去实现它。
程序计数器是线程私有的,因为处理器(或多核处理器的一个核)同时只能执行一个线程。如果是JAVA方法,程序计数器指向正在执行的虚拟机字节码指令的地址。如果是本地方法(native方法),计数器值应该为空(没有OutOfMemory异常)
线程私有,生命周期与线程相同
栈帧:在每一个方法被调用时在栈上被创建,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
局部变量表:存放了各种基本类型和对象引用(对象引用则是指针,句柄等指向对象的位置)。和returnAddress(指向了一条字节码指令的地址)
与虚拟机栈的功能基本相同,但是是为了本地方法服务。
堆是虚拟机所管理的内存最大的一块。几乎所有的对象实例和数组都应该在堆上分配。栈上分配,标量替换等都是使这个几乎出现可能的地方。
java堆可以分配在不连续的物理空间上,只要逻辑连续就可以。但是多数虚拟机出于实现简单,储存高效等原因,在物理上也为数组等对象分配连续的内存空间。
运行时常量池是方法区的一部分,Class文件中除了有类的版本、 字段、 方法、 接口等描述信息外, 还有一项信息是常量池表(Constant Pool Table) , 用于存放编译期生成的各种字面量与符号引用, 这部分内容将在类加载后存放到方法区的运行时常量池中。
不是虚拟机运行时数据区的一部分,但是会被频繁使用(直接调用native方法分配堆外内存),可以显著提高性能,避免了在JAVA堆和native堆中来回复制数据。
首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。然后在堆上进行内存分配。(在第7章将详细描述)
如果堆规整(一边是已经用过的堆空间,一边是没用过的)那么可以使用指针碰撞(Bump the pointer)。即把指针偏移来分配堆空间。
如果堆不规整,那么需要使用空闲列表(freelist)。即虚拟机需要维护一个列表,其中写着哪些内存块可用。
堆是否规整又是由所采用的垃圾收集器是否有空间压缩整理的能力决定的。
两种可选方案:
一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性.
另外一种是把内存分配的动作按照线程划分在不同的空间之中进行, 即每个线程在Java堆中预先分配一小块内存, 称为本地线程分配缓冲(Thread Local AllocationBuffer, TLAB) , 哪个线程要分配内存, 就在哪个线程的本地缓冲区中分配, 只有本地缓冲区用完了, 分配新的缓存区时才需要同步锁定。 虚拟机是否使用TLAB, 可以通过-XX: +/-UseTLAB参数来设定。
虚拟机会把分配的内存空间都初始化为0,所以不赋值也是可以使用的。
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
对象头
1.存储对象自身的运行时数据如河西吗,锁标志,偏向时间戳等。
2.类型指针,指向它类型元数据的指针 (并不是所有的虚拟机都使用了这个)。
数组的话还有一部分对象头存储数组长度。
实例数据:
对象真正存储的有效信息包括子类和从父类继承的。
对齐填充:
并不是必然的存在,也没有特别的意义,仅仅是占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍, 换句话说就是任何对象的大小都必须是8字节的整数倍。
对象头是8的倍数,实例数据可以通过对齐填充来补齐。
java程序会根据栈的reference来访问堆中的数据,通常有两种方法。
除了程序计数器,java的这几个区域都会涉及OOM异常
原因:程序计数器只需记录一个地址就可以了,不会分配更多的内存
堆溢出:循环建立新对象
栈溢出:不断自我call
方法区和运行时常量池溢出: 不断调用 String.valueOf().intern();
String::intern()是一个本地方法, 它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串, 则返回代表池中这个字符串的String对象的引用; 否则, 会将此String对象包含的字符串添加到常量池中, 并且返回此String对象的引用。
HotSpot从JDK 7开始逐步“去永久代”的计划, 并在JDK 8中完全使用元空间来代替永久代的背景故事, 在此我们就以测试代码来观察一下, 使用“永久代”还是“元空间”来实现方法区,
自JDK 7起, 原本存放在永久代的字符串常量池被移至Java堆之中,
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
在JDK6中是false,false。因为把这个String存到了永久代并且返回,显然和StringBuilder创建的不是同一个。
在JDK7中 true,false。true的原因是常量表记录了首次出现的实例的引用,intern将会返回该值:即与StringBuilder产生的是同一个。但是java关键字以前出现过,那么intern会返回首次出现的引用,那么两个java并不是同一个值。