Java内存模型5
- Java 堆不包含对表示该类加载器的 java.lang.ClassLoader 对象的引用。
- Java 堆不包含对表示类加载器加载的类的任何 java.lang.Class 对象的引用。
- 在 Java 堆上,该类加载器加载的任何类的所有对象都不再存活(被引用)。
需要注意的是,Java 运行时为所有 Java 应用程序创建的 3 个默认类加载器(?bootstrap、extension?和?application?)都不可能满足这些条件,因此,任何系统类(比如 java.lang.String)或通过应用程序类加载器加载的任何应用程序类都不能在运行时释放。即使类加载器适合进行收集,运行时也只会将收集类加载器作为 GC 周期的一部分。一些实现只会在某些 GC 周期中卸载类加载器,也可能在运行时生成类,而不去释放它。许多 Java EE 应用程序使用 JavaServer Pages (JSP) 技术来生成 Web 页面。使用 JSP 会为执行的每个 .jsp 页面生成一个类,并且这些类会在加载它们的类加载器的整个生存期中一直存在 —— 这个生存期通常是 Web 应用程序的生存期。另一种生成类的常见方法是使用 Java 反射。反射的工作方式因 Java 实现的不同而不同,当使用?java.lang.reflect?API 时,Java 运行时必须将一个反射对象(比如 java.lang.reflect.Field)的方法连接到被反射到的对象或类。这可以通过使用 Java 本机接口(Java Native Interface,JNI)访问器来完成,这种方法需要的设置很少,但是速度缓慢,也可以在运行时为您想要反射到的每种对象类型动态构建一个类。后一种方法在设置上更慢,但运行速度更快,非常适合于经常反射到一个特定类的应用程序。Java 运行时在最初几次反射到一个类时使用 JNI 方法,但当使用了若干次?JNI 方法之后,访问器会膨胀为字节码访问器,这涉及到构建类并通过新的类加载器进行加载。执行多次反射可能导致创建了许多访问器类和类加载器,保持对反射对象的引用会导致这些类一直存活,并继续占用空间,因为创建字节码访问器非常缓慢,所以 Java 运行时可以缓存这些访问器以备以后使用,一些应用程序和框架还会缓存反射对象,这进一步增加了它们的本机内存占用。
4)JNI JNI支持本机代码调用Java方法,反之亦然,Java运行时本身极大依赖于JNI代码来实现类库功能,比如文件和网络I/O,JNI应用程序可以通过三种方式增加Java运行时对本机内存的使用:- JNI应用程序的本机代码被编译到共享库中,或编译为加载到进程地址空间中的可执行文件,大型本机应用程序可能仅仅加载就会占用大量进程地址空间
- 本机代码必须与Java运行时共享地址空间,任何本机代码分配或本机代码执行的内存映射都会耗用Java运行时内存
- 某些JNI函数可能在它们的常规操作中使用本机内存,GetTypeArrayElements和GetTypeArrayRegion函数可以将Java堆复制到本机内存缓冲区中,提供给本地代码使用,是否复制数据依赖于运行时实现,通过这种方式访问大量Java堆数据就可能使用大量的本机内存堆空间 5)NIO JDK 1.4开始添加了新的I/O类,引入了一种基于通道和缓冲区执行I/O的新方式,就像Java堆上的内存支持I/O缓冲区一样,NIO添加了对直接ByteBuffer的支持,ByteBuffer受本机内存而不是Java堆的支持,直接ByteBuffer可以直接传递到本机操作系统库函数,以执行I/O,这种情况虽然提高了Java程序在I/O的执行效率,但是会对本机内存进行直接的内存开销。ByteBuffer直接操作和非直接操作的区别如下:
对于在何处存储直接?ByteBuffer?数据,很容易产生混淆。应用程序仍然在 Java 堆上使用一个对象来编排 I/O 操作,但持有该数据的缓冲区将保存在本机内存中,Java 堆对象仅包含对本机堆缓冲区的引用。非直接?ByteBuffer?将其数据保存在 Java 堆上的?byte[]?数组中。直接ByteBuffer对象会自动清理本机缓冲区,但这个过程只能作为Java堆GC的一部分执行,它不会自动影响施加在本机上的压力。GC仅在Java堆被填满,以至于无法为堆分配请求提供服务的时候,或者在Java应用程序中显示请求它发生。 6)线程: 应用程序中的每个线程都需要内存来存储器堆栈(用于在调用函数时持有局部变量并维护状态的内存区域)。每个 Java 线程都需要堆栈空间来运行。根据实现的不同,Java 线程可以分为本机线程和?Java 堆栈。除了堆栈空间,每个线程还需要为线程本地存储(thread-local storage)和内部数据结构提供一些本机内存。尽管每个线程使用的内存量非常小,但对于拥有数百个线程的应用程序来说,线程堆栈的总内存使用量可能非常大。如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致糟糕的性能和更高的内存占用。 ii.本机内存耗尽: Java运行时善于以不同的方式来处理Java堆空间的耗尽和本机堆空间的耗尽,但是这两种情形具有类似症状,当Java堆空间耗尽的时候,Java应用程序很难正常运行,因为Java应用程序必须通过分配对象来完成工作,只要Java堆被填满,就会出现糟糕的GC性能,并且抛出OutOfMemoryError。相反,一旦 Java 运行时开始运行并且应用程序处于稳定状态,它可以在本机堆完全耗尽之后继续正常运行,不一定会发生奇怪的行为,因为需要分配本机内存的操作比需要分配 Java 堆的操作少得多。尽管需要本机内存的操作因 JVM 实现不同而异,但也有一些操作很常见:启动线程、加载类以及执行某种类型的网络和文件 I/O。本机内存不足行为与 Java 堆内存不足行为也不太一样,因为无法对本机堆分配进行控制,尽管所有 Java 堆分配都在 Java 内存管理系统控制之下,但任何本机代码(无论其位于 JVM、Java 类库还是应用程序代码中)都可能执行本机内存分配,而且会失败。尝试进行分配的代码然后会处理这种情况,无论设计人员的意图是什么:它可能通过 JNI 接口抛出一个?OutOfMemoryError,在屏幕上输出一条消息,发生无提示失败并在稍后再试一次,或者执行其他操作。 iii.例子: 这篇文章一致都在讲概念,这里既然提到了ByteBuffer,先提供一个简单的例子演示该类的使用: ——[$]使用NIO读取txt文件——package?org.susan.java.io;
import?java.io.FileInputStream;import?java.io.IOException;import?java.nio.ByteBuffer;import?java.nio.channels.FileChannel;
public class?ExplicitChannelRead {?? ?public static void?main(String?args[]){?? ??? ?FileInputStream fileInputStream;?? ??? ?FileChannel fileChannel;?? ??? ?long?fileSize;?? ??? ?ByteBuffer byteBuffer;?? ??? ?try{?? ??? ??? ?fileInputStream = new FileInputStream("D:\\read.txt");?? ??? ??? ?fileChannel = fileInputStream.getChannel();?? ??? ??? ?fileSize = fileChannel.size();?? ??? ??? ?byteBuffer = ByteBuffer.allocate((int)fileSize);?? ??? ??? ?fileChannel.read(byteBuffer);?? ??? ??? ?byteBuffer.rewind();?? ??? ??? ?for(?int?i = 0; i < fileSize; i++ )?? ??? ??? ??? ?System.out.print((char)byteBuffer.get());?? ??? ??? ?fileChannel.close();?? ??? ??? ?fileInputStream.close();?? ??? ?}catch(IOException ex){?? ??? ??? ?ex.printStackTrace();?? ??? ?}?? ?}} 在读取文件的路径放上该txt文件里面写入:Hello World,上边这段代码就是使用NIO的方式读取文件系统上的文件,这段程序的输入就为:Hello World ——[$]获取ByteBuffer上的字节转换为Byte数组——package?org.susan.java.io;
import?java.nio.ByteBuffer;
public class?ByteBufferToByteArray {?? ?public static void?main(String args[])?throws?Exception{?? ??? ?// 从byte数组创建ByteBuffer?? ??? ?byte[] bytes =?new byte[10];? ??? ??ByteBuffer buffer = ByteBuffer.wrap(bytes);
? ??? ??// 在position和limit,也就是ByteBuffer缓冲区的首尾之间读取字节? ??? ??bytes =?new?byte[buffer.remaining()];? ??? ??buffer.get(bytes,?0, bytes.length);
? ??? ??// 读取所有ByteBuffer内的字节? ??? ??buffer.clear();? ??? ??bytes =?new?byte[buffer.capacity()];? ??? ??buffer.get(bytes,?0, bytes.length);?? ?}} 上边代码就是从ByteBuffer到byte数组的转换过程,有了这个过程在开发过程中可能更加方便,ByteBuffer的详细讲解我保留到IO部分,这里仅仅是涉及到了一些,所以提供两段实例代码。