8.1 物理内存与虚拟内存
地址总线(连接处理器和RAM或处理器和寄存器的)的宽度影响了物理地址的索引范围,决定了处理器一次可以从寄存器或内存中获取多少个bit。同时决定了处理器最大的寻址空间,32位总线宽度可以有4GB的内存空间。
每个进程有独立的地址空间,进程之间不重合,OS保证每个进程只能访问自己的地址空间。
上面的独立的空间是逻辑上的空间,但是真正的物理空间是否独立就不一定了,在多进程的系统中,物理内存不能保证多个进程使用,这种情况下,就有了虚拟内存。
虚拟内存出现使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间上的共享,逻辑上是不能互相访问的。虚拟内存可以让进程共享物理内存,提高内存利用率,而且能扩展内存的地址空间,如一个虚拟地址可能被映射到物理内存、文件或其他可寻址的存储上。不活动的进程,OS将物理内存的数据转移到一个磁盘文件中(Windows系统上的页面文件或是Linux系统上的交换分区),而真正高效的物理内存留给正在活动的程序使用。
我们重新唤醒一个很长时间没有使用的程序时,磁盘会吱吱作响,这时OS会把磁盘上的数据重新交换到物理内存中。如果频繁的交换物理内存和和磁盘数据,则效率非常低,尤其是Linux服务器上,我们要关注Linux中SWAP的分区的活跃度。如果swap分区被频繁使用,系统会非常慢,意味着物理内存严重不足。
8.2 内核空间与用户空间
比如4G的内存空间,会被划分成内核空间和用户空间。用户只能使用用户空间的内存。
内核空间主要指OS运行时所使用的用于程序调用、虚拟内存的使用或者连接硬件资源等的程序逻辑。
处于安全考虑,用户程序不能直接访问硬件资源。如用户需要访问硬件资源,如网络连接,可以调用OS提供的接口来实现,这个调用接口的过程就是系统调用。
每一次系统调用都会存在两个内存空间的切换,通常网络传输也是一次系统调用,通过网络传输的数据先从内核空间接收到远程主机的数据,然后再从内核空间复制到用户空间。现在已经出现了很多其他技术能减少从内核空间到用户空间的数据复制的方式,如Linux系统提供了sendfile文件传输方式。
内核空间和用户空间如何分配呢?内核多一些还是用户空间多一些呢?需要平衡一下。如果是一台登录服务器,因为每一个登录用户操作系统都会初始化一个用户进程,这个进程大部分都在内核空间运行,所以要分配更多的内核空间。
8.3 在JAVA中哪些组件需要使用内存
JAVA启动后,也作为一个进程运行在OS中,那么这个进程有哪些部分需要分配内存空间呢?
8.3.1 JAVA堆
JAVA堆是用于存储JAVA对象的内存区域,堆的大小在JVM启动时就一次向OS申请完成。通过-Xmx 和 -Xmx两个选项来控制大小,Xmx表示堆的最大,Xms表示初始大小。一旦分配完成,堆的大小就固定,不够用的时候不能再申请,多了也不能还给OS。
在JAVA堆中内存空间的管理由JVM控制,对象创建由JAVA应用程序控制,但是对象所占用的空间释放由管理堆内存的垃圾收集器完成。
8.3.2 线程
JVM运行实际程序的实体是线程,线程需要内存空间来存储一些必要的数据。每个线程创建时JVM都会为它创建一个栈(线程栈)。
如果线程过多,线程栈的总内存使用量可能非常大。当前很多应用程序根据CPU的核数来分配创建线程数,如果线程数比可用的处理器多,效率低。可能导致较差的性能和更高的内存占用率。
8.3.3 类和类加载器
类和类加载器都需要内存。他们也被存储在堆中,这个区域叫做永久代(PermGen区)。
JVM是按需加载类的,JVM要加载一个jar包是否把jar包中所有类都加载到内存中?显然不是。JVM只会加载那些在你应用程序中明确使用的类到内存。
理论上使用的JAVA类越多,需要占用的内存也越多,还有一种情况是可能会重复加载同一个类。通常JVM只会加载一个类到内存一次,但是如果是自己实现的类加载器会出现重复加载的情况,如果PermGen区不能对已经失效的类做卸载,可能会导致PermGen区内存泄漏。
通常,一个类能被卸载,要满足如下3个条件:
- 在JAVA堆中没有对表示该类加载器java.lang.ClassLoader对象的引用。
- JAVA堆中没有对表示类加载器加载的类的人户java.lang.Class对象的引用。
- 在JAVA堆上该类加载器加载的任何类的所有对象都不再存活(被引用)
JVM的三个默认类加载器Bootstrap ClassLoader, ExtClassLoader和AppClassLoader都不可能满足这些条件,因此系统类(JAVA.lang.String)或通过应用程序类加载器加载的任何应用程序类都不能在运行时释放。
8.3.4 NIO
8.3.5 JNI
java native interface, JNI技术使得本机代码(如C语言程序)可以调用JAVA方法,实际上JAVA运行时本身也依赖于JNI代码来实现类库功能,如文件操作、网络 I/O或其他系统调用。
8.4 JVM内存结构
JVM是按照运行时数据的存储结构来划分内存结构的,JVM在运行JAVA程序时,将它们划分成几种不同格式的数据,分别存储在不同的区域,这些数据统一称为运行时数据。运行时数据包括JAVA程序本身的数据信息和JVM运行JAVA程序需要的额外数据信息。如PC指针。
在JAVA虚拟机规范中将JAVA运行时数据分为6种,分别是:
- PC寄存器数据:
- JAVA栈:
- 堆:
- 方法区:
- 本地方法区:
- 运行时常量池:
8.4.1 PC寄存器
pc是一个数据结构,保存当前正常执行的程序的内存地址。线程中断时,必须保存当前线程正在执行的命令的地址。
8.4.2 Java 栈:
每当创建一个线程的时候,JVM就会为这个线程创建一个对象的JAVA栈,JAVA栈中又包含多个栈帧,每运行一个方法就会产生一个栈帧,栈帧含有一些内部变量。
每当一个方法执行完成时,这个栈帧就会弹出栈帧的元素作为这个方法的返回值,并消除这个栈帧。
JAVA栈与JAVA线程对应起来,这个数据不是线程共享的,所以我们不用关心它的数据一致性问题,也不存在同步锁的问题。
8.4.3 堆
存储java对象的地方,存储在堆中的JAVA对象都会是这个对象类的一个副本,它会复制父类非静态属性。
堆是JAVA线程共享的,有同步的问题。
8.4.4 方法区
JVM方法区是用于存储类结构信息的地方,class文件解析成JVM能识别的几个部分,这些不同部分在这个class被加载到JVM时,会被分到不同的数据结构中,其中常量池,域,方法数据,方法体,构造函数,包括类中的专用方法,实例初始化,接口初始化都存储在这个区域。
方法区这个存储区域也属于后面介绍的JAVA堆中的一部分,也就是我们通常所说的JAVA堆中的永久区。这个区域可以被所有线程共享,大小可以设置。
这个区域大小一般在启动后就固定了。但是如果存在动态编译,而且是一个类的多次动态编译,那么需要观察方法区大小是否能满足类存储。
方法区有点特殊,由于它不像其他JAVA堆一样会频繁地GC回收器回收,它存储的信息相对比较稳定,但是它仍然占用了Java堆的空间,所以仍然会被JVM的GC回收器来管理。在一些特殊场合下,有时通常需要缓存一些内容,这个内容也很少变动,但会不停的被GC回收期扫描,直到很长时间后进入OLD区。在这种情况下,通常能控制这个缓冲区中的数据的声明周期的,我们不希望它被GC管理,但是又希望它在内存中。
8.4.5 运行时常量池
编译器的数字常量,方法或者域的引用。
8.4.6 本地方法栈
本地方法栈是为JVM运行Native方法准备的空间,它和前面介绍的JAVA栈的作用是类似的,由于很多native方法都是用C语言实现的,所以它通常又叫C栈,除了在我们的代码中包含的常规的Native方法会使用这个存储空间,在JVM利用JTI技术时会将一些JAVA方法重新编译为Native code代码,这些编译后的本地代码通常也利用这个栈来跟踪方法的执行状态。
8.5 JVM内存分配策略
8.5.1 通常的内存分配策略
- 静态内存分配
在程序编译时就能确定每个数据在运行时候存储空间的需要,因此在编译器就可以分配固定空间。这种分配策略不允许在程序代码中有可变数据结构存在(比如可变数组),也不允许有嵌套或者递归结构出现。
- 栈内存分配
也称为动态存储分配,数据在编译时未知,
- 堆内存分配
8.5.2 JAVA中的内存分配详解
JAVA栈分配是和线程绑定在一起的,当我们创建一个线程时,JVM就会创建一个新的JAVA栈,当线程激活一个JAVA方法时,JVM会在线程的JAVA栈里新压入一个栈帧,方法执行期间,这个栈帧用来保存参数,局部变量,中间过程和其他数据。
创建对象的时候,对象是在堆中创建的,对象的引用是在栈中。
8.6 JVM内存回收策略
8.6.1 静态内存分配和回收
在JAVA中静态内存分配是在JAVA编译时就已经确定需要内存空间,程序加载时系统把内存一次性分配给它。这些内存不会在程序执行时发生变化,直到程序结束内存才被回收。在JAVA的类和方法中的局部变量包括原生数据类型(int,long, char等)和对象的引用都是静态内存分配。
静态内存空间当方法结束时就回收,这些静态内存空间是在JAVA栈上分配的,当方法运行结束时,栈帧就撤销,静态内存空间就回收了。
8.6.2 动态内存分配和回收
Long和long不同,Integer和int不同,后者是原生类型,存储在栈上;而后者是对象类型 存储在JAVA堆上。
JAVA中对象的内存空间是动态分配的,所谓的动态分配是在程序执行的时候才知道分配多少。内存回收是以对象不再引用为前提。
8.6.3 如何检测垃圾
垃圾收集器必须能完成两件事情:一件是能够正确的检测出垃圾对象,另一件事能够释放垃圾对象占用的内存空间。其中检测垃圾是关键。
只要是某个对象不再被其他活动对象引用,那么这个对象就可以被回收了。活动对象是指能被一个根对象集合到达的对象。
那么根对象集合又都是些什么呢?和JVM的具体实现也有关系,大都会包含如下一些元素。
- 方法中局部变量区的对象的引用,他们存储在栈帧的局部变量区。
- AVA操作栈中的对象引用:
- 常量池中的对象引用:
- 本地方法中持有的对象引用:
- 类的class对象:
JVM在做垃圾回收时会检查堆中的所有对象是否被这些根对象直接或间接引用,能被引用的就是活动对象,否则就可以被垃圾收集器回收。
8.6.4 基于分代的垃圾收集算法
垃圾回收算法有很多种,这里主要介绍hotspot中使用的基于分代的垃圾收集方式。
算法思路是:把对象按照寿命长短来分组,分为年轻代和年老代,新创建的是年轻代,经过几次回收后仍然存活,那么对象就变成年老代。年老代的收集频度不像年轻代那么频繁,这样就减少了每次垃圾收集时要扫描的对象的数量,从而提高垃圾回收效率。
这种设计思路是把堆划分成若干个子堆,每个子堆是一个年龄代。
JVM将整个区划分为Young区,old区和Perm区,存放的对象有如下区别:
- Young区又分为Eden区和两个Survivor区,其中所有新创建的对象都在Eden区,当Eden区满后会触发minor GC将Eden区仍然存活的对象复制到Survivorr区中,另外一个Survivor区中的存活对象也复制到整个survivor中,以始终保证有一个Survivor区是空的。
- OLD区存放的是Young区的Survivor区满后触发minorGC后仍然存活的对象。 Eden满后将对象放到survivro区中,当Survivor区仍然存不下这些对象,GC收集器会将这些对象直接放到OLD区;如果Suvivor区的对象足够老,也直接放到old区。如果OLD区也满了,将会触发Full GC,回收整个堆内存。
- Perm区存放的是类的Class对象,如果一个类被频繁的加载,也可能导致Perm区满,Perm区的垃圾回收也是由FULL GC触发。
Sun对堆中不同代的大小给出了建议,一般建议Young区是真个堆的四分之一,而young区中的Survivor区一般设置为Young区的八分之一。
GC收集器对这些区采用的垃圾算法也不一样,Hotspot提供了三类垃圾收集算法
1 Serial Collector(串行回收)
可以通过参数设置默认内存回收算法。
JVM在做GC时由于是单线程的,所以这些动作都是单线程完成的,JVM中的其他应用程序会全部停止。
2 Parallet Collector(并行回收)
3 CMS Collector
8.7 内存问题分析
8.7.1 GC日志分析
在JVM启动时加上一些参数来控制,当JVM出问题时能记下一些当时的情况。还有就是记录下来的GC日志,我们可以观察GC的频度以及每次GC都回收了哪些内存。
8.7.2 堆快照文件分析
可以用命令记录下堆的内存快照,然后利用第三方工具分析head的对象关联情况。
8.7.3 JVM Crash日志分析
JVM崩溃可以产生日志。日志信息有:退出原因分析,导致退出的thread信息,退出时的Process状态信息,退出时与OS相关信息。
8.8 实例1
8.8 实例2
8.10 实例3
8.11 总结