程序计数器
作用: 字节码解释器工作时通过修改计数器的值来选择下一条需要执行的字节码指令,因为虚拟机是通过多线程来切换并分配处理器执行时间的方式来执行的,因此 为了线程切换后能恢复到正确的位置,每一个线程需要有一个独立的程序计数器,各个计数器互不影响,在线程内独立存储。
虚拟机栈
和程序计数器一样,虚拟机栈也是线程私有的,其生命周期与线程一样每个方法在执行的同时都会创建一个栈帧,栈帧用于存储 局部变量表,操作数栈,动态链接 方法出口等等。每个方法从开始到完结 就对应了栈帧在虚拟机栈的入栈与出栈过程。
局部变量表所需要的内存空间在编译器完成分配,是完全确定的 ,运行时不会改变局部变量表的大小。
虚拟机中的两种异常:StackOverFlowError和OutOfMemoryError。
StackOverFlowError:如果线程请求栈帧的深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常。
OutOfMemoryError: 如果虚拟机栈客园扩展(大部分虚拟机都可以动态扩展),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
2种异常的理解:StackOverFlowError 就是方法调用的的太深了 比如深层递归 自己调用自己
public class StackOverflowTest { public static void main(String[] args){ method(); } private static void method() { method(); } }
OutOfMemoryError:表示创建对象的速度快于JVM回收空间,就会发生这个异常,比如死循环创建对象
public class OutOfMemoryTest { public static void main(String[] args){ List<Object> list = new ArrayList<Object>(); while(true){ int[] index = new int[20_0000_0000]; list.add(index); } } }
本地方法栈
本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法区是本地方法运行的内存模型,本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表等等。
堆
堆是java虚拟机所管理内存最大的一块,是被所有线程共享的内存区域,在虚拟机启动的时候创建,用于存放对象实例和数组。
根据分代算法可以分为新生代和老年代,再细致一点 可以分为Eden、From Survior、To Survior 空间。
堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的(-Xmx,Xms来控制),因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError异常。
方法区
方法区与堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量 静态变量,即时编译器编译后的代码等。
方法区的信息一般是永久存在的 因此我们也称之为永久代,因为需要长期存在 所有GC在此区域回收效率较低,另外这里还允许不实现垃圾回收。
运行时常量池
方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码。其中常量存储在运行时常量池中,常量池是方法区的一部分。
当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。 当常量池无法申请到足够的内存时也会抛出OutOfMemoryError异常。
当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。
直接内存
直接内存是除Java虚拟机之外的内存,但也有可能被Java使用,在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率,直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OOM异常。
HotSpot虚拟机创建对象(普通java对象,不包括数组和Class对象)
1 当虚拟机遇到一条含有new的指令时 首先会去检查常量池中是否有即将要创建的这个对象所属的类的符号引用,如果没有会报ClassNotFoundException.。
2 然后检查这个符号引用对应的类是否已经被类加载器加载过 如果没有那么进行类加载过程, 类加载之后进行对象的内存分配过程。
3 对象所需内存大小 在类加载之后就可以确定
分配堆中内存有两种方式
1 指针碰撞
如果java堆中内存是绝对规整的,使用的内存放在一边,未被使用的内存放在一边 中间放着一个指针作为分界点指示器。那么分配内存的过程就是把那个指针移动即可,这种方式称为指针碰撞。
2 空闲列表
如果java堆中内存不是绝对规整的,已经使用的和未被使用的内存交错排列,那就没办法进行简单的指针碰撞 ,虚拟机需要维护一个表 记录哪些内存是已经使用的 哪些是未被使用的,分配内存时 划分一块足够的内存给对象 然后更新表记录,这种方式称之为空闲列表。
综上所述:JVM究竟采用哪种内存分配方法 取决于内存是否规整。
在进行内存分配的时候 不是线程安全的,可能正在给A分配内存 指针还未来得及进行修改,对象B 又同时使用原来的指针来分配内存,一般使用两种方法来解决这个问题:
1 虚拟机采用CAS+失败重试来保证更新操作的原子性 2 为每个线程堆预先分配内存 后面再同步锁定。
4 为对象中的成员变量赋上初始值(默认初始化);
5 设置对象头中的信息;
6 调用对象的构造函数进行初始化 .
对象在内存中布局分为三个部分:·对象投 实例数据 对齐填充
对象头中记录了对象在运行过程中所需要使用的一些数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。 此外对象头还可能包含类型指针 通过该指针能确定这个对象所属哪个类。此外,如果对象是一个数组,那么对象头中还要包含数组长度。
实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。
对齐填充不是必然存在的,也没有特别的含义 仅仅是一个补全的作用。
对象的访问定位
1 句柄访问方式
堆中需要有一块叫做“句柄池”的内存空间,用于存放所有对象的地址和所有对象所属类的类信息。
引用类型的变量存放的是该对象在句柄池中的地址。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址再访问对象。
2 直接指针访问方式
引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。
但对象所在的内存空间中需要额外的策略存储对象所属的类信息的地址。
HotSpot采用直接指针方式访问对象,因为它只需一次寻址操作,从而性能比句柄访问方式快一倍。但它需要额外的策略存储对象在方法区中类信息的地址。
各种OutOfMemoryError异常例子
堆溢出