Java内存区域
1、运行时数据区域
Java虚拟机在Java程序运行时还会把它所管理的内存划分为不同的数据区域。
分别有:方法区、虚拟机栈、本地方法栈、程序计数器、堆
1、程序计数器
(线程执行到哪了?)
当前线程所执行的字节码的行号指示器。字节码解释器的工作就是通过改变计数器的值来选取吓一跳需要执行的指令。计数器是程序控制流的指示器
每一条线程都要有一个独立的程序计数器,属于线程私有的内存
2、Java虚拟机栈
虚拟机栈也是线程私有的,生命周期与线程相同
Java中的方法被执行的时候,会创建一个栈帧。Java中的每一个方法被调用直至执行完毕就对应着一个栈桢在虚拟机栈从入栈到出栈的过程
虚拟机栈中还有局部变量表
3、Java堆
被所有线程共享的一块区域,虚拟机启动时创建。该区域唯一目的:存放对象实例。所有的对象实例都在这里分配内存
Java堆可以处于物理上不连续的空间,但在逻辑上它应该被视为连续的。只不过有一些大数组对象,多数的虚拟机为了简单高效,会要求连续的内存空间
垃圾收集器管理的最大的内存区域
4、方法区
线程共享区域,用于存储已被虚拟机加载的类型信息、常量、静态变量
垃圾收集机制在这个区域很少出现,该区域可以选择不实现垃圾收集
5、运行时常量池
是方法区的一部分
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,放的是编译期间生成的字面量、符号引用,将在类加载后存放到方法区的运行时常量池
2、HotSpot虚拟机
(这一部分以HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程)
1、对象的创建
new对象,当虚拟机读取到一条字节码new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查该符号引用代表的类是否已经被加载。如果被加载了就ok,没加载先加载引用类
内存空间分配:类加载通过后,为新生对象分配内存,就是把一块和对象所需大小一致的内存空间从堆中划分出来。有两种分配机制,一种是“指针碰撞”,一种是“空闲列表”。使用哪一种分配机制取决于堆空间的垃圾回收器采用的是哪种,如果是带有空间压缩整理能力的垃圾收集器,就是用“指针碰撞”分配机制,如果只是基于清除算法的垃圾收集器,就只能用复杂的空闲列表来分配内存
还需要考虑的一个问题:分配对象非常频繁,在并发情况下线程不安全。两个解决方案:一、对分配内存空间的动作进行同步处理,CAS配上失败重试,保证更新操作的原子性。二、每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程需要分配内存,就在自己的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定
然后虚拟机对对象进行一些必要的设置,例如:该对象是谁的实例、对象GC分代年龄、对象的哈希码。这些信息都存放在对象的对象头中
以上工作完成之后,虚拟机角度来看对象创建完成了,程序角度来看对象创建刚刚开始,然后要调用构造对象等操作
2、对象的内存布局
对象在堆中的存储分三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头:分为两部分信息。一、用于存储对象自身运行时数据(如哈希码、GC分代年龄、锁状态标志等),32位计算机占32bit,64位计算机占64bit。二、类型指针,对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是那个类的实例。此外,如果对象是数组,对象头中必须有一块用来记录数组长度的数据
实例数据:是对象真正存储的有效信息,放的是我们代码里所定义的各种类型的字段内容(包括从父类中继承下来的)
对齐填充:不是必要的,只不过HotSpot虚拟机要求对象起始地址是8字节的整数倍,而对象头已经被精心设置为8字节的整数倍,所以,如果对象的实例数据没对齐8字节,就需要对齐填充来占位
3、对象的访问定位
对象创建出来肯定是要用的,就需要访问定位对象了。Java程序会通过栈上的reference数据来操作堆上的具体对象。
主流的访问方式有两种:使用句柄和直接指针。如果是访问句柄的话,Java堆中划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息;如果是直接指针访问,reference中存储的直接就是对象地址,不需要(像句柄一样)多一次访问开销
使用直接访问最大的好处就是速度快,节省了一次指针定位的时间开销,这类开销积少成多的话,成本也很高。HotSpot用的就是直接指针来访问定位对象,其他虚拟机用句柄的也很多
3、内存区域
1、运行时数据区域
方法区:类信息、常量池、方法信息
堆:新生代、老年代,对象放在堆区中
java方法栈:栈帧(栈帧中有局部变量表、操作数栈),Java中的一个方法对应一个栈帧
本地方法栈:存本地方法信息(不是用Java代码写的)
程序计数器:记录当前程序下一条指令的地址
方法区和堆是线程共享的,Java方法栈和本地方法栈、程序计数器是线程私有的
2、程序计数器
不会出现内存溢出,是物理寄存器的抽象实现
3、虚拟机栈
-
有一个线程就会创建一个虚拟机栈,当前线程中执行一个方法就会创建一个栈帧放入栈中。执行一个方法的时候创建一个栈帧,先把所有要执行的方法都入栈,然后要执行的时候,从栈顶开始执行,执行完一个就把栈帧出栈一个
-
这块内存区域可能会出现两种异常,一种是StackOverflowError,还有一种是OutOfMemoryError。第一种是栈溢出,原因是一个虚拟机栈中的栈帧太多导致。第二种异常是因为线程太多导致虚拟机栈太多,然后内存溢出了
-
-Xss参数来设置虚拟机栈的大小
-
虚拟机栈不需要垃圾回收,随着线程执行完方法之后就自动把栈回收了