Java内存模型-本机内存

Java内存模型-本机内存

BangQ IT哈哈
Java堆空间是在编写Java程序中被咱们使用得最频繁的内存空间,平时开发过程,开发人员必定遇到过OutOfMemoryError,这种结果有可能来源于Java堆空间的内存泄漏,也多是由于堆的大小不够而致使的,有时候这些错误是能够依靠开发人员修复的,可是随着Java程序须要处理愈来愈多的并发程序,可能有些错误就不是那么容易处理了。有些时候即便Java堆空间没有满也可能抛出错误,这种状况下须要了解的就是JRE(Java Runtime Environment)内部到底发生了什么。Java自己的运行宿主环境并非操做系统,而是Java虚拟机,Java虚拟机自己是用C编写的本机程序,天然它会调用到本机资源,最多见的就是针对本机内存的调用。本机内存是能够用于运行时进程的,它和Java应用程序使用的Java堆内存不同,每一种虚拟化资源都必须存储在本机内存里面,包括虚拟机自己运行的数据,这样也意味着主机的硬件和操做系统在本机内存的限制将直接影响到Java应用程序的性能。java

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

  1)堆空间和垃圾回收

  Java运行时是一个操做系统进程(Windows下通常为java.exe),该环境提供的功能会受一些位置的用户代码驱动,这虽然提升了运行时在处理资源的灵活性,可是没法预测每种状况下运行时环境须要何种资源,这一点Java堆空间讲解中已经提到过了。在Java命令行可使用-Xmx和-Xms来控制堆空间初始配置,mx表示堆空间的最大大小,ms表示初始化大小,这也是上提到的启动Java的配置文件能够配置的内容。尽管逻辑内存堆能够根据堆上的对象数量和在GC上花费的时间增长或者减小,可是使用本机内存的大小是保持不变的,并且由-Xms的值指定,大部分GC算法都是依赖被分配的连续内存块的堆空间,所以不能在堆须要扩大的时候分配更多的本机内存,全部的堆内存必须保留下来,请注意这里说的不是Java堆内存空间是本机内存。
  本机内存保留和本机内存分配不同,本机内存被保留的时候,没法使用物理内存或者其余存储器做为备用内存,尽管保留地址空间块不会耗尽物理资源,可是会阻止内存用于其余用途,由保留从未使用过的内存致使的泄漏和泄漏分配的内存形成的问题其严重程度差很少,但使用的堆区域缩小时,一些垃圾回收器会回收堆空间的一部份内容,从而减小物理内存的使用。对于维护Java堆的内存管理系统,须要更多的本机内存来维护它的状态,进行垃圾收集的时候,必须分配数据结构来跟踪空闲存储空间和进度记录,这些数据结构的确切大小和性质因实现的不一样而有所差别。算法

  2)JIT

  JIT编译器在运行时编译Java字节码来优化本机可执行代码,这样极大提升了Java运行时的速度,而且支持Java应用程序与本地代码至关的速度运行。字节码编译使用本机内存,并且JIT编译器的输入(字节码)和输出(可执行代码)也必须存储在本机内存里面,包含了多个通过JIT编译的方法的Java程序会比一些小型应用程序使用更多的本机内存。编程

  3)类和类加载器

  Java 应用程序由一些类组成,这些类定义对象结构和方法逻辑。Java 应用程序也使用 Java 运行时类库(好比 java.lang.String)中的类,也可使用第三方库。这些类须要存储在内存中以备使用。存储类的方式取决于具体实现。Sun JDK 使用永久生成(permanent generation,PermGen)堆区域,从最基本的层面来看,使用更多的类将须要使用更多内存。(这可能意味着您的本机内存使用量会增长,或者您必须明确地从新设置 PermGen 或共享类缓存等区域的大小,以装入全部类)。记住,不只您的应用程序须要加载到内存中,框架、应用服务器、第三方库以及包含类的 Java 运行时也会按需加载并占用空间。Java 运行时能够卸载类来回收空间,可是只有在很是严酷的条件下才会这样作,不能卸载单个类,而是卸载类加载器,随其加载的全部类都会被卸载。只有在如下状况下才能卸载类加载器bootstrap

  • 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直接操做和非直接操做的区别以下:
    Java内存模型-本机内存
      对于在何处存储直接 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部分,这里仅仅是涉及到了一些,因此提供两段实例代码。

  iv.共享内存:

  在Java语言里面,没有共享内存的概念,可是在某些引用中,共享内存却很受用,例如Java语言的分布式系统,存着大量的Java分布式共享对象,不少时候须要查询这些对象的状态,以查看系统是否运行正常或者了解这些对象目前的一些统计数据和状态。若是使用的是网络通讯的方式,显然会增长应用的额外开销,也增长了没必要要的应用编程,若是是共享内存方式,则能够直接经过共享内存查看到所须要的对象的数据和统计数据,从而减小一些没必要要的麻烦。

  1)共享内存特色:

  • 能够被多个进程打开访问
  • 读写操做的进程在执行读写操做的时候其余进程不能进行写操做
  • 多个进程能够交替对某一个共享内存执行写操做
  • 一个进程执行了内存写操做事后,不影响其余进程对该内存的访问,同时其余进程对更新后的内存具备可见性
  • 在进程执行写操做时若是异常退出,对其余进程的写操做禁止自动解除
  • 相对共享文件,数据访问的方便性和效率  

      2)出现状况:

  • 独占的写操做,相应有独占的写操做等待队列。独占的写操做自己不会发生数据的一致性问题;
  • 共享的写操做,相应有共享的写操做等待队列。共享的写操做则要注意防止发生数据的一致性问题;
  • 独占的读操做,相应有共享的读操做等待队列;
  • 共享的读操做,相应有共享的读操做等待队列;

      3)Java中共享内存的实现:

      JDK 1.4里面的MappedByteBuffer为开发人员在Java中实现共享内存提供了良好的方法,该缓冲区其实是一个磁盘文件的内存映象,两者的变化会保持同步,即内存数据发生变化事后会当即反应到磁盘文件中,这样会有效地保证共享内存的实现,将共享文件和磁盘文件简历联系的是文件通道类:FileChannel,该类的加入是JDK为了统一外围设备的访问方法,而且增强了多线程对同一文件进行存取的安全性,这里可使用它来创建共享内存用,它创建了共享内存和磁盘文件之间的一个通道。打开一个文件可以使用RandomAccessFile类的getChannel方法,该方法直接返回一个文件通道,该文件通道因为对应的文件设为随机存取,一方面能够进行读写两种操做,另一个方面使用它不会破坏映象文件的内容。这里,若是使用FileOutputStream和FileInputStream则不能理想地实现共享内存的要求,由于这两个类同时实现自由读写很困难。
      下边代码段实现了上边说起的共享内存功能

// 得到一个只读的随机存取文件对象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 得到相应的文件通道
FileChannel fc = RAFile.getChannel();
// 取得文件的实际大小
int size = (int)fc.size();
// 得到共享内存缓冲区,该共享内存只读 
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);
// 得到一个可读写的随机存取文件对象 
RAFile = new RandomAccessFile(filename,"rw");
// 得到相应的文件通道 
fc = RAFile.getChannel();
// 取得文件的实际大小,以便映像到共享内存 
size = (int)fc.size();
// 得到共享内存缓冲区,该共享内存可读写 
mapBuf = fc.map(FileChannel.MAP_RW,0,size);
// 获取头部消息:存取权限 
mode = mapBuf.getInt();

  若是多个应用映象使用同一文件名的共享内存,则意味着这多个应用共享了同一内存数据,这些应用对于文件能够具备同等存取权限,一个应用对数据的刷新会更新到多个应用中。为了防止多个应用同时对共享内存进行写操做,能够在该共享内存的头部信息加入写操做标记,该共享文件的头部基本信息至少有:

  • 共享内存长度
  • 共享内存目前的存取模式
      共享文件的头部信息是私有信息,多个应用能够对同一个共享内存执行写操做,执行写操做和结束写操做的时候,可使用以下方法:
public boolean startWrite()
{
    if(mode == 0) // 这里mode表明共享内存的存取模式,为0表明可写
    {
        mode = 1; // 意味着别的应用不可写
        mapBuf.flip();
        mapBuf.putInt(mode);    //写入共享内存的头部信息
        return true;
    }
    else{
        return false; //代表已经有应用在写该共享内存了,本应用不可以针对共享内存再作写操做
    }
}

public boolean stopWrite()
{
    mode = 0; // 释放写权限
    mapBuf.flip();
    mapBuf.putInt(mode);    //写入共享内存头部信息
    return true;
}

  【:上边提供了对共享内存执行写操做过程的两个方法,这两个方法其实理解起来很简单,真正须要思考的是一个针对存取模式的设置,其实这种机制和最前面提到的内存的锁模式有点相似,一旦当mode(存取模式)设置称为可写的时候,startWrite才能返回true,不只仅如此,某个应用程序在向共享内存写入数据的时候还会修改其存取模式,由于若是不修改的话就会致使其余应用一样针对该内存是可写的,这样就使得共享内存的实现变得混乱,而在中止写操做stopWrite的时候,须要将mode设置称为1,也就是上边注释段提到的释放写权限。】
  关于锁的知识这里简单作个补充【
:上边代码的这种模式能够理解为一种简单的锁模式】:通常状况下,计算机编程中会常常遇到锁模式,在整个锁模式过程当中能够将锁分为两类(这里只是辅助理解,不是严格的锁分类)——共享锁和排他锁(也称为独占锁),锁的定位是定位于针对全部与计算机有关的资源好比内存、文件、存储空间等,针对这些资源均可能出现锁模式。在上边堆和栈一节讲到了Java对象锁,其实不只仅是对象,只要是计算机中会出现写入和读取共同操做的资源,都有可能出现锁模式。
  共享锁——当应用程序得到了资源的共享锁的时候,那么应用程序就能够直接访问该资源,资源的共享锁能够被多个应用程序拿到,在Java里面线程之间有时候也存在对象的共享锁,可是有一个很明显的特征,也就是内存共享锁只能读取数据,不可以写入数据,不管是什么资源,当应用程序仅仅只能拿到该资源的共享锁的时候,是不可以针对该资源进行写操做的。
  独占锁——当应用程序得到了资源的独占锁的时候,应用程序访问该资源在共享锁上边多了一个权限就是写权限,针对资源自己而言,一个资源只有一把独占锁,也就是说一个资源只能同时被一个应用或者一个执行代码程序容许写操做,Java线程中的对象写操做也是这个道理,若某个应用拿到了独占锁的时候,不只仅能够读取资源里面的数据,并且能够向该资源进行数据写操做。
  数据一致性——当资源同时被应用进行读写访问的时候,有可能会出现数据一致性问题,好比A应用拿到了资源R1的独占锁,B应用拿到了资源R1的共享锁,A在针对R1进行写操做,而两个应用的操做——A的写操做和B的读操做出现了一个时间差,s1的时候B读取了R1的资源,s2的时候A写入了数据修改了R1的资源,s3的时候B又进行了第二次读,而两次读取相隔时间比较短暂并且初衷没有考虑到A在B的读取过程修改了资源,这种状况下针对锁模式就须要考虑到数据一致性问题。独占锁的排他性在这里的意思是该锁只能被一个应用获取,获取过程只能由这个应用写入数据到资源内部,除非它释放该锁,不然其余拿不到锁的应用是没法对资源进行写入操做的。
  按照上边的思路去理解代码里面实现共享内存的过程就更加容易理解了。
  若是执行写操做的应用异常停止,那么映像文件的共享内存将再也不能执行写操做。为了在应用异常停止后,写操做禁止标志自动消除,必须让运行的应用获知退出的应用。在多线程应用中,能够用同步方法得到这样的效果,可是在多进程中,同步是不起做用的。方法能够采用的多种技巧,这里只是描述一可能的实现:采用文件锁的方式。写共享内存应用在得到对一个共享内存写权限的时候,除了判断头部信息的写权限标志外,还要判断一个临时的锁文件是否能够获得,若是能够获得,则即便头部信息的写权限标志为1(上述),也能够启动写权限,其实这已经代表写权限得到的应用已经异常退出,这段代码以下:

// 打开一个临时文件,注意统一共享内存,该文件名必须相同,能够在共享文件名后边添加“.lock”后缀
RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
// 获取文件通道
FileChannel lockFileChannel = files.getChannel();
// 获取文件的独占锁,该方法不产生任何阻塞直接返回
FileLock fileLock = lockFileChannel.tryLock();
// 若是为空表示已经有应用占有了
if( fileLock == null ){
    // ...不可写
}else{
    // ...能够执行写操做
}

  4)共享内存的应用:

  在Java中,共享内存通常有两种应用:
  [1]永久对象配置——在java服务器应用中,用户可能会在运行过程当中配置一些参数,而这些参数须要永久 有效,当服务器应用从新启动后,这些配置参数仍然能够对应用起做用。这就能够用到该文 中的共享内存。该共享内存中保存了服务器的运行参数和一些对象运行特性。能够在应用启动时读入以启用之前配置的参数。
  [2]查询共享数据——一个应用(例 sys.java)是系统的服务进程,其系统的运行状态记录在共享内存中,其中运行状态多是不断变化的。为了随时了解系统的运行状态,启动另外一个应用(例 mon.java),该应用查询该共享内存,汇报系统的运行状态。

  v.小节:

  提供本机内存以及共享内存的知识,主要是为了让读者可以更顺利地理解JVM内部内存模型的物理原理,包括JVM如何和操做系统在内存这个级别进行交互,理解了这些内容就让读者对Java内存模型的认识会更加深刻,并且不容易遗忘。其实Java的内存模型远不及咱们想象中那么简单,并且其结构极端复杂,看过《Inside JVM》的朋友应该就知道,结合JVM指令集去写点小代码测试.class文件的里层结构也不失为一种好玩的学习方法。

相关文章
相关标签/搜索