Java内存机制详解Word文件下载.docx

上传人:b****2 文档编号:4512000 上传时间:2023-05-03 格式:DOCX 页数:31 大小:39.18KB
下载 相关 举报
Java内存机制详解Word文件下载.docx_第1页
第1页 / 共31页
Java内存机制详解Word文件下载.docx_第2页
第2页 / 共31页
Java内存机制详解Word文件下载.docx_第3页
第3页 / 共31页
Java内存机制详解Word文件下载.docx_第4页
第4页 / 共31页
Java内存机制详解Word文件下载.docx_第5页
第5页 / 共31页
Java内存机制详解Word文件下载.docx_第6页
第6页 / 共31页
Java内存机制详解Word文件下载.docx_第7页
第7页 / 共31页
Java内存机制详解Word文件下载.docx_第8页
第8页 / 共31页
Java内存机制详解Word文件下载.docx_第9页
第9页 / 共31页
Java内存机制详解Word文件下载.docx_第10页
第10页 / 共31页
Java内存机制详解Word文件下载.docx_第11页
第11页 / 共31页
Java内存机制详解Word文件下载.docx_第12页
第12页 / 共31页
Java内存机制详解Word文件下载.docx_第13页
第13页 / 共31页
Java内存机制详解Word文件下载.docx_第14页
第14页 / 共31页
Java内存机制详解Word文件下载.docx_第15页
第15页 / 共31页
Java内存机制详解Word文件下载.docx_第16页
第16页 / 共31页
Java内存机制详解Word文件下载.docx_第17页
第17页 / 共31页
Java内存机制详解Word文件下载.docx_第18页
第18页 / 共31页
Java内存机制详解Word文件下载.docx_第19页
第19页 / 共31页
Java内存机制详解Word文件下载.docx_第20页
第20页 / 共31页
亲,该文档总共31页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

Java内存机制详解Word文件下载.docx

《Java内存机制详解Word文件下载.docx》由会员分享,可在线阅读,更多相关《Java内存机制详解Word文件下载.docx(31页珍藏版)》请在冰点文库上搜索。

Java内存机制详解Word文件下载.docx

物理地址(处理器用于索引物理RAM的地址)的大小限制了可以寻址的内存。

例如,一个16位物理地址可以寻址0x0000到0xFFFF的内存地址,这个地址范围包括2^16=65536个惟一的内存位置。

如果每个地址引用一个存储字节,那么一个16位物理地址将允许处理器寻址64KB内存。

处理器被描述为特定数量的数据位。

这通常指的是寄存器大小,但是也存在例外,比如32位390指的是物理地址大小。

对于桌面和服务器平台,这个数字为31、32或64;

对于嵌入式设备和微处理器,这个数字可能小至4。

物理地址大小可以与寄存器带宽一样大,也可以比它大或小。

如果在适当的操作系统上运行,大部分64位处理器可以运行32位程序。

表1列出了一些流行的Linux和Windows架构,以及它们的寄存器和物理地址大小:

表1.一些流行处理器架构的寄存器和物理地址大小

架构寄存器带宽(位)物理地址大小(位)

(现代)Intel®

x863232

36,具有物理地址扩展(PentiumPro和更高型号)

x866464目前为48位(以后将会增大)

PPC6464在POWER5上为50位

39031位3231

39064位6464

操作系统和虚拟内存

如果您编写无需操作系统,直接在处理器上运行的应用程序,您可以使用处理器可以寻址的所有内存(假设连接到了足够的物理RAM)。

但是要使用多任务和硬件抽象等特性,几乎所有人都会使用某种类型的操作系统来运行他们的程序。

在Windows和Linux等多任务操作系统中,有多个程序在使用系统资源。

需要为每个程序分配物理内存区域来在其中运行。

可以设计这样一个操作系统:

每个程序直接使用物理内存,并且可以可靠地仅使用分配给它的内存。

一些嵌入式操作系统以这种方式工作,但是这在包含多个未经过集中测试的应用程序的环境中是不切实际的,因为任何程序都可能破坏其他程序或者操作系统本身的内存。

虚拟内存允许多个进程共享物理内存,而且不会破坏彼此的数据。

在具有虚拟内存的操作系统(比如Windows、Linux和许多其他操作系统)中,每个程序都拥有自己的虚拟地址空间——一个逻辑地址区域,其大小由该系统上的地址大小规定(所以,桌面和服务器平台的虚拟地址空间为31、32或64位)。

进程的虚拟地址空间中的区域可被映射到物理内存、文件或任何其他可寻址存储。

当数据未使用时,操作系统可以在物理内存与一个交换区域(Windows上的页面文件或者Linux上的交换分区)之间移动它,以实现对物理内存的最佳利用率。

当一个程序尝试使用虚拟地址访问内存时,操作系统连同片上硬件会将该虚拟地址映射到物理位置,这个位置可以是物理RAM、一个文件或页面文件/交换分区。

如果一个内存区域被移动到交换空间,那么它将在被使用之前加载回物理内存中。

图1展示了虚拟内存如何将进程地址空间区域映射到共享资源:

图1.虚拟内存将进程地址空间映射到物理资源

程序的每个实例以进程的形式运行。

在Linux和Windows上,进程是一个由受操作系统控制的资源(比如文件和套接字信息)、一个典型的虚拟地址空间(在某些架构上不止一个)和至少一个执行线程构成的集合。

虚拟地址空间大小可能比处理器的物理地址大小更小。

32位Intelx86最初拥有的32位物理地址仅允许处理器寻址4GB存储空间。

后来,添加了一种称为物理地址扩展(PhysicalAddressExtension,PAE)的特性,将物理地址大小扩大到了36位,允许安装或寻址至多64GBRAM。

PAE允许操作系统将32位的4GB虚拟地址空间映射到一个较大的物理地址范围,但是它不允许每个进程拥有64GB虚拟地址空间。

这意味着如果您将大于4GB的内存放入32位Intel服务器中,您将无法将所有内存直接映射到一个单一进程中。

地址窗口扩展(AddressWindowingExtension)特性允许Windows进程将其32位地址空间的一部分作为滑动窗口映射到较大的内存区域中。

Linux使用类似的技术将内存区域映射到虚拟地址空间中。

这意味着尽管您无法直接引用大于4GB的内存,但您仍然可以使用较大的内存区域。

内核空间和用户空间

尽管每个进程都有其自己的地址空间,但程序通常无法使用所有这些空间。

地址空间被划分为用户空间和内核空间。

内核是主要的操作系统程序,包含用于连接计算机硬件、调度程序以及提供联网和虚拟内存等服务的逻辑。

作为计算机启动序列的一部分,操作系统内核运行并初始化硬件。

一旦内核配置了硬件及其自己的内部状态,第一个用户空间进程就会启动。

如果用户程序需要来自操作系统的服务,它可以执行一种称为系统调用的操作与内核程序交互,内核程序然后执行该请求。

系统调用通常是读取和写入文件、联网和启动新进程等操作所必需的。

当执行系统调用时,内核需要访问其自己的内存和调用进程的内存。

因为正在执行当前线程的处理器被配置为使用地址空间映射来为当前进程映射虚拟地址,所以大部分操作系统将每个进程地址空间的一部分映射到一个通用的内核内存区域。

被映射来供内核使用的地址空间部分称为内核空间,其余部分称为用户空间,可供用户应用程序使用。

内核空间和用户空间之间的平衡关系因操作系统的不同而不同,甚至在运行于不同硬件架构之上的同一操作系统的各个实例间也有所不同。

这种平衡通常是可配置的,可进行调整来为用户应用程序或内核提供更多空间。

缩减内核区域可能导致一些问题,比如能够同时登录的用户数量限制或能够运行的进程数量限制。

更小的用户空间意味着应用程序编程人员只能使用更少的内存空间。

默认情况下,32位Windows拥有2GB用户空间和2GB内核空间。

在一些Windows版本上,通过向启动配置添加/3GB开关并使用/LARGEADDRESSAWARE开关重新链接应用程序,可以将这种平衡调整为3GB用户空间和1GB内核空间。

在32位Linux上,默认设置为3GB用户空间和1GB内核空间。

一些Linux分发版提供了一个hugemem内核,支持4GB用户空间。

为了实现这种配置,将进行系统调用时使用的地址空间分配给内核。

通过这种方式增加用户空间会减慢系统调用,因为每次进行系统调用时,操作系统必须在地址空间之间复制数据并重置进程地址-空间映射。

图2展示了32位Windows的地址-空间布局:

图2.32位Windows的地址-空间布局

图3显示了32位Linux的地址-空间配置:

图3.32位Linux的地址-空间布局

31位Linux390上还使用了一个独立的内核地址空间,其中较小的2GB地址空间使对单个地址空间进行划分不太合理,但是,390架构可以同时使用多个地址空间,而且不会降低性能。

进程空间必须包含程序需要的所有内容,包括程序本身和它使用的共享库(在Windows上为DDL,在Linux上为.so文件)。

共享库不仅会占据空间,使程序无法在其中存储数据,它们还会使地址空间碎片化,减少可作为连续内存块分配的内存。

这对于在拥有3GB用户空间的Windowsx86上运行的程序尤为明显。

DLL在构建时设置了首选的加载地址:

当加载DLL时,它被映射到处于特定位置的地址空间,除非该位置已经被占用,在这种情况下,它会加载到别处。

WindowsNT最初设计时设置了2GB可用用户空间,这对于要构建来加载接近2GB区域的系统库很有用——使大部分用户区域都可供应用程序自由使用。

当用户区域扩展到3GB时,系统共享库仍然加载接近2GB数据(约为用户空间的一半)。

尽管总体用户空间为3GB,但是不可能分配3GB大的内存块,因为共享库无法加载这么大的内存。

在Windows中使用/3GB开关,可以将内核空间减少一半,也就是最初设计的大小。

在一些情形下,可能耗尽1GB内核空间,使I/O变得缓慢,且无法正常创建新的用户会话。

尽管/3GB开关可能对一些应用程序非常有用,但任何使用它的环境在部署之前都应该进行彻底的负载测试。

本机内存泄漏或过度使用本机内存将导致不同的问题,具体取决于您是耗尽了地址空间还是用完了物理内存。

耗尽地址空间通常只会发生在32位进程上,因为最大4GB的内存很容易分配完。

64位进程具有数百或数千GB的用户空间,即使您特意消耗空间也很难耗尽这么大的空间。

如果您确实耗尽了Java进程的地址空间,那么Java运行时可能会出现一些陌生现象,本文稍后将详细讨论。

当在进程地址空间比物理内存大的系统上运行时,内存泄漏或过度使用本机内存会迫使操作系统交换后备存储器来用作本机进程的虚拟地址空间。

访问经过交换的内存地址比读取驻留(在物理内存中)的地址慢得多,因为操作系统必须从硬盘驱动器拉取数据。

可能会分配大量内存来用完所有物理内存和所有交换内存(页面空间),在Linux上,这将触发内核内存不足(OOM)结束程序,强制结束最消耗内存的进程。

在Windows上,与地址空间被占满时一样,内存分配将会失败。

同时,如果尝试使用比物理内存大的虚拟内存,显然在进程由于消耗内存太大而被结束之前就会遇到问题。

系统将变得异常缓慢,因为它会将大部分时间用于在内存与交换空间之间来回复制数据。

当发生这种情况时,计算机和独立应用程序的性能将变得非常糟糕,从而使用户意识到出现了问题。

当JVM的Java堆被交换出来时,垃圾收集器的性能会变得非常差,应用程序可能被挂起。

如果一台机器上同时使用了多个Java运行时,那么物理内存必须足够分配给所有Java堆。

Java运行时如何使用本机内存

Java运行时是一个操作系统进程,它会受到我在上一节中列出的硬件和操作系统局限性的限制。

运行时环境提供的功能受一些未知的用户代码驱动,这使得无法预测在每种情形中运行时环境将需要何种资源。

Java应用程序在托管Java环境中执行的每个操作都会潜在地影响提供该环境的运行时的需求。

本节描述Java应用程序为什么和如何使用本机内存。

Java堆和垃圾收集

Java堆是分配了对象的内存区域。

大多数JavaSE实现都拥有一个逻辑堆,但是一些专家级Java运行时拥有多个堆,比如实现Java实时规范(RealTimeSpecificationforJava,RTSJ)的运行时。

一个物理堆可被划分为多个逻辑扇区,具体取决于用于管理堆内存的垃圾收集(GC)算法。

这些扇区通常实现为连续的本机内存块,这些内存块受Java内存管理器(包含垃圾收集器)控制。

堆的大小可以在Java命令行使用-Xmx和-Xms选项来控制(mx表示堆的最大大小,ms表示初始大小)。

尽管逻辑堆(经常被使用的内存区域)可以根据堆上的对象数量和在GC上花费的时间而增大和缩小,但使用的本机内存大小保持不变,而且由-Xmx值(最大堆大小)指定。

大部分GC算法依赖于被分配为连续的内存块的堆,因此不能在堆需要扩大时分配更多本机内存。

所有堆内存必须预先保留。

保留本机内存与分配本机内存不同。

当本机内存被保留时,无法使用物理内存或其他存储器作为备用内存。

尽管保留地址空间块不会耗尽物理资源,但会阻止内存被用于其他用途。

由保留从未使用的内存导致的泄漏与泄漏分配的内存一样严重。

当使用的堆区域缩小时,一些垃圾收集器会回收堆的一部分(释放堆的后备存储空间),从而减少使用的物理内存。

对于维护Java堆的内存管理系统,需要更多本机内存来维护它的状态。

当进行垃圾收集时,必须分配数据结构来跟踪空闲存储空间和记录进度。

这些数据结构的确切大小和性质因实现的不同而不同,但许多数据结构都与堆大小成正比。

即时(JIT)编译器

JIT编译器在运行时编译Java字节码来优化本机可执行代码。

这极大地提高了Java运行时的速度,并且支持Java应用程序以与本机代码相当的速度运行。

字节码编译使用本机内存(使用方式与gcc等静态编译器使用内存来运行一样),但JIT编译器的输入(字节码)和输出(可执行代码)必须也存储在本机内存中。

包含多个经过JIT编译的方法的Java应用程序会使用比小型应用程序更多的本机内存。

类和类加载器

Java应用程序由一些类组成,这些类定义对象结构和方法逻辑。

Java应用程序也使用Java运行时类库(比如java.lang.String)中的类,也可以使用第三方库。

这些类需要存储在内存中以备使用。

存储类的方式取决于具体实现。

SunJDK使用永久生成(permanentgeneration,PermGen)堆区域。

Java5的IBM实现会为每个类加载器分配本机内存块,并将类数据存储在其中。

现代Java运行时拥有类共享等技术,这些技术可能需要将共享内存区域映射到地址空间。

要理解这些分配机制如何影响您Java运行时的本机内存占用,您需要查阅该实现的技术文档。

然而,一些普遍的事实会影响所有实现。

从最基本的层面来看,使用更多的类将需要使用更多内存。

(这可能意味着您的本机内存使用量会增加,或者您必须明确地重新设置PermGen或共享类缓存等区域的大小,以装入所有类)。

记住,不仅您的应用程序需要加载到内存中,框架、应用服务器、第三方库以及包含类的Java运行时也会按需加载并占用空间。

Java运行时可以卸载类来回收空间,但是只有在非常严酷的条件下才会这样做。

不能卸载单个类,而是卸载类加载器,随其加载的所有类都会被卸载。

只有在以下情况下才能卸载类加载器:

Java堆不包含对表示该类加载器的java.lang.ClassLoader对象的引用。

Java堆不包含对表示类加载器加载的类的任何java.lang.Class对象的引用。

在Java堆上,该类加载器加载的任何类的所有对象都不再存活(被引用)。

需要注意的是,Java运行时为所有Java应用程序创建的3个默认类加载器(bootstrap、extension和application)都不可能满足这些条件,因此,任何系统类(比如java.lang.String)或通过应用程序类加载器加载的任何应用程序类都不能在运行时释放。

即使类加载器适合进行收集,运行时也只会将收集类加载器作为GC周期的一部分。

一些实现只会在某些GC周期中卸载类加载器。

也可能在运行时生成类,而不用释放它。

许多JEE应用程序使用JavaServerPages(JSP)技术来生成Web页面。

使用JSP会为执行的每个.jsp页面生成一个类,并且这些类会在加载它们的类加载器的整个生存期中一直存在——这个生存期通常是Web应用程序的生存期。

另一种生成类的常见方法是使用Java反射。

反射的工作方式因Java实现的不同而不同,但Sun和IBM实现都使用了这种方法,我马上就会讲到。

当使用java.lang.reflectAPI时,Java运行时必须将一个反射对象(比如java.lang.reflect.Field)的方法连接到被反射到的对象或类。

这可以通过使用Java本机接口(JavaNativeInterface,JNI)访问器来完成,这种方法需要的设置很少,但是速度缓慢。

也可以在运行时为您想要反射到的每种对象类型动态构建一个类。

后一种方法在设置上更慢,但运行速度更快,非常适合于经常反射到一个特定类的应用程序。

Java运行时在最初几次反射到一个类时使用JNI方法,但当使用了若干次JNI方法之后,访问器会膨胀为字节码访问器,这涉及到构建类并通过新的类加载器进行加载。

执行多次反射可能导致创建了许多访问器类和类加载器。

保持对反射对象的引用会导致这些类一直存活,并继续占用空间。

因为创建字节码访问器非常缓慢,所以Java运行时可以缓存这些访问器以备以后使用。

一些应用程序和框架还会缓存反射对象,这进一步增加了它们的本机内存占用。

JNI

JNI支持本机代码(使用C和C++等本机编译语言编写的应用程序)调用Java方法,反之亦然。

Java运行时本身极大地依赖于JNI代码来实现类库功能,比如文件和网络I/O。

JNI应用程序可能通过3种方式增加Java运行时的本机内存占用:

JNI应用程序的本机代码被编译到共享库中,或编译为加载到进程地址空间中的可执行文件。

大型本机应用程序可能仅仅加载就会占用大量进程地址空间。

本机代码必须与Java运行时共享地址空间。

任何本机代码分配或本机代码执行的内存映射都会耗用Java运行时的内存。

某些JNI函数可能在它们的常规操作中使用本机内存。

GetTypeArrayElements和GetTypeArrayRegion函数可以将Java堆数据复制到本机内存缓冲区中,以供本机代码使用。

是否复制数据依赖于运行时实现。

(IBMDeveloperKitforJava5.0和更高版本会进行本机复制)。

通过这种方式访问大量Java堆数据可能会使用大量本机堆。

NIO

Java1.4中添加的新I/O(NIO)类引入了一种基于通道和缓冲区来执行I/O的新方式。

就像Java堆上的内存支持I/O缓冲区一样,NIO添加了对直接ByteBuffer的支持(使用java.nio.ByteBuffer.allocateDirect()方法进行分配),ByteBuffer受本机内存而不是Java堆支持。

直接ByteBuffer可以直接传递到本机操作系统库函数,以执行I/O—这使这些函数在一些场景中要快得多,因为它们可以避免在Java堆与本机堆之间复制数据。

对于在何处存储直接ByteBuffer数据,很容易产生混淆。

应用程序仍然在Java堆上使用一个对象来编排I/O操作,但持有该数据的缓冲区将保存在本机内存中,Java堆对象仅包含对本机堆缓冲区的引用。

非直接ByteBuffer将其数据保存在Java堆上的byte[]数组中。

图4展示了直接与非直接ByteBuffer对象之间的区别:

图4.直接与非直接java.nio.ByteBuffer的内存拓扑结构

直接ByteBuffer对象会自动清理本机缓冲区,但这个过程只能作为Java堆GC的一部分来执行,因此它们不会自动响应施加在本机堆上的压力。

GC仅在Java堆被填满,以至于无法为堆分配请求提供服务时发生,或者在Java应用程序中显式请求它发生(不建议采用这种方式,因为这可能导致性能问题)。

发生垃圾收集的情形可能是,本机堆被填满,并且一个或多个直接ByteBuffers适合于垃圾收集(并且可以被释放来腾出本机堆的空间),但Java堆几乎总是空的,所以不会发生垃圾收集。

线程

应用程序中的每个线程都需要内存来存储器堆栈(用于在调用函数时持有局部变量并维护状态的内存区域)。

每个Java线程都需要堆栈空间来运行。

根据实现的不同,Java线程可以分为本机线程和Java堆栈。

除了堆栈空间,每个线程还需要为线程本地存储(thread-localstorage)和内部数据结构提供一些本机内存。

堆栈大小因Java实现和架构的不同而不同。

一些实现支持为Java线程指定堆栈大小,其范围通常在256KB到756KB之间。

尽管每个线程使用的内存量非常小,但对于拥有数百个线程的应用程序来说,线程堆栈的总内存使用量可能非常大。

如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致糟糕的性能和更高的内存占用。

本机内存耗尽会发生什么?

Java运行时善于以不同的方式来处理Java堆的耗尽与本机堆的耗尽,但这两种情形具有类似的症状。

当Java堆耗尽时,Java应用程序很难正常运行,因为Java应用程序必须通过分配对象来完成工作。

只要Java堆被填满,就会出现糟糕的GC性能并抛出表示Java堆被填满的OutOfMemoryError。

相反,一旦Java运行时开始运行并且应用程序处于稳定状态,它可以在本机堆完全耗尽之后继续正常运行。

不一定会发生奇怪的行为,因为需要分配本机内存的操作比需要分配Java堆的操作少得多。

尽管需要本机内存的操作因JVM实现不同而异,但也有一些操作很常见:

启动线程、加载类以及执行某种类型的网络和文件I/O。

本机内存不足行为与Java堆内存不足行为也不太一样,因为无法对本机堆分配进行单点控制。

尽管所有Java堆分配都在Java内存管理系统控制之下,但任何本机代码(无论其位于JVM、Java类库还是应用程序代码中)都可能执行本机内存分配,而且会失败。

尝试进行分配的代码然后会处理这种情况,无论设计人员的意图是什么:

它可能通过JNI接口抛出一个OutOfMemoryError,在屏幕上输出一条消息,发生无提示失败并在稍后再试一次,或者执行其他操作。

缺乏可预测行为意味着无法确

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 人文社科 > 军事政治

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2