Chinaunix首页 | 论坛 | 博客
  • 博客访问: 15809
  • 博文数量: 9
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 115
  • 用 户 组: 普通用户
  • 注册时间: 2013-12-25 10:41
文章分类

全部博文(9)

文章存档

2014年(1)

2013年(8)

我的朋友

分类: Java

2013-12-29 22:44:59

这一部分来了解一下JVM如何分配它所管理的内存

了解虚拟机的目的

C/C++开发者拥有对象“所有权“,同时负责对象生命周期从创建到销毁的维护,
对于Java程序员,JVM(Java虚拟机)自动管理内存,但是避免不了内存泄漏和溢出,了解虚拟机是为了(内存泄漏和溢出)排查错误,我们知道JVM的主要作用是负责编译Java源代码,然后在不同的平台上把字节码解释成具体的机器指令来执行,从而实现跨平台

JVM如何管理内存
JVM在执行Java程序的过程当中,会把它所管理的内存划分为若干个不同的数据区域


下面分别说说每一部分
程序计数器一块较小的内存空间,代表当前线程执行的字节码的行号指示器
根据JVM的概念模型,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。JVM是多线程的,而多线程是通过轮流切换时间片的方式实现,任何一个确定的时刻,只能执行一个线程中的指令,线程之间互相切换就是根据每个线程独有的程序计数器来恢复到正确的执行位置和状态。所以各线程的程序计数器独立存在,互不影响,是"线程私有"内存,如果:

    线程执行Java方法,那么计数器记录正在执行字节码指令的地址
    线程执行Native方法,那么计数器为空(Undefined)
程序计数器是JVM规范中没有规定任何OutOfMemoryError异常情况的区域

Java虚拟机栈线程私有”的,它的生命周期与线程相同。这块内存区域描述的是Java方法执行的内存模型:一般每个方法执行的时候都会同时创建一个栈帧(Stack Frame)用来存储局部变量表、操作栈、动态链接、方法出口等信息,一个方法被调用直至执行完成的过程就是一个栈帧在虚拟机中从入栈到出栈的过程
    有人把Java内存区划分为"堆内存“(Heap)”和“栈内存(Stack)”,这是很粗糙的分法。其实人们口中的“栈内存(Stack)” 就是Java虚拟机栈,更详细的说,指的是 Java虚拟机栈中的局部变量表
    局部变量表里面存放:
    1. 存放了编译期可知的8种基本数据类型(boolean,byte,char,short,int,float,long,double)
        long和double占据2个局部变量空间(slot),其他的基本数据类型占1个
    2. 对象的引用(reference类型)
    3. returnAddress类型
    (JVM有一个只在内部使用的基本类型:returnAddress,Java程序员不能使用这个类型,这个基本类型被用来实现Java程序中的finally子句。该类型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作码的指针returnAddress类型不是简单意义上的数值,不属于8种基本类型,并且它的值是不能被运行中的程序所修改的)
    局部变量表所需要的内存空间在编译期就能决定并且完成分配,进入一个方法时需要在帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小

Java虚拟机栈可抛出如下异常
StackOverflowError异常:线程请求栈深度大于虚拟机所允许的深度时报出此异常
OutOfMemoryError异常:虚拟机栈可以扩展,当扩展时无法申请到足够的内存时报出此异常

本地方法栈这块内存区域和Java虚拟机栈发挥的作用很相似,不同的是Java虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的native方法服务,同样会抛出StackOverflowError异常和OutOfMemoryError异常

Java堆(Heap):JVM管理的内存中最大的一块,是“线程共享”的区域,在JVM启动的时候被创建。堆的唯一目的就是存放对象实例,几乎所有的对象都在这块区域里分配内存,为什么说是“几乎”呢?JVM规范的描述是这样的:所有对象的实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了
    堆还是GC管理的主要区域,所以这块区域很多时候也被称为“GC堆”(“垃圾收集器堆”,不要叫成“垃圾堆”啊!)
    从内存回收的角度看,现代的GC算法都是分代收集算法,所以堆还可以分成:新生代和老年代,再分还有Eden空间,From Survivor空间,To Survivor空间等,具体内容请参考深入理解Java虚拟机-JVM高级特性与最佳实践-阅读笔记(第三章-2)
    从内存分配的角度看,线程共享的这块Java堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)    我们知道堆内的对象数据是各个线程所共享的(对象创建完成后才是共享的),当在堆内创建新的对象时,是需要进行锁操作的,锁操作也是比较耗时的,因此JVM为每个线程在堆上分配了一块“自留地(或称为-线程本地分配缓冲区)”——TLAB,位于堆内存的新生代,也就是Eden区。每个线程在创建新的对象时,会首先尝试在自己的TLAB里进行分配,如果成功就返回,失败了再到共享的Eden区里去申请空间。在线程自己的TLAB区域创建对象失败一般有两个原因:一是对象太大,二是自己的TLAB区剩余空间不够。通常默认的TLAB区域大小是Eden区域的1%
    最后一点,堆在逻辑上是连续的,但是在物理上可以是不连续的内存空间。它可以固定大小也可以扩展。在堆上为实例分配内存时,当没有足够的内存完成实例分配,并且堆也无法再扩展的时候,就会抛出OutOfMemoryError异常

方法区是“线程共享”的区域。存储已经被JVM加载的类信息,常量,静态变量,即时编译器编译后的代码等,为了和堆区分开,它的别名叫Non-Heap(非堆)。它的特性也和堆很类似:不需要连续的物理内存,可以固定大小也可以扩展,而且还可以选择不实现垃圾收集,垃圾收集在这块内存中很少出现(很少出现不代表就不出现,不代表数据在方法区是永久存在的,它不同于“永久代”),这个区域的内存回收目标会主要集中针对常量池的回收和类型的卸载,同样方法区无法满足内存分配时一样会抛出OutOfMemoryError异常

运行时常量池(Runtime Constant Pool):这是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等信息以外,还有一个信息就是常量池(Constant Pool Table),它用来存放编译器生成的各种字面量和符号引用,然后这部分内容在类被加载的时候存放到方法区的运行时常量池里面
    另外,运行时常量池具有动态性,因为常量不一定都在编译时产生,运行时也会生成新的常量,比方说String的intern()方法,这种运行时的常量也会被放入运行时常量池,当然,如果这个时候没有足够的内存,会抛出OutOfMemoryError异常

直接内存(Direct Memory):不是JVM运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存一直被频繁的使用,而且可能导致OutOfMemoryError异常
    在JDK1.4之后,新加入了NIO类,NIO是java nonblocking(非阻塞)的简称,它为所有的原始类提供了缓存支持(buffer),基于通道(channel),这个新特性可以让native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作,这样一来,Java堆上有一个DirectByteBuffer引用到Java对外的一个直接由native函数库分配的内存,避免了在Java堆和native堆上来回复制数据,提高了性能。由于直接内存是在堆外的一块内存,而服务器管理员分配虚拟机参数时,往往会忽略掉这块内存,换句话说如果加上直接内存,各个内存区域的总和会大于实际的物理内存,这个时候如果用直接内存来分配空间,很可能会导致动态扩展有问题,从而出现OutOfMemoryError异常

对象访问
了解了内存分配,我们来看下面的代码,比方说在一个方法中出现下面的代码

  1. Object obj = new Object();
Object obj反映到Java栈的本地变量表中,作为一个reference类型数据出现
new Object()反映到堆上,形成了一块结构化内存,存储了
1). Object类型所有实例数据值(Instance Data)
2). 查找对象类型数据的地址信息(注意:对象的实际类型数据存储在方法区上面)
方法区中,Object对象的类型数据(对象类型,父类,实现接口,方法等)
reference类型在JVM规范中是指向对象的引用,不同的JVM通过reference定位对象的方式不一样,主要有两种:句柄和指针
1. 句柄的访问方式:这个时候,Java堆上会划分一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中又包含了对象实例数据和类型数据各自的具体地址

2. 指针的访问方式:这个时候,reference存放的就是对象的地址,但是Java堆上的对象的布局就必须考虑如何存放对象类型数据的信息

优势分析
句柄的好处:reference中存储的是稳定的句柄地址,对象被移动的时候,只会改变句柄中实例数据指针,reference本身不需要被修改
指针的好处:指针的好处就是速度快,因为它的地址就是对象的地址,节省了一次指针定位的时间开销,随着对象访问的频繁,这种开销积少成多很可观
书中讨论的主要VM HotSpot用的是指针的方式

阅读(1790) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:关于Java String

给主人留下些什么吧!~~