内存的划分:
线程私有:程序计数器、Java虚拟机栈、本地方法栈。
线程共享:Java堆、方法区。
程序计数器:是当前线程所执行字节码的行号指示器,通过改变计数器的值来选取下一条需要执行的字节码指令。对于多线程,在任何一个时刻,一个处理器只能执行一个线程中的一条指令。为了使线程切换后能回到切换前的执行的位置,因此需要使用一个和线程相关的计数器来记录执行的位置。即程序计数器。
程序计数器是线程私有的,唯一一个不会发生OOM的区域。执行Java方法时记录的是字节码指令的地址,执行Native方法时计数器为Undefined。
Java虚拟机栈:描述的是Java方法执行的内存模型.。方法的调用->执行完成 对应的是 栈帧的入栈->出栈 的过程。栈帧存放的是局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表所需的内存空间在编译期间完成分配。
Java虚拟机栈是线程私有,生命周期和线程一致。会抛出两种异常:
- OutOfMemoryError:创建Java虚拟机栈的时候内存空间不够了。可能是由于创建过多的线程,或者是由于每个Java虚拟机栈所需要的内存空间太大所导致。
- StackOverFlowError:调用方法时,会将创建的栈帧压入。栈满时,会抛出此异常。可能是由于方法调用的层数太深,或者是由于创建的栈空间太小所导致。
Java堆:所有线程共享的内存,绝大多数的对象都存在堆上。虚拟机启动时创建Java堆。可以划分为:新生代(Eden、From survivor、To Survivor)和老年代,从分配的角度可以分出多个线程私有的TLAB。合理的划分可以提高分配和回收的效率。
可以通过-Xms和-Xmx来调整堆的初始化大小和最大时的大小,一般将其设置成一致的防止内存抖动。空间不够用时会抛出OOM异常。
方法区:用于存储类信息、常量、静态常量、编译后的代码等数据。此区域的内存回收目标主要是针对 常量池的回收 和 对类型的卸载。空间不足时抛出OOM。运行时常量池是此区域的一部分。运行时常量池具有动态特性,可以在运行时向常量池中添加。如调用String的inter()方法。
对象的创建:
- 检查参数对应的符号应用所代表类是否被加载、解析、初始化。如果没有就进行加载。
- 分配空间:加载后需要的空间大小就确定了,可以使用 指针碰撞 或 空闲列表的方法进行分配。指针碰撞需要的内存空间连续,使用标记整理法回收。空闲列表不连续也可以,查表找出没有被用的空间,然后进行分配。 多线程同时分配会造成的不安全问题,可以进行同步或者再TLAB区里分配。
- 设置零值,确保实例字段在Java代码中可以不赋初值就能使用。
- 设置对象头的信息:元数据信息、对象的Hash码、对象的GC年龄等。
- 执行构造方法,按照程序员的意愿进行初始化。
对象的构成:
- 对象头:存放哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向锁ID、偏向时间戳等。还有一部分是类型指针,指向它的类元数据。如果是数组,还有一块空间用于记录数组长度。
- 实例数据:存储对象的有效信息,有自己的、从父类继承来的。相同宽度的字段总是总是被分配在一起。
- 对齐填充:对象的大小是8字节的整数倍,需要通过这部分来控制。
对象的访问定位:
- 句柄式:在内存中划分出一块空间作为句柄池,引用存储的就是句柄池的地址。句柄池存储了又存储了对象的实例数据与类型数据。 优点是对象在频繁移动时(比如垃圾收集时)reference中存储的是稳定的句柄地址。不需要修改。
- 直接指针式:reference直接指向对象的地址,对象头中的类型指针指向类型数据。 优点是访问速度提升,节省了一次指针定位的时间开销。是sun HotSpot虚拟机的实现方式。
OutOfMemoryError异常:
- 内存溢出:创建对象的过程中,堆的剩余空间不足,堆中的对象都还有用途,不能被回收。
- 内存泄漏:创建对象的过程中,堆的剩余空间不足,堆中应该被回收的对象由于与GC Roots的引用链相连所以导致没有被回收。
- 堆溢出:通过-Xms与-Xmx调整堆内存的大小。
- 栈溢出:通过-Xss参数设定,太小容易发生StackOverFlowError异常,太大容易发生OutOfMemory异常
- 方法区和运行时常量池的溢出:String.inter()是个Native的方法。可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小。间接限制常量池的大小。大量使用反射、动态代理、CGLib字节码增强时,需要调大一点。
String.inter()方法:
- JDK1.6中,会把首次遇到的字符串实例复制到永久代中,返回永久代中这个字符串实例的引用。
- JDK1.6之后,不会再复制实例,而是常量池中记录首次出现的实例引用,调用此方法时,返回这个引用。
---THE END---