java内存区域
运行时数据区域
包括方法区,虚拟机栈,本地方法栈,堆,程序计数器。其中,方法区和堆是所有线程共享的,其他的为线程隔离的。
程序计数器
可以看作是当前线程所执行的字节码的行号指示器。因为cpu在一个时刻只能执行一个线程中的指令,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器
执行java方法,记录正在执行的虚拟机字节码指令地址
执行native方法,计数器值为空
唯一一个没有oom情况的区域
java虚拟机栈
线程私有,生命周期与线程相同。
每一个方法执行的时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等消息,每个方法调用到完成的过程,就是栈帧在虚拟机栈中入栈出栈的过程。
平时所说的堆内存就是堆内存,栈内存就是虚拟机栈中局部变量表部分
局部变量表
- 局部变量表存放编译器可知的基本数据类型,对象引用和returnAddress类型。
- 局部变量表所需的内存空间在编译期间完成分配,也就是说,我们在进入一个方法时,这个方法需要的空间是完全确定的。运行期间不会改变。
会出现下列情况:
线程请求的栈深度大于虚拟机允许的深度:*
虚拟机栈扩展时申请不到足够内存:oom
本地方法栈
和虚拟机栈相似,只不过虚拟机栈为执行java方法服务,本地方法栈为native方法服务。
java堆
所有线程共享,虚拟机启动时创建。此内存区域唯一目的就是存放对象实例,所有对象实例和数组都在这里分配内存的
堆是垃圾收集器的主要作用区域。由于收集器采用分代收集算法,所以堆分为新生代和老年代。这里只考虑内存区域的作用
堆可以是物理上不连续,但是逻辑上必须连续。可以通过-Xmx控制堆的内存扩展
方法区
线程共享,存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等
垃圾收集在这里比较少出现。内存回收主要是针对常量池的回收和对类型的卸载,回收还是有必要的。
运行时常量池
是方法区的一部分。class文件中有类的版本,字段,方法,接口,还有一个就是常量池,用于存放编译期生成的各种字面量和符号引用,他们都要在类加载后放在运行时常量池中。
运行时常量池和class常量池不同的一个重要特征就是具备动态性,并不是只有编译期的常量能进去,运行以后也能进去。比如string的intern方法。
总结一下就是,共享的堆存放所有的对象实例,方法区存放已经加载了的类的信息,常量,静态变量。 线程私有的虚拟方法栈和本地方法栈都是针对方法的,并且方法中的东西都存在里面,最后一个程序计数器作为线程切换的保证。
虚拟机对象揭秘
下面讨论虚拟机hotspot在堆上对象分配,布局和访问的全过程。
对象的创建
这里的对象只限于普通java对象,不包括数组和class对象。下面开始分析
虚拟机遇到了new指令——>检查能否在常量池中定位到一个类的符号引用,检查该类是否已被加载,解析和初始化过,如果没有,先加载类,否则——>为新生对象分配内存,大小在类加载完成后便可完全确定—->将分配到的内存空间初始化为零值(不包括对象头)——>将对象进行必要的设置,把这些信息放在对象头中。—–>执行方法,也就是把当时初始化为零的的内存空间进行初始化
对象的内存布局
对象在内存中存储布局分为:对象头,实例数据,对齐填充。
对象头包含两部分,一部分是存储对象自身的运行时数据,另一部分是类型指针,虚拟机通过这个指针判断这个对象是哪个类的实例
实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。包括父类和子类的。字段存储顺序受虚拟机分配策略和字段定义顺序的影响
对齐填充起占位符的作用。
对象的访问定位
我们对于对象的使用往往是通过引用来使用对象。引用是放在虚拟机栈的局部产量表中,而对象确实放在堆中对象的主流访问方式有句柄和直接指针两种。
句柄访问:堆中划分出一块来作为句柄池,栈中的引用指向句柄池中的指针,这个指针不但有指向堆中实例池的指针,也有指向方法区中的对象类型数据。
直接指针:引用直接存放对象地址。
内存溢出
- 堆溢出:对象太多内存溢出
- 栈溢出:单个线程下,无论是栈帧太大还是虚拟机栈容量太小,当内存无法分配时,都是srackoverflow异常。
- 方法区和运行时常量池溢出