1.JVM优化-JVM运行时数据区域

       内存动态分配和垃圾收集技术是JVM的关键技术;

运行时数据区域

       Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域;

       每个区域都有各自的用途,以及各自的创建和销毁时间;

       有些区域随着虚拟机进程的启动而建立,有些区域则依赖于用户线程的启动和结束而建立和销毁;


       程序计数器用于存放正在执行的指令的地址,Java虚拟机栈和本地方法栈用于存放函数调用的堆栈信息,堆用于存放Java程序运行时所需的对象等数据,方法区用于存放程序的类元数据信息;

程序计数器(Program Counter Register)

      程序计数器是一块较小的内存空间,可以把它看作是当前线程所执行的字节码的行号指示器

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

      当线程数量超过CPU数量时,线程之间根据时间片轮询抢夺CPU资源;在任何时候,一个处理器内核都只会执行一条线程中的指令,而其他线程会被切换出去;

      因此,为了线程切换后能够恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,用于记录正在执行的指令的地址;

      每个线程的程序计数器互不影响,独立存储,是线程私有的。

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

       所以,程序计数器的这块内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

Java虚拟机栈(Java Virtual Machine Stacks)

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

      Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,常量池引用,动态连接方法,返回地址等信息。

       每个方法从调用直至执行完成,这个过程就对应着一个栈帧在Java虚拟机栈中入栈到出栈的过程。

       我们平常说的,"堆"指的就是堆,"栈"指的就是Java虚拟机栈,准确来说是虚拟机栈中的局部变量表部分;当然这种说法比较粗糙;


-----------------------------------------------

      局部变量表存放方法的参数和方法内部的局部变量。

      局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char等),对象引用(reference类型,它不等于对象本身,是一个指向对象起始地址的引用指针,或是指向一个代表对象的句柄或其他与此对象相关的位置),还有returnAddress类型(z指向一条字节码指令的地址)。

      局部变量表以"字"为单位进行内存划分,一个字为32bit的长度;其中,64位的long和double类型的数据将会占用2个局部变量空间(Slot),其余的数据类型只占用1个;

      调用方法时,是通过局部变量表来完成参数传递的,而且当调用非static方法时,会通过局部变量表传递this对象;

      局部变量表所需的内存空间在编译期间就完成分配,当进入一个方法时,这个方法所需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间是不会改变局部变量表的大小的;

----------------------------------------------

       局部变量表中的字空间是可以重用的,因为在一个方法体内,局部变量的作用范围并不一定是整个方法体。

public class TestWordReuse {
   public void test1(){
       {
         long a=0;  //a的作用域只限于最近的大括号中
       }
       long b=0;    //所以,在定义b时,a已经没有意义了,b可以重用a的空间;所以,其最大局部变量表为2+1=3个字
   }

   public void test2(){
       long a=0;
       long b=0;  //a与b的作用范围相同,不存在重用的可能,其最大局部变量表为2+2+1=5个字
   }
}

       局部变量表的字空间重用对系统GC是有影响的。按上面的例子test1,定义变量b的时候,a已经失效。如果这个时候,未能有足够多的局部变量来复用a所占的子空间。

       那么,在这个函数结束之前,这块内存区域是不会被回收的。但你可以把a=null来帮助系统释放这个内存区域;

----------------------------------------------

       可以通过 -Xss 这个虚拟机参数来指定一个程序的栈内存大小:java -Xss=512M HackTheJava;

       栈的大小直接决定了函数调用的可达深度;函数嵌套调用的次数由栈的大小决定,栈越大,嵌套调用的次数就越多;对于一个函数而言,它的参数越多,内部局部变量越多,它的栈帧就越大,其嵌套调用的次数就越少;

       当线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError异常;

      如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可以动态扩展),扩展时如果无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈(Native Method Stack)

       本地方法栈与虚拟机栈所起的作用是类似的,只不过虚拟机栈是为执行Java方法(也就是字节码)服务的,而本地方法栈是为虚拟机执行Native方法服务的;

       有些虚拟机(如 Sun HotSpot虚拟机)直接把本地方法栈和Java虚拟机栈合并在一起了。

       与Java虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。


Java堆(Java Heap)

       Java堆是虚拟机管理的最大的一块内存,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建;

       此区域是用来存放对象实例的,几乎所有的对象实例都会在这里分配内存;虚拟机规范中,是这样说的,所有的对象实例和数组都要在堆上分配;

注:但随着JIT编译器的发展与逃逸分析技术的成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也变得不是那么“绝对”;

       Java堆是垃圾收集器管理的主要区域,因此有时候也称它为“GC”堆(Garbage Collected Heap);

       从内存回收这个角度来看,由于现在收集器基本采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,所以Java堆中还可以细分为:新生代,老年代;

       当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。

       新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:Eden空间,From Survivor空间和To Survivor空间。


       而从内存分配的角度来看,线程共享的Java堆中可划分出多个线程私有的 分配缓冲区(TLAB,Thread  Local Allocation Buffer);

        无论怎么分,都与存放的内容无关,无论哪个区域,存储的都是对象实例;进一步划分都是为了更好地回收内存,或更快地分配内存。

        根据虚拟机规范,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就可以了。Java堆可以是固定大小的,也可以是可扩展的,这可以通过-Xmx和-Xms来控制。如果在堆中没有内存完成实例分配,并且堆也无法再扩展的时候,就会抛出OutOfMemoryError异常。

方法区(Method Area)

       是各个线程共享的内存区域,被用于存储已被虚拟机加载的类信息常量池,域信息,方法信息等;

       类型信息包括类的完整名称,父类的完整名称,类的修饰符(public/protected/private)和类的直接接口类表;

       常量池包括这个类方法,域等信息所引用的常量信息;

       域信息包括域名称,域类型和域修饰符;

       方法信息包括方法名称,返回类型,方法参数,方法修饰符,方法字节码,操作数栈和方法帧栈的局部变量表大小以及异常表;

       方法区的信息大部分来自class文件;

--------------------------------------------------

       虚拟机规范把方法区当做是堆的一个逻辑部分;

       很多人把HotSpot虚拟机上的方法区称为“永久代”(Permanent Generation),这是因为HotSpot虚拟机把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已;

       对这块区域进行垃圾回收的主要目标是对常量池的回收和对类元数据的回收

       只要常量池中的常量没有被任何地方引用,就可以回收了;

       对于类元数据,只要虚拟机确定这个类的所有实例已经被回收,并且加载该类的ClassLoader已经被回收,GC就可以回收该类元数据了。

--------------------------------------------------

       对于其他的虚拟机是不存在永久代的概念的。从现在来看,使用永久代来实现方法区并不是很好的做法。因为这样更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限)。

       在JDK1.7中,HotSpot已经把原本放在永久代中的字符串常量池移出了。

       方法区可以像Java堆一样不需要连续的内存,也可以选择固定的内存或可扩张,也可以选择不实现垃圾收集。相对而言,垃圾收集行为在方法区是比较少出现的。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

       当方法区无法满足内存分配需求时,将抛出OutOfMemory异常。

       Mark:关于方法区,JDK1.7以上版本发生的变化,还需要好好看看。

运行时常量池(Runtime Constant Pool)

       这是方法区的一部分(这一点,还待商榷)。

      Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,这是用于存放编译期生成的各种字面量和符号引用的,这部分内容将在类加载后进入方法区的运行时常量池中存放。

       不过,一般来说,除了保存Class文件中描述的符号引用外,还会把直接引用也存储在运行时常量池中。

       运行时常量池相对于Class文件的常量池的另外一个重要特征是具备动态性,Java并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件的常量池才能进入方法区的运行时常量池,运行期间也可以,例如String的intern()方法。

直接内存(Direct Memory)

       这并不是虚拟机运行时数据区的一部分,也不是虚拟机规范规定的内存区域。但这部分也有可能会发生OutOfMemoryError异常(因为物理内存也是有大小限制的)。

      主要与Java的NIO有关系,通过使用Native函数库直接分配堆外内存,存储在Java堆中的DirectByteBuffer对象直接引用堆外的一块内存。
       这样,就能避免在Java堆和Native堆中来回复制数据,以提高性能。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值