JVM的内存区域划分:
  jvm的内存区域分为5部分:程序计数器,虚拟机栈,本地方法栈,堆跟方法区。
  程序计数器,虚拟机栈,本地方法栈三部分是线程私有的,堆跟方法区是公共的。
1、程序计数器
  是一块较小的内存区域,用于记录当前线程运行的位置,可以看做是程序所执行的字节码的行号指示器。如果正在执行的是一个java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是native方法,则计数器值为undefined。该部分也是jvm中唯一不会内存溢出的区域。说白了,以jvm规范的设计来讲,就是想内存溢出,也是没有机会的。
2、虚拟机栈
  就是我们通常所说的栈内存。线程中每个方法执行的时候都会创建一个栈帧,用于记录局部变量表,操作数栈,动态链接,方法出口等信息。
  在这里,如果线程请求深度大于虚拟机所允许的深度,将抛出StackOveflowError异常;如果动态扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。
  例如:无限递归调用。会导致不停的往当前线程的栈中添加栈帧,总会有栈太大,占满栈内存的时候,就会跑出StackOverflowError。
  《深入理解java虚拟机》一书中讲到,实际上jvm在这里的规定有点问题,因为当栈内存不够的时候,是因为已经使用过多还是栈空间太小,本质上是同一件事的两种描述。就是栈帧无限叠加,就会导致占用内存不断上升,从而申请内存,如果扩展到足够大,就会无法申请到足够的内存。但书中言明,经测试,单线程下无论由于栈帧太大还是虚拟机容量太小,抛出的都会stackOverflowError。但如果不限制单线程,在采用多线程情况下,只要为每个线程的栈分配的内存较大,就很容易产生OutOfMemoryError,但这跟jvm关于栈空间是否足够大的定义略有偏差。
出现StackOverflowError异常时,因为有错误堆栈信息可以阅读,相对来说较容易找到问题所在。而且使用虚拟机默认参数下,栈的深度大多数情况下达到1000-2000是很轻松的,对于正常的方法调用(包括真常递归)是足够的。但如果建立过多线程导致内存溢出,则需要考虑减少线程数量和更换64位虚拟机。若不能做到以上两者,则需要减少最大堆和减少栈容量,以换取更多的线程。
3、本地方法栈
  所发挥的作用跟虚拟机栈一样,只不过是jvm调用native方法时的栈而已。jvm规范对该部分所使用语言,使用方式以及数据结构并没有强制规定,因此不同的jvm实现是不一样的,甚至有的直接跟虚拟机栈合二为一(比如我们多数使用的HotSpot)。一样会抛出虚拟机栈的两种异常。
4、堆
  就是我们平常所说的堆内存,该部分主要用途就是存放对象实例。jvm规范中的描述是:所有的对象实例以及数组都要在堆上分配;但随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也不是那么“绝对”了。
这也是垃圾回收的主要区域,可以通过-Xmx跟-Xms控制大小。如果堆中没有内存完成实例分配,而且也无法扩展时,抛出OutOfMemoryError.
5、方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。这里也是我们通常所说的“永久代”。但实际上并不是进入该区域的对象就真的永久存活了,这里仍然有相应的垃圾回收机制,只是相对并不频繁;而且此区域的内存回收目标主要是针对常量池的回收和对象类型的卸载。同样会有OutOfMemoryError
6、直接内存
对,除了jvm规范中定义的以上5个区域,还有一块区域是被频繁使用的。jdk1.4中加入了new Input/Output类,引入了一种基于通道与缓冲区的IO方式,就是jdk的新io中的各种reader,writer,inputstream,outputstream之类的。如果该部分内存跟虚拟机各个内存区域之和大于物理内存容量,同样会出现OutOfMemoryError。
对象的创建跟访问:
  当虚拟机遇到new指令的时候,首先将去查询这个指令的参数是否能在常量池中找到这个类的符号引用,并检查这个符号引用是否被加载、解析跟初始化过。若没有,必须先执行相应的类加载过程。
  1、类加载检查通过后,将对新生对象分配内存。对象所需的内存大小,在类加载完成后就可以完全确定。分配空间就是把一块儿确定大小的内存从java堆中划分出来。分配新内存有两种方式:“指针碰撞”跟“空闲列表”。
指针碰撞:用过的内存放一边,没用过的放一边,new的时候,指针移动一定距离,用来创建新对象。
空闲列表:用过的跟没用过的混在一起,但有一个列表记录着哪些用了哪些没用,new的时候从列表找一块儿足够大的划分给对象实例,并更新列表。
选择使用哪种方式,是由java堆是否规整决定的,而java堆是否规整是由于所采用的垃圾收集器是否带有压缩整理功能决定的。因此:Serial,ParNew等有压缩功能的垃圾收集器一般使用指针碰撞,CMS这种标记清除算法的,通常是空闲列表。
  2、并发情况下的new操作,有两种方案:
  a、对分配空间的动作进行同步处理,一半虚拟机上采用CAS配上失败重试的方式保证更新的原子性。
  b、给每个线程分配一个缓冲区,每个线程在自己缓冲区new对象,满了再进行申请。可以设置是否采用该方式,默认是true;
对象的内存布局
  HotSpot虚拟机中,对象的的内存布局分为3部分:对象头,实例数据,对齐填充。
  对象头:包括2部分,一部分用于存储对象自身的运行时数据,如HashCode,gc分带年龄,锁状态,线程持有锁,偏向锁id,偏向锁时间戳等,该部分称为“Mark Word”;另一部分是类型指针,是用来确定这个对象是哪个类的实例用的,如果对象是数组,该部分还包括数组的长度;但由于对象的访问方式有句柄跟直接指针2种,因此有的虚拟机实现种没有类型指针这部分,这类虚拟机的该部分内容在句柄中。
至于句柄跟直接指针,句柄就是引用类型的变量指针执行句柄对象(句柄对象在句柄池中),句柄对象包含到对象实例的指针跟对象类型的指针两部分;直接指针就是变量指针直接指向变量对象,但变量对象的头信息中包含一个指向对象类型信息的类型指针;很显然,我们用的HotSpot使用的是后者。
05-28 21:25