JVM内存划分、JVM内存分配机制、JVM垃圾回收机制

 -------------------------------------------------------------------------------------

JVM内存管理分为两部分:

  1. 内存分配
  2. 内存回收

--------------------------------------------------------------------------------------

内存回收经常也被叫做垃圾回收。

 

*很多人迷惑一个问题,既然Java采用自动内存管理,程序员不用关心内存管理的细节,那么为什么我们仍然需要了解Java内存管理的内幕?

很简单:

1.了解Java内存管理的细节,有助于程序员编写出性能更好的程序。

   比如,在新的线程创建时,JVM会为每个线程创建一个专属的栈(stack),其栈   是先进后出的数据结构,这种方式的特点,让程序员编程时,必    须特别注意递归方法要尽量少使用,另外栈的大小也有一定的限制,如果过多的递归,容易导致stack overflow。

2.了解Java内存管理的细节,一旦内存管理出现问题,有助于找到问题的根本原因所在

3.了解Java内存管理的内幕,有助于优化JVM,使自己的应用获得最好性能体验。

 

本节要说的几个主要内容:     内存--- Java中哪些组件用到内存---内存分配机制---内存回收机制

 

 

1 内存


1.1 物理内存和虚拟内存

 

物理内存就是常说的RAM(随机存储器),操作系统作为我们管理计算机物理内存的接口,我们通常都是通过调用计算机操作系统来访问内存的,在Java中,甚至不需要写和内存相关的代码。

通常操作系统管理内存申请空间是按照进程来管理的,每个进程都有一段独立的空间,互不重合,互不访问。这里所说的内存空间的独立是指逻辑上的独立,由操作系统来保证的。

虚拟内存的出现使得多个进程可以同时运行时共享物理内存,空间上共享,逻辑仍然互不访问。虚拟内存提高了内存利用率,扩展了内存地址空间。


1.2 内核空间和用户空间

通常一个4GB的物理内存地址空间并不能完全被使用,因为它被划分为两部分:内核空间用户空间

  【内核空间】主要是指操作系统运行时所使用的用于程序调度、虚拟内存的使用或者连接硬件资源的程序逻辑。

  【用户空间】是用户运行程序能够申请使用的空间。

为什么这么划分呢?

    1)有效抵御恶意用户的窥探,也能防止质量低劣的用户程序的侵害,从而使系统运行得更稳定可靠。

    2)用户空间与内核空间的权限不同,内核空间拥有所有硬件设备的权限,用户空间只有普通硬件的权限;两者隔离可以防止用户程序直接访问硬件资源。

但是,每一次系统调用都会在两个内存空间之间切换,通过网络传输的数据首先被接收到内核空间,然后再从内核空间复制到用户空间供用户使用。这样比较费时,虽然保证了程序运行的安全性和稳定性,但同时也牺牲了一部分效率。(后来出现了一些列优化技术,如Linux提供的sendfile文件传输方式)

另外:

Windows32位操作系统【内核空间:用户空间=1:1

Linux32位操作系统【内核空间:用户空间=1:3

 

Java中的内存就是从物理内存中申请下来的内存,它怎么被划分的呢!?


 

2 Java内存结构划分


2.1 Java程序执行流程

一个Java程序的具体执行流程如下:

                       JVM内存划分、JVM内存分配机制、JVM垃圾回收机制

       首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存

 

2.2 Java内存划分

我们常说的Java内存管理就是指这块区域的内存分配和回收,那么,这块儿区域具体是怎么划分的呢?

根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:

程序计数器(ProgramCounter Register)

Java栈(VM Stack)

本地方法栈(Native MethodStack)

方法区(Method Area)

堆(Heap)

                    JVM内存划分、JVM内存分配机制、JVM垃圾回收机制

Java堆和方法区是所有线程共享(所有执行引擎可访问);

【Java堆】用于存储Java对象,每个Java对象都是这个对象类的副本,会复制包含继承自它父类的所有非静态属性。

【方法区】用于存储类结构信息,class文件加载进JVM时会被解析成JVM识别的几个部分分别存储在不同的数据结构中:常量池、域、方法数据、方法体、构造函数,包括类中的方法、实例初始化、接口初始化等。

方法区被JVM的GC回收器管理,但是比较稳定,并没有那么频繁的被GC回收。

 

java栈和PC寄存器是线程私有,每个执行引擎启动时都会创建自己的java栈和PC寄存器;

 【Java栈】和线程关联,每个线程创建的时候,JVM都会为他分配一个对应的Java栈,这个栈含有多个栈帧;栈帧则是个方法关联,每个方法的运行都会创建一个自己的栈帧,含有内存变量,操作栈、方法返回值。

 (用于存储方法参数、局部变量、方法返回值和运算中间结果)

【PC寄存器】则用于记录下一条要执行的字节码指令地址和被中断。如果方法是 native的,程序计数器寄存器的值不会被定义为空。

【本地方法栈】是为JVM运行Native方法准备的空间,类似于Java栈。

【运行时常量池】关于这个东西要明白三个概念:

  • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。
  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
  • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。 

 

3 Java中哪些组件用到内存

 

Java堆

Java堆用于存储Java对象,在JVM启动时就一次性申请到固定大小的空间,所以,一旦分配,大小不变。

  • 内存空间管理:JVM
  • 对象创建:Java应用程序
  • 对象所占空间释放:垃圾收集器 

线程

JVM运行实际程序的实体就是线程,每个线程创建的时候JVM都为它创建了私有的堆栈和程序计数器(或者叫做PC寄存器);很多应用程序是根据CPU的核数来分配创建的线程数。

 

类和类加载器

   Java中的类和类加载器同样需要存储空间,被存储在永久代(PermGen区)当中。

   JVM加载类方式:按需加载,只加载那些你在程序中明确使用到的类,通常只加载一次,如果一直重复加载,可能会导致内存泄露,所以也要注意对PernGen区失效类的卸载内存回收问题。

通常PernGen区满足内存回收的条件为:

1) 堆中没有对该类加载器的引用;(java.lang.ClassLoader对象)

2) 堆中没有对类加载器加载的类的引用;(java.lang.Class对象)

3) 该类加载器加载的类的所有实例化的对象不再存活。

 

NIO

     NIO使用java.nio.ByteBuffer.allocateDirect()方法分配内存,每次分配内存都会调用操作系统函数os::malloc(),所以,分配的内存是本机的内存而不是Java堆上的内存;

     另外利用该方法产生的数据和网络、磁盘发生交互的时候都是在内核空间发生的,不需要复制到用户空间Java内存中,这种技术避免了Java堆和本机堆之间的数据复制;但是利用该方法生成的数据会作为Java堆GC的一部分来自动清理本机缓冲区。

 

JNI

     JNI技术使本机代码可调用java代码,Java代码的运行本身也依赖于JNI代码来实现类库功能,所以JNI也增加内存占用。

 

4 JVM内存分配机制

 

4.1 通常的内存分配策略

操作系统中内存分配策略通常分为三类:

【静态内存分配】编译时就分配了固定的内存空间(编译器确定所需空间大小),不允许有可变数据和递归嵌套等情况,这样难以计算具体空间;

【栈内存分配】在程序运行时进入一个程序模块(程序入口处确定空间大小)知道一个程序模块分配所需数据区大小并为之分配内存。

【堆内存分配】在程序运行到相应代码是才会知道所需空间大小。(运行时确定空间大小)

 很明显,三种分配策略中,堆内存分配策略最自由,但是效率也是比较差的。

 

4.2 Java内存分配详解

在Java程序运行过程中,JVM定义了各种区域用于存储运行时数据。其中的有些数据区域在JVM启动时创建,并只在JVM退出时销毁;其它的数据区域与每个线程相关。这些数据区域,在线程创建时创建,在线程退出时销毁。

栈和线程

JVM是基于栈的虚拟机,为每个新创建的线程都分配一个栈,也就是说一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈出栈操作。

  某个线程正在执行的方法称为此线程的当前方法,当前方法使用的帧称为当前帧。当线程**一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程其他数据。这个帧在这里和编译原理中的活动纪录的概念是差不多的。

   从Java的这种分配机制来看,可以这样理解:栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。


堆和栈的区别

1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方 。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
2. 栈的优势是,存取速度比堆要快 ,仅次于直接位于CPU中的寄存器,缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可       以共享,详见第4点。

    堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动       态分配内存,存取速度较慢。

3.两者存储数据类型不同

  堆是一个运行时数据区,存放通过new、newayyray.anewarray和mulitanewarray等指令建立的对象,无需代码显式的释放;

  栈中存放一些基本类型的变量数据(int/short/long/byte/float/double/Boolean/char)和对象句柄(引用);  

    Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配;也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)。 

 

5 JVM内存回收机制

5.1 几个问题要搞清楚

问题一:什么叫垃圾回收机制?
      垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露。

问题二:java的垃圾回收有什么特点?
      Java语言不允许程序员直接控制内存空间的使用。内存空间的分配和回收都是由JRE负责在后台自动进行的,尤其是无用内存空间的回收操作(garbagecollection,也称垃圾回收),只能由运行环境提供的一个超级线程进行监测和控制。

问题三:垃圾回收器什么时候会运行?
      一般是在CPU空闲或空间不足时自动进行垃圾回收,而程序员无法精确控制垃圾回收的时机和顺序等。、

问题四:什么样的对象符合垃圾回收条件?
      当没有任何获得线程能访问一个对象时,该对象就符合垃圾回收条件。

问题五:垃圾回收器是怎样工作的?
      垃圾回收器如发现一个对象不能被任何活线程访问时,他将认为该对象符合删除条件,就将其加入回收队列,但不是立即销毁对象,何时销毁并释放内存是无法预知的。垃圾回收不能强制执行,然而java提供了一些方法(如:System.gc()方法),允许你请求JVM执行垃圾回收,而不是要求,虚拟机会尽其所能满足请求,但是不能保证JVM从内存中删除所有不用的对象。

问题六:一个java程序能够耗尽内存吗?
      可以。垃圾收集系统尝试在对象不被使用时把他们从内存中删除。然而,如果保持太多活的对象,系统则可能会耗尽内存。垃圾回收器不能保证有足够的内存,只能保证可用内存尽可能的得到高效的管理。

问题七:程序中的数据类型不一样存储地方也不一样,原生数据类型存储在java栈中,方法执行结束就会消失;对象类型存储在Java堆中,可以被共享,不一       定随着方法执行结束而消失。

问题八:如何检测垃圾?(垃圾检测机制)

     垃圾收集器的两个任务:正确检测出垃圾对象和释放垃圾对象占用的内存空间,而前者是关键所在。

     垃圾收集器有一个根对象集合,包含的元素:1)方法中局部变量的引用;2)Java操作栈中的对象引用;3)常量池中的对象引用;4)本地方法持有的对象引用;5)类的class对象。

     JVM在垃圾回收的时候会检查堆中的所有对象是否会被根对象直接或间接的引用,能够被根对象到达的叫做活动对象,否则叫做非活动对象可以被回收。

 

5.2 基于分代的垃圾收集算法 

      Sun的JVM Generational Collecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对象生命周期分析)

   设计思路:把对象按照寿命长短来分组,分为年轻代和年老代,新创建的对象被分在年轻代,如果对象经过几次回收后仍然存活,那么再把这个对象划分到年老代。年老代的收集频度没有那么频繁,这样就减少了每次垃圾收集时所需要的扫描的对象和数量,从而提高垃圾回收效率。

                   JVM内存划分、JVM内存分配机制、JVM垃圾回收机制

1.Young(年轻代)

     年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。

2.Tenured(年老代)

     年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象;如果Tenured区(old区)也满了,就会触发FullGC回收整个堆内存。

3.Perm(持久代)

      用于存放类的Class文件或静态文件,如Java类、方法等,垃圾回收是由FullGC触发的。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

举个例子

      当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。

      通常我们说的JVM内存回收总是在指堆内存回收确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的MethodArea,不属于Heap。

 

 

 关于JVM内存管理我们需要注意的几个地方:

      1、程序中的无用对象、中间对象置为null,可加快内存回收。

      2、对象池技术如果生成的对象是可重用的对象,只是其中的属性不同时,可以考虑采用对象池减少对象的生成。

           如果对象池中有空闲的对象就取出使用,没有则生成新的对象,提高对象复用率。

      3、JVM调优通过配置JVM的参数来提高垃圾回收的速度,如果在没有出现内存泄露且上面两种办法都不能保证JVM内存回收时,可以考虑采用JVM调优            的方式来解决,不过一定要经过实体机的长期测试,因为不同的参数可能引起不同的效果。如-Xnoclassgc参数等。