内存模型:程序计数器、虚拟机栈、本地方法栈、堆、方法区(永久代)
- 程序计数器(counter)
- 线程私有。
- 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- 虚拟机栈(VM stack)
- 线程私有,使用连续的内存空间。
- 描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
- 两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。
- 本地方法栈(native)
- 与虚拟机栈相似。
- 线程私有。
- 描述的是本地方法内存模型。
- 堆(heap)
- 所有线程共享。
- 唯一目的就是存放对象实例。
- 垃圾收集器管理的主要区域,也称GC堆。
- 通过-Xms(最小值)和-Xmx(最大值)参数设置大小。
- 分为新生代和老年代。
- 新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小。
- 老年代用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。2、大的数组对象,且数组中无引用外部对象。
- 方法区(method area)
- 所有线程共享。
- 是线程安全的。
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集。
- 通过-XX:PermSize 和 -XX:MaxPermSize 参数限制大小。
- 堆和栈的对比
- 栈:主要存放引用和基本数据类型。
- 堆:用来存放new出来的对象实例。
- 新建对象的内存分配
- JVM 会试图为该对象在Eden Space中初始化一块内存区域。
- 当Eden空间足够时,内存申请结束;否则到下一步。
- JVM 试图释放在Eden中所有不活跃的对象。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。
- Survivor区被用来作为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区。
- 当Old区空间不够时,JVM 会在Old区进行完全的垃圾收集。
- 完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现“outofmemory”错误。
- 对象的访问
- 涉及栈、堆、方法区这三个最重要内存区域之间的关联关系。
- 如这句代码:Object obj = newObject()。
- “Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。
- “new Object()”这部分的语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值(对象中各个实例字段的数据)的结构化内存。另外,堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息。
- 对象类型数据(如对象类型、父类、实现的接口、方法等)是存储在方法区中。
- 主流的访问方式有两种:使用句柄和直接指针。
- 使用句柄:堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。优点是稳定。
- 直接指针:有点是速度快,hotspot用的就是直接指针。
- 判断对象是否可以回收的算法
- 引用技术算法:引用就+1,没引用就-1,0则回收。缺点是无法解决对象之间的循环引用。
- 可达性算法(目前使用的算法):
- 用GC Root判断对象是否与之有关系链,有则可达,没有则不可达,不可达的对象可以回收。
- 可以作为GC Root的对象:虚拟机栈中可用的对象;方法区静态变量;方法区常量;本地方法栈中可用的对象。
垃圾收集器
- serial 收集器(Client VM首选)
- 单线程、stop the world、复制算法、主用于新生代。
- ParNew(Server VM首选)
- 多线程、stop the world、新生代。
- -XX:ParallelGCThreads参数控制垃圾收集的线程数量。
- Parallel Scavenge
- 拥有ParNew的特点,专注于吞吐量。
- 控制最大垃圾收集停顿时间:-XX:MaxGcPauseMillis(缩短将以牺牲吞吐量和新生代空间换取)。
- 吞吐量大小:-XX:GCTimeRatio。
- GC自适应调节策略:-XX:UseAdaptiveSizePolicy(与ParNew主要区别)。
- Serial Old(Client VM)
- Serial的老年代版本。
- Parallel Old
- Parallel Scavenge的老年代版本。
- 注重吞吐量和CPU资源敏感的考虑:Parallel Scavenge + Parallel Old。
- CMS(Concurrent Mark Sweep)
- 标记 - 清除,已获取最短GC时间为目标,使用注重响应快的服务。
- 标记分为4步:
- 初始标记:只标记GC Root直接关联的节点,stop the world,速度快,单线程。
- 并发标记:根据步骤一中的节点继续追踪标记有关联的对象,并发运行,时间长,占用CPU资源。
- 重新标记:修正步骤二期间对象引用有变动的部分,stop the world,多线程标记。
- 并发清楚:跟用户代码并发运行。
- 缺点:会产生垃圾碎片,并且不能等到老年代100%才进行回收,得预留给并发回收的时候同时新分配的内存。
- jdk1.6后,CMS启动阈值为92%,可以通过参数:-XX:CMSInitiatingOccupancyFraction 设置阈值。
- 如果发生Concurrent Mode Failure,则会启动后备收集器Serial Old,这时GC时间会很长。
- -XX:CMSFullGcsBeforeCompaction:设置执行多少次不压缩的fullGc后,跟着来一次带压缩的整理。
- G1收集器(暂不整理)
jdk命令行工具
- jps:显示指定系统内所有的HotSpot虚拟机进程。
- jstat:用于收集HotSpot虚拟机各方面的运行数据。
- 例:jstat -gcutil 3700 500 10 指收集进程号为3700的虚拟机的信息,每个500毫秒收集一次,总共收集10次。显示的参数值表示已使用的空间占比。
- jstack:用于生成虚拟机当前的线程快照信息,包含每一条线程的堆栈信息。该命令通常用于定位线程停顿原因,当出现线程停顿时,可通过stack查看每个线程的堆栈信息,进而分析停顿原因。
- 例:jstack 3700
- jinfo:用于查看和修改虚拟机的各项参数信息。
- 例:jinfo 3700
- jmap:可以产生堆dump文件,查询堆和持久代的详细信息等。导出的文件再用mat工具进行解析。
- 例:$ jmap -dump:format=b,file=dump.tmp 3700
JVM常见问题分析
- 年老代堆空间被占满:
- 异常: java.lang.OutOfMemoryError: Java heap space。
- 说明:这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。
- 解决:这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
- 持久代被占满:
- 异常:java.lang.OutOfMemoryError: PermGen space。
- 说明:Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。
- 解决:-XX:MaxPermSize=16m。
- 堆栈溢出
- 异常:java.lang.StackOverflowError。
- 说明:一般就是递归没返回,或者循环调用造成。
- 线程堆栈满
- 异常:Fatal: Stack size too small。
- 说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
- 解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
- 系统内存被占满
- 异常:java.lang.OutOfMemoryError: unable to create new native thread。
- 说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。
- 解决:1、重新设计系统减少线程数量。2、线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
- 内存溢出和内存泄漏
- 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
- 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
- OutOfMemoryError异常定位的步骤:
- 使用jps和jmap导出堆存储快照,通过内存映像分析工具对dump 出来的堆转储快照进行分析。
- 确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
- 若内存中的对象必须活着,即不存在内存泄漏,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
- 若内存中的对象不必要活着,即存在内存泄漏。可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
JVM性能调优的矛盾:吞吐量和低延迟
- 吞吐量优先:那么GC就必然会比较少进行垃圾回收,会达到一定程度才进行垃圾回收,相对的就需要花费更长的暂停时间来执行内存回收。可以使用Parallel Scavenge + Parallel Old垃圾收集器。
- 低延迟优先:会频繁地执行垃圾回收,又会导致程序吞吐量的下降。可以使用CMS垃圾收集器。
java堆外内存
- java本地存储对象的几种方式:堆内存、堆外内存和磁盘。堆外内存可以通过-XX:MaxDirectMemorySize设置,不设置的话默认跟堆内存一样。来自于java.nio。
- 相对于堆内存的优点:
- 避免了垃圾回收GC的工作。
- 减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。
- 创建(以Netty为例):Netty使用的堆外内存是Java NIO的DirectByteBuffer类。首先向Bits类申请额度,根据一个全局变量totalCapacity(记录堆外内存的总大小),判断是否有足够限额分配,批准的话调用sun.misc.Unsafe分配内存,返回内存基地址。额度不够则先进行GC后再分配,如果垃圾回收了100ms内存还不够,则抛OOM异常。
- 回收:只有在进行full GC的时候才会回收对外内存,可以主动调用System.gc来触发,但有不确定性。所以一般主动从DirectByteBuffer中取出sun.misc.Cleaner,然后调用其clean()方法即可。
- 适用场景:不适合存储很复杂的对象,一般简单的对象或者扁平化的结构比较适合。