<深入理解JVM>Note之运行时数据区

May 31, 2017


简介

1.运行时数据区:

  • 1.程序计数器:PC Register
  • 2.虚拟机栈:VM Stack
  • 3.本地方法栈:Native Method Stack
  • 4.堆:Heap
  • 5.方法区:Method Area
  • 6.运行时常量池
  • 7.直接内存

正文

1.程序计数器(PC Register)

程序计数器在JVM中是一块比较小的内存空间,在«深入理解Java虚拟机:JVM高级特性与最佳实践»中程序计数器看作当前线程所执行的字节码的行号指示器

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支 循环 跳转异常处理 线程恢复等基础功能都依赖这个计数器来完成

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,所以说在任何一个确定的时刻,一个处理器都只会执行一个线程中的指令.因此,为了线程切换的时候能够恢复到正确的执行位置,每条线程都有属于自己的程序计数器,各条线程之间计数器互不影响,独立存储,所以称这类内存区域为“线程私有”的内存

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个技术器值则为空

程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

程序计数器占用的可能为CPU寄存器或者操作系统内存

2.Java虚拟机栈(Java VM Stack)

Java虚拟机栈也是线程私有的,它的生命周期与线程相同.

  • 虚拟机栈描述的是Java方法执行的内存模型:每个Java方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储Java方法内的局部变量表,操作数栈,动态链接,方法出口等信息.每个方法从调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈到出栈的过程,当方法执行完毕对应的栈帧出栈,其所占用的内存也会自动被释放
  • 虚拟机栈占用的为操作系统内存,其在内存分配上非常高效

在虚拟机栈中规定了两种异常情况:

1.如果线程请求的栈深度大于虚拟机栈允许的深度,会抛出StackOverflowError错误,在Sun JDK中可以通过-Xss来指定其大小:

public class StackOverFlow{
	public static void main(String []args){
		new Thread(new Runnable(){
			public void run(){
				loop(0);
			}
			private void loop(int i){
				if(!=2000){
				   i++;
				   loop(i);
				}
				else{
				   return ;
				}
			}					
		}).start();
	}
}

当JVM参数设置-Xss 1K时,运行以上程序会报出:Exception in thread …. java.lang.StackOverflowError

2.现在绝大部分的虚拟机栈可以动态扩展(也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈(Native Method Stack)

1.本地方法栈的作用与Java虚拟栈的作用类似,只不过Java虚拟栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则是为虚拟机所使用到的Native方法服务.在虚拟机规范中对本地方法使用的语言,使用方式,数据结构并没用强制性的规定,因此可以自由的实现它(可以通过C/C++来实现,也可以通过Python/Ruby脚本动态语言来实现).

public class Object{
	private static native void registerNatives();
	static{
		registerNatives();
	} 
	......
}

现在Sun JDK使用的Hotspot虚拟机直接就把本地方法栈与Java虚拟机栈合二为一.

2.如Java虚拟机一样,本地方法栈也会抛出StackOverflowError,OutOfMemoryError异常

Java堆(Heap)

  • 1.Java堆(Java Heap)是虚拟机管理内存最大的一块区域,Java堆是被所有线程共享的一块内存区域,因此在堆上分配内存时需要加锁,这导致了创建对象的开销比较大,所以在堆上空间不足时触发GC,如果GC后空间还不足,则抛出OutOfMemoryError错误.
  • 2.当虚拟机启动时,Java堆就被创建,其唯一目的就是存放对象实例.在虚拟机规范中说到:所有的对象实例及数组都要在堆上分配(可以认为Java中所有通过new关键字创建出的对象的内存都在堆中分配),堆中的内存由GC回收
  • 3.在32位操作系统中Heap内存最大为2G,64位系统则无限制,其大小可以通过-Xms,-Xmx 来进行控制.-Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1GB;-Xmx为申请的最大Heap内存,默认为物理内存的1/4但小于1GB.默认当空余堆内存小于40%,JVM会增大Heap到-Xmx的指定的大小,可以通过-XX:MinHeapFreeRatio=s%来指定此比例;默认当空闲堆大于70%,JVM会减少Heap到-Xms的指定的大小,可以通过-XX:MaxHeapFreeRatio=s%来指定此比例
  • 4.为了更有效率的回收内存.Sun JDK1.2开始对堆进行分代处理:
  • 4.1.新生代(New Generation):大多数情况下Java程序中新建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常称为SO和S1或者From和To)

  • 4.2.旧生代(Old Generation 或 Tenuring Generation):用于存放新生代中多次垃圾回收依旧存活的对象,例如缓存对象内存分配与回收策略:

    • 4.2.1.大多数情况下,对象在新生代Eden区分配内存.当Eden区没有足够的空间时,虚拟机将会发起一次Minor GC

    • 4.2.2.大对象直接进入旧生代进行分配,所谓的大对象是指占有大量连续内存的java对象,比如很长的字符串及数组(如byte[]数组是典型的大对象)

    • 4.2.3.长期存活的对象进入旧生代,首先对象在Eden区分配,经过第一次Minor GC之后依旧存活并且能被Survivor Space容纳,将被移动到Survivor区域并且对象年龄设置为1.没经历一个Minor GC后对象存活对象年龄加1.当对象的年龄增加到一定程度时(默认15)对象会被保存在旧生代.可以通过设置参数-XX:MaxTenuringThreshold设置默认对象年龄

    • 4.2.4.Minor GC指的是在新生代进行垃圾回收的动作(所以Minor GC发生比较频繁,回收速度快),Full GC/Major GC是指在旧生代发生垃圾回收的动作(所以Full GC回收速度慢)

    • 4.2.5.动态年龄判断:虚拟机并不是一定要满足MaxTenuringThreshold值才能进入旧生代,若Survivor区相同年龄对象的总和大于Survivor空间的一半,则大于或等于此年龄的对象会进入旧生代

  • 5.会抛出OutOfMemory错误信息:
public class HeapOOMTest{
	public static void main(String []args){
		List<OOMTest> heapOomTest = new ArrayList<OOMTest>();

		while(true){
			heapOomTest.add(new OOMTest());
		}
	}

	static class OOMTest{
	}
}
java.lang.OutOfMemoryError: heap sapce.....

方法区(Method Area)

  • 1.方法区存放了要加载类的信息(名称,修饰符),类中的静态变量,类中被定义为final类型的常量,类中的Field信息,类中的Method信息,当我们运用反射或者其他方法访问getName,getField,isInterface等方法获取信息时,这些信息全部来自方法区
  • 2.方法区是全局共享的,在一定条件下也会被GC,当方法区要使用的内存超过其所允许的大小会抛出OutOfMemory的错误信息
  • 3.方法区对应这Permanet Generation,称为持久代,默认最小值是16MB,最大为64MB,可以通过-XX:PermSize及-XX:MaxPermSize来进行设置
  • 4.java堆分为:Heap区与Perm区,Perm区对应着就是方法区,存放类的基本信息,静态变量,常量.在物理上方法区是存储在堆中的,但是在逻辑上方法区被称为:‘非堆’.
  • 5.许多人认为方法区就是Perm区,对于HotSpot虚拟机来讲,只是设计人员把GC分代收集扩展到方法区.仅仅是利用Perm来实现方法区

运行时常量池

  • 1.运行时常量池是方法区的一部分,虚拟机加载一个类时,除了类的版本,字段,方法,接口等描述信息.还包括常量池.用于存放编译期生成的各种变量和符号引用.
  • 2.运行时常量池具有动态性,Java语言并不要求常量一定在编译期产生.程序运行期间也可以把变量放入常量池中,比如String.intern()方法:首先会去字符串常量池中查找相对应的字符串,如果字符串不存在,就会在字符串常量池中创建该字符串然后返回.
  • 3.字符串常量池是一个固定大小的HashMap,在Java6中字符串常量是放在Perm空间的,Java7中字符串常量池是放在Heap空间下:因为Perm区大小是有限的,通常只有几十MB,Heap区的大小只受制于机器的真实内存大小
  • 4.当常量池内存不足时,会抛出OutOfMemory错误信息
public class SringPoolOOM{
	public static void main(String []args){
		List<String> stringTest = new ArrayList<String>();
		int i = 0;
		while(true){
			stringTest.add(String.valueOf(i++).intern());
		}
	}
}
java.lang.OutOfMemoryError: heap sapce.....

直接内存

  • 1.在JDK1.4中新加入了NIO(New Input/Output)非阻塞的IO,引入了一种基于通道(channel)和缓存(Buffer)机制的IO方式,它可以直接利用Native库直接分配堆外内存,然后通过DirectByteBuffer对象作为这块内存的引用进行操作.这样能显著提高性能,因为避免了在Java堆与Native堆中来回复制数据
  • 2.直接内存的分配不会受到Java堆大小的影响,但是会受到机器内存的影响.
  • 3.会抛出OutOfMemory错误信息