参考:《深入理解Java虚拟机》周志明著
当前线程所执行的字节码的信号指示器。
JVM的多线程是通过线程轮流切换并分配处理器执行时间(操作系统)实现 因此每条线程都需要有一个独立的程序计数器,各线程互不影响,称之为“线程私有”的内存
字节码解释器工作时就是通过改变计数器的值来选取下一条要执行的字节码命令
唯一一个不会抛出任何内存溢出(OutOfMemoryError简称OOM)的区域
线程私有。
java方法执行的内存模型:每一个方法执行时都会创建一个栈帧,用于存储局部变量表,操作栈数,动态链接,方法出口等信息。每一个方法从调用直至完成对应着入栈和出栈。
局部变量表: 存放编译期可知的各种数据类型,对象的引用(reference)和returnAddress类型(指向了一条字节码指令的地址)。其中long和double 会占用2个局部变量空间(Slot),其余都只占用1个。所需内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。
会抛出StackOverflowError和OOM。
线程私有。
和虚拟机栈作用非常相似,区别在于本地方法栈为虚拟机用到的Native方法服务。
Native方法:
简单来说就是一个Java调用非Java代码的接口,该方法的实现由非Java语言实现。因为在虚拟机中并没有规范本地方法栈中使用的语言,使用方式和数据结构,因此虚拟机可以自由实现他,如偏向底层操作系统层的c语言之类的方法来实现。native static public void methodName();
除了不能与abstract修饰使用外,其他均可以。
会抛出StackOverflowError和OOM。
在虚拟机启动时创建,是所管理内存中最大的一块,被所有线程共享使用。
所有的对象实例以及数组都要在堆上分配
堆细分为:
- 新生代
- 老年代
新生代一般占据1/3的空间,且可再细分为:Eden空间,From Survivor空间,To Survivor空间。
无论怎么分,存储的都是对象实例,划分区域主要是便于垃圾回收。java堆可以在物理上不连续,逻辑上连续即可。
会抛出OOM
共享区
用于存储已被虚拟机加载的类的信息,常量,静态变量,即时编译器编译后的代码等数据,
Java虚拟机规范没有多大限制,甚至可以选在不实现垃圾回收,但是这一部分的回收是很有必要的
运行时常量池:
Class文件中有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这些内容在类加载后进去方法区的运行时常量池
除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中
具备动态性,运行期间也有可能有新的常量放入池中
public class test{
public static void main(String[] arg){
String str1 = new StringBuilder("计算机"),append("软件").toString();
System.out.println(str1.intern() == str1);//JDK1.6返回false,1.7返回true
String str2 = new StringBuilder("Ja"),append("va").toString();
System.out.println(str2.intern() == str2);//JDK1.6返回false,1.7返回false
}
}
JDK1.6的intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例。而JDK1.7只是在常量池中记录首次出现的实例引用,上诉代码中的"Java"已经出现过了。
会抛出OOM
- 当虚拟机遇到一条new命令时,先去检查命令参数是否能在常量池中定位到一个类的符号引用,并检查是否被加载,解析初始化过。若没有则先初始化。
- 检查通过后为其分配内存,两种方式:
指针碰撞: 假设Java堆中内存绝对规整,所有用过的内存放在一边,没有使用过的放在一边,中间放一个指针作为分界点的指示器,分配内存就是把这个指针向空闲的一边移动和对象同等大小的距离。(若垃圾收集器使用的是复制算法或者标记-整理例如,Serial,ParNew可以实现内存的绝对规整)
空闲列表: 若内存不是规整的,空闲与使用的内存相互交错,虚拟机就要在一张表上面记录哪些内存是可以用的,分配时从表中找到一块足够大的空间分配给对象(垃圾收集器使用的是标记-清除算法,例如CMS则使用空闲列表)
并发创建对象时分配地址避免正在分配还没来得及修改,下一个对象又使用了同样的指定分配地址的两种方法
·CAS配上重试保证原子性
乐观锁用到的机制就是CAS,Compare and Swap(比较并交换)。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
·把内存分配按照线程划分在不同空间中进行,即在线程创建之时预先在队中分配一小块内存,称之为本地线程分配缓冲
- 分配完内存之后,设置对象的必要信息即设置对象头,哪个类的实例,如何找到元数据,对象的哈希码,年龄代等信息
此时从JVM的角度来看一个对象已经创建完毕,但是从JAVA的角度来看要执行完
主要分为3个区域
I.对象头(Header)
第一部分:用于存储对象自身运行时数据:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等,官方称之为Mark Word
第二部分:类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,但并不是所有的都要求保留类型指针,也就是说查找对象的元数据信息不一定要经过对象本身,例如通过句柄访问
II.实例数据(Instance)
对象真正存储的有效信息。从父类继承的,子类自身定义的都要存储,相同宽度的字段总是被分配在一起,且父类中定义的变量会出现在子类之前
III.对齐填充(padding)
仅起占位作用,HotSpot要求对象的大小必须是8字节的倍数,对象头刚好是8的倍数,所以实例数据部分没有对齐时就需要填充
句柄访问:
在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象的实例数据与类型数据各自的地址信息。
该方式比较稳定,在对象被移动(垃圾回收使用标记-整理之类的算法就会产生移动,非常普遍的行为)时只会改变句柄中的实例指针,而不会修改reference本身。
直接指针访问: