java怎么制作游戏,看完这篇彻底明⽩了
Java虚拟机内存模型
Java虚拟机内存模型中定义的访问操作与物理计算机处理的基本⼀致!
洛克人x4
Java中通过多线程机制使得多个任务同时执⾏处理,所有的线程共享JVM内存区域main memory,⽽每个线程⼜单独的有⾃⼰的⼯作内存,当线程与内存区域进⾏交互时,数据从主存拷贝到⼯作内存,进⽽交由线程处理(操作码+操作数)。更多信息我们会在后⾯的《深⼊JVM—JVM类执⾏机制中详细解说》。
在之前,我们也已经提到,JVM的逻辑内存模型如下:
我们现在来逐个的看下每个到底是做什么的!
1、程序计数器
程序计数器(Program Counter Register)是⼀块较⼩的内存空间,它的作⽤可以看做是当前线程所执⾏的字节码的⾏号指⽰器。在虚拟机的概念模型⾥(仅是概念模型,各种虚拟机可能会通过⼀些更⾼效的⽅式去实现),字节码解释器⼯作时就是通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执⾏时间的⽅式来实现的,在任何⼀个确定的时刻,⼀个处理器(对于多核处理器来说是⼀个内核)只会执⾏⼀条线程中的指令。因此,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要有⼀个独⽴的程序计数器,各条线程之间的计数器互不影响,独⽴存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执⾏的是⼀个Java ⽅法,这个计数器记录的是正在执⾏的虚拟机字节码指令的地址;如果正在执⾏的是Natvie ⽅法,这个计数器值则为空(Undefined)。此内存区域是唯⼀⼀个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
2、Java 虚拟机栈
与程序计数器⼀样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的⽣命周期与线程相同。虚拟机栈描述的是Java ⽅法执⾏的内存模型:每个⽅法被执⾏的时候都会同时创建⼀个栈帧(Stack Frame ①)⽤于存储局部变量表、操作栈、动态链接、⽅法出⼝等信息。每⼀个⽅法被调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在虚拟机栈中从⼊栈到出栈的过程。
经常有⼈把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法⽐较粗糙,Java 内存区域的划分实际上远⽐这复杂。这种划分⽅式的流⾏只能说明⼤多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后⾯会专门讲述,⽽所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引⽤(reference 类型,它不等同于对象本⾝,根据不同的虚拟机实现,它可能是⼀个指向对象起始地址的引⽤指针,也可能指向⼀个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了⼀条字节码指令的地址)。
其中64 位长度的long 和double 类型的数据会占⽤2 个局部变量空间(Slot),其余的数据类型只占⽤1 个。局部变量表所需的内存空间在编译期间完成分配,当进⼊⼀个⽅法时,这个⽅法需要在帧中分配多⼤的局部变量空间是完全确定的,在⽅法运⾏期间不会改变局部变量表的⼤⼩。
在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度⼤于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前⼤部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时⽆法申请到⾜够的内存时会抛出OutOfMemoryError 异常。
3、本地⽅法栈
本地⽅法栈(Native Method Stacks)与虚拟机栈所发挥的作⽤是⾮常相似的,其区别不过是虚拟机栈为虚拟机执⾏Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则是为虚拟机使⽤到的Native ⽅法服务。虚拟机规范中对本地⽅法栈中的⽅法使⽤的语⾔、使⽤⽅式与数据结构并没有强制规定,因此具体的虚拟机可以⾃由实现它。甚⾄有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地⽅法栈和虚拟机栈合⼆为⼀。
与虚拟机栈⼀样,本地⽅法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。
4、Java 堆
对于⼤多数应⽤来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最⼤的⼀块。Java 堆是被所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,体现英雄行为的成语
⼏乎所有的对象实例都在这⾥分配内存。这⼀点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致⼀些微妙的变化发⽣,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(GarbageCollected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的⾓度看,由于现在收集器基本都是采⽤的分代收集算法,所以Java 堆中还可以细分为:新⽣代和⽼年代;再细致⼀点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的⾓度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread LocalAllocation Buffer,TLAB)。不过,⽆论如何划分,都与存放内容⽆关,⽆论哪个区域,存储的都仍然是对象实例,进⼀步划分的⽬的是为了更好地回收内存,或者更快地分配内存。在本章中,我们仅仅针对内存区域的作⽤进⾏讨论,Java 堆中的上述各个区域的分配和回收等细节将会是下⼀章的主题。
根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间⼀样。在实现时,既可以实现成固定⼤⼩的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也⽆法再扩展时,将会抛出OutOfMemoryError 异常。
4、⽅法区
⽅法区(Method Area)与Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但是它却有⼀个别名叫做Non-Heap(⾮堆),⽬的应该是与Java 堆区分开来。
对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多⼈愿意把⽅法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展⾄⽅法区,或者说使⽤永久代来实现⽅法区⽽已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本⾝,根据官⽅发布的路线图信息,现在也有放弃永久代并“搬家”⾄Native Memory 来实现⽅法区的规划了。
Java 虚拟机规范对这个区域的限制⾮常宽松,除了和Java 堆⼀样不需要连续的内存和可以选择固定⼤⼩或者可扩展外,还可以选择不实现垃圾收集。相对⽽⾔,垃圾收集⾏为在这个区域是⽐较少出现的,但并⾮数据进⼊了⽅法区就如永久代的名字⼀样“永久”存在了。这个区域的内存回收⽬标主要是针对常量池的回收和对类型的卸载,⼀般来说这个区域的回收“成绩”⽐较难以令⼈满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若⼲个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收⽽导致内存泄漏。
市场策划方案根据Java 虚拟机规范的规定,当⽅法区⽆法满⾜内存分配需求时,将抛出OutOfMemoryError 异常。
5、运⾏时常量池
运⾏时常量池(Runtime Constant Pool)是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述等信息外,还有⼀项信息是常量池(Constant PoolTable),⽤于存放编译期⽣成的各种字⾯量和符号引⽤,这部分内容将在类加载后存放到⽅法区的运⾏时常量池中。
Java 虚拟机对Class ⽂件的每⼀部分(⾃然也包括常量池)的格式都有严格的规定,每⼀个字节⽤于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执⾏。但对于运⾏时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照⾃⼰的需要来实现这个内存区域。不过,⼀般来说,除了保存Class ⽂件中描述的符号引⽤外,还会把翻译出来的直接引⽤也存储在运⾏时常量池中①。
运⾏时常量池相对于Class ⽂件常量池的另外⼀个重要特征是具备动态性,Java 语⾔并不要求常量⼀定只能在编译期产⽣,也就是并⾮预置⼊Class ⽂件中常量池的内容才能进⼊⽅法区运⾏时常量池,运⾏期间也可能将新的常量放⼊池中,这种特性被开发⼈员利⽤得⽐较多的便是String 类的intern() ⽅法。
既然运⾏时常量池是⽅法区的⼀部分,⾃然会受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛出OutOfMemoryError 异常6、直接内存
鼠标没反应直接内存(Direct Memory)并不是虚拟机运⾏时数据区的⼀部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤,⽽且也可能导致OutOfMemoryError 异常出现,所以我们放到这⾥⼀起讲解。
在JDK 1.4 中新加⼊了NIO(New Input/Output)类,引⼊了⼀种基于通道(Channel)与缓冲区(Buffer)的I/O ⽅式,它可以使⽤Native 函数库直接分配堆外内存,然后通过⼀个存储在Java 堆⾥⾯的DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼性能,因为避免了在Java 堆和Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到Java 堆⼤⼩的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页⽂件)的⼤⼩及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,⼀般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和⼤于物理内存限制(包括物理上的和操作系统级的限制),从⽽导致动态扩展时出现OutOfMemoryError异常
逻辑内存模型我们已经看到了,那当我们建⽴⼀个对象的时候是怎么进⾏访问的呢?在Java 语⾔中,
对象访问是如何进⾏的?对象访问在Java 语⾔中⽆处不在,是最普通的程序⾏为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、⽅法区这三个最重要内存区域之间的关联关系,如下⾯的这句代码:
Object obj = new Object();
悬疑片假设这句代码出现在⽅法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为⼀个reference 类型数据出现。⽽“new Object()”这部分的语义将会反映到Java 堆中,形成⼀块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查到此对象类型数据(如对象类型、⽗类、实现的接⼝、⽅法等)的地址信息,这些类型数据则存储在⽅法区中。
由于reference 类型在Java 虚拟机规范⾥⾯只规定了⼀个指向对象的引⽤,并没有定义这个引⽤应该通过哪种⽅式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问⽅式会有所不同,主流的访问⽅式有两种:使⽤句柄和直接指针。
句柄池
如果使⽤句柄访问⽅式,Java 堆中将会划分出⼀块内存来作为句柄池,reference中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据和类型数据各⾃的具体地址信息,如下图所⽰。
如果使⽤直接指针访问⽅式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址,如下图所⽰重阳节对长辈的祝福语
这两种对象的访问⽅式各有优势,使⽤句柄访问⽅式的最⼤好处就是reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是⾮常普遍的⾏为)时只会改变句柄中的实例数据指针,⽽reference 本⾝不需要被修改。
使⽤直接指针访问⽅式的最⼤好处就是速度更快,它节省了⼀次指针定位的时间开销,由于对象的访问在Java 中⾮常频繁,因此这类开销积少成多后也是⼀项⾮常可观的执⾏成本。就本书讨论的主要虚拟机Sun HotSpot ⽽⾔,它是使⽤第⼆种⽅式进⾏对象访问的,但从整个软件开发的范围来看,各种语⾔和框架使⽤句柄来访问的情况也⼗分常见。
下⾯我们来看⼏个⽰例
1、Java 堆溢出
下⾯的程中我们限制Java 堆的⼤⼩为20MB,不可扩展(将堆的最⼩值-Xms 参数与最⼤值-Xmx 参数设置为⼀样即可避免堆⾃动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump 出当前的内存堆转储快照以便事后进⾏分析。
参数设置如下
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论