java的内存区域与内存溢出异常
一.程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。
如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是native方法,计数器为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。
多线程:在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。
多核处理器的思想是:若果你有多个程序运行在一台机器上,你可以让每个程序单独占据一个核,从而得到多倍的整体性能。
在使用多线程技术时考虑的是多线程,并没有提高执行效率,程序计算器一般情况下不会增加数量。
在使用分布式技术时考虑的是多个进程同时处理问题,根据核心数来共同执行不同进程,程序计数器一定会增加。
二.Java虚拟机栈
同样是线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法对应一个栈帧。
局部变量表存放了各种基本类型、对象引用和returnAddress类型(指向了一条字节码指令地址)。其中64位长度long 和 double占两个局部变量空间,其他只占一个。
规定的异常情况有两种:1.线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;2.如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。
三.Java堆
是Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。堆区唯一目的就是存放对象实例。
堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。
堆无法扩展时,抛出OutOfMemoryError异常
四.方法区
所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError
1、类型信息
类型的全限定名
超类的全限定名
直接超接口的全限定名
类型标志(该类是类类型还是接口类型)
类的访问描述符(public、private、default、abstract、final、static)
2、类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在动态链接中起到核心作用)。
3、字段信息(该类声明的所有字段)
字段修饰符(public、protect、private、default)
字段的类型
字段名称
4、方法信息
方法信息中包含类的所有方法,每个方法包含以下信息:
方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表
5、类变量(静态变量)
指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。
6、 指向类加载器的引用
每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。
7、指向Class实例的引用
类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
8、方法表
为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的,类似C++虚函数表vtbl。
9.运行时常量池
运行时常量池是方法区的一部分,存放编译生成的各种字面量和符号引用。对象创建的时候,首先去找常量池中是否有相同的对象,如果有,则直接引用。不用创建。
五.本地方法栈与虚拟机栈区别
本地方法调用的是虚拟机本地的方法,虚拟机栈调用的是编译之后的方法,无太大区别,所以hotspot虚拟机把他们合在了一起。
六.直接内存
在jdk1.4新加入了nio类,引入了一种基于通道于缓存区的i/o方式,它可以使用本地方方法直接分配堆内存,然后通过一个存储在java堆中的directByteBuffer对象作为这块内存的引用进行操作。
七.对象的创建及引用。
JAVA虚拟机创建对象的流程
JAVA对象的结构
空间分配
空间分配的两种方式
指针碰撞
当已分配空间被集中存放,已分配和未分配空间使用一个指针来标记时,分配新的空间只需要移动该空间即可,此方法为指针碰撞。适用于GC算法会做COMPACT的情况。
空闲列表
当已分配的空间是分散存放时,虚拟机必须维护一个记录了哪些内存块是可用的列表,此为空闲列表,需要分配新空间时只需要从该列表中获取。
解决空间分配线程安全问题的两种方式
CAS方式失败重试
顾名思义,遇上分配时的线程冲突时,会再次进行空间分配直至成功。实现简单直观但是效率较低
空间划分
为每个线程分配单独的一块空间,该空间只用来给该线程做创建对象分配空间时使用,这单独的空间被称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。当该线程的TLAB分配光了后,才需要同步锁定,效率较高。是否使用TLAB可以通过虚拟机参数指定。
设置对象头(对象管理的必要信息)
空间初始化成0后(初始化不包含对象头),需要针对对象设置一些必要信息,如:对象归属于哪个类、对象的hash码、对象的GC分代年龄等,这些信息都放在对象头中。根据虚拟机状态——如是否使用偏向锁,设置对象头。
对象头包含信息
对象头包含两部分信息:
-
-
- 运行时数据(hash码、GC分代年两、持有锁、锁标识状态、偏向线程ID),这部分数据被称为Mark Word。
- 类型指针,用来说明该对象是哪个类的实例。
-
设置对象实例数据
对象的实例数据主要是根据JAVA代码的编写生成的,包含包括父类在内的各种类型的字段,其字段安排的顺序受虚拟机实现及代码的编写影响。
虚拟机安排字段的方式
1、相同宽度的字段放在一起
2、父类的字段放在子类的前面
3、窄小的变量也会被安排在父类的字段空隙中(C++的内存安排规则,HotSpot VM是由C++语言编写)
填充字段
HotSpot VM要求对象的其实位置必须是8字节的整数倍,也就是说对象必须是8字节的整数倍,所以需要填充占位(这也是一句C++的规则来的)。
对象的定位
JAVA程序访问对象需要通过栈上的reference数据操作堆上的具体对象。reference对象要么通过指向句柄再指向对象实例,要么直接指向对象实例。
以句柄的方式访问
使用句柄方式访问对象实例,需要在堆中划分出一块儿句柄池,句柄与指针类似,记录了具体的实例所存放的地址。这种方式的好处是reference是稳定的,当对象实例地址改变时,只需改变句柄中的对象实例指针。
以指针直接访问
以指针方式访问对象实例,是通过reference直接指向对象实例,优点是速度快,因为比句柄访问方式少了一次寻址的过程。