自动内存管理机制
Java虚拟机(JVM)在执行Java程序过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范 第2版》规定,运行时数据区包括:
1、程序计数器
一块较小的内存空间,不在Ram上,而是直接划分在CPU上的,程序员无法直接操作它。当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令。每条线程都有一个独立的程序计数器,属于线程私有的内存。该区域是唯一一个没有规定任何OutOfMemoryError(内存溢出)情况的区域。
2、Java虚拟机栈
描述Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。生命周期与线程相同,也属于线程私有的内存。
根据规范该区域存在两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常,如果虚拟机栈进行动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError(内存溢出)异常。
3、本地方法栈
为虚拟机使用的native方法服务。Java类的祖先类Object中有众多Native方法,如hashCode()、wait()等,他们的执行很多时候是借助于操作系统,但是JVM需要对他们做一些规范,来处理他们的执行过程。有的虚拟机(如:Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。也会抛出*Error和OutOfMemoryError异常。属于线程私有的内存。
4、堆
被所有线程共享的一块内存区域,在虚拟机启动时创建。几乎所有的对象实例以及数组都要在堆上分配内存。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。 Java堆是垃圾收集器管理的主要区域,也被称为“GC堆”,在32位系统上最大为2G,64位系统上无限制。可通过-Xms和-Xmx控制。Java性能的优化,主要就是针对这部分内存的。
如果堆中没有内存完成实例分配,并且堆也无法再进行扩展时,将会抛出OutOfMemoryError异常。
由于现在垃圾收集器基本采用分代收集算法,所以Java堆细分为:年轻代(Eden区、From Survivor区、To Survivor区) 和 年老代。Java堆上还可能划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
- 年轻代(New):年轻代用来存放JVM刚分配的Java对象
- 年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代
- Eden:Eden用来存放JVM刚分配的对象
- Survivor:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时(对象仍然存活),会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到年老代。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。Eden与Survivor的比例默认是8:1,可以通过参数-XX:SurvivorRadio指定。
5、方法区(非堆、”永久代“)
线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。垃圾收集器在这个区域是比较少出现的,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。可通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
方法区的一部分,class文件的常量池用于存放编译期生成的各种字面值和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。当常量池无法满足内存分配需求时,将抛出OutOfMemoryError异常。
6、直接内存
在JDK1.4中新加入类NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,即我们所说的直接内存,这样在某些场景中会提高程序的性能。直接内存不是虚拟机运行时数据区的一部分(当然也不会受到Java堆大小的限制),也不是规范规定的,但这部分内存会被频繁使用,也可能抛出OutOfMemoryError异常。
垃圾收集(GC)
Java语言对程序员做了一个美好的承诺:程序员无需去管理内存,因为JVM有垃圾回收(GC),会去自动进行垃圾回收。其实不然:
- 垃圾回收并不会按照程序员的要求,随时进行GC。
- 垃圾回收并不会及时的清理内存,尽管有时程序需要额外的内存。
- 程序员不能对垃圾回收进行控制。
因为上面这些事实,以致我们在写程序的时候,只能根据垃圾回收的规律,合理安排内存,这就要求我们必须彻底了解JVM的内存管理机制,这样才能随心所欲,将程序控制于鼓掌之中。
JVM在进行GC时,会暂停所有其他工作线程(用户线程)。
GC需要完成的三件事:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?(垃圾收集算法)
1、哪些内存需要回收?
在上面介绍的五大区中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC。
GC应该回收这样一些对象,这些对象没有任何引用指向(即对象已死)。 Java使用根搜索算法判断对象是否存活。基本思路是:通过一系列的名为“GC Roots”的对象作为起始点,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
Java语言里,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中的常量引用的对象。
- 本地方法栈中JNI(Native方法)的引用的对象。
四种引用的GC特点
JDK1.2之后,对引用进行了扩充,引入了强、软、若、虚四种引用,被标记为这四种引用的对象,在GC时分别有不同的意义:
- 强引用(Strong Reference)---就是为刚被new出来的对象所加的引用,它的特点就是,永远不会被回收。
- 软引用(Soft Reference)---声明为软引用的类,是可被回收的对象,如果JVM内存并不紧张,这类对象可以不被回收,如果内存紧张,则会被回收。此处有一个问题,既然被引用为软引用的对象可以回收,为什么不去回收呢?其实我们知道,Java中是存在缓存机制的,就拿字面量缓存来说,有些时候,缓存的对象就是当前可有可无的,只是留在内存中如果还有需要,则不需要重新分配内存即可使用,因此,这些对象即可被引用为软引用,方便使用,提高程序性能。
- 弱引用(Weak Reference)---弱引用的对象就是一定需要进行垃圾回收的,不管内存是否紧张,当进行GC时,标记为弱引用的对象一定会被清理回收。
- 虚引用(Phantom Reference)---虚引用弱的可以忽略不计,JVM完全不会在乎虚引用,其唯一作用就是做一些跟踪记录,辅助finalize函数的使用。
2、什么时候回收?
- 当年轻代内存满时,会引发一次普通GC(Minor GC),该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
- 当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
- 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
- System.gc()被显示调用时也会引发Full GC (使用RMI协议时由于会产生大的堆对象,为防止内存泄露,会显示调用gc,如jconsole、jprofiler等)
两种GC的区别:
何时会抛出OutOfMemoryException?
并不是内存被耗空的时候才抛出,满足如下两个条件将触发OutOfMemoryException:
- JVM98%的时间都花费在内存回收
- 每次回收的内存小于2%
3、如何回收?(垃圾收集算法)
常见的GC算法:
1)标记-清除算法(Mark-Sweep)
最基础的GC算法,将需要进行回收的对象做标记,之后扫描,有标记的进行回收,这样就产生两个步骤:标记和清除。这个算法效率不高,而且在清理完成后会产生内存碎片,这样,如果有大对象需要连续的内存空间时,还需要进行碎片整理,所以,此算法需要改进。
2)复制算法(Copying)
前面我们谈过,新生代内存分为了三份,Eden区和2块Survivor区,一般Sun的JVM会将Eden区和Survivor区的比例调为8:1,保证有一块Survivor区是空闲的,这样,在垃圾回收的时候,将不需要进行回收的对象放在空闲的Survivor区,然后将Eden区和第一块Survivor区进行完全清理,这样有一个问题,就是如果第二块Survivor区的空间不够大怎么办?这个时候,就需要当Survivor区不够用的时候,暂时借持久代的内存用一下。此算法适用于新生代。
3)标记-整理(或叫压缩)算法(Mark-Compact)
和标记-清楚算法前半段一样,只是在标记了不需要进行回收的对象后,将标记过的对象移动到一起,使得内存连续,这样,只要将标记边界以外的内存清理就行了。此算法适用于持久代。
4)分代收集算法
根据各个年代的特点采用最适当的收集算法。
常见的垃圾收集器:
1)Serial GC(串行GC):是最基本、最古老的收集器,但是现在依然被广泛使用,是一种单线程垃圾回收机制,而且不仅如此,它最大的特点就是在进行垃圾回收的时候,需要将所有正在执行的线程暂停(Stop The World),对于有些应用这是难以接受的,但是我们可以这样想,只要我们能够做到将它所停顿的时间控制在N个毫秒范围内,大多数应用我们还是可以接受的,而且事实是它并没有让我们失望,几十毫米的停顿我们作为客户机(Client)是完全可以接受的,该收集器适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
2)ParNew GC(并行GC):基本和Serial GC一样,但本质区别是加入了多线程机制,提高了效率,这样它就可以被用在服务器端(Server)上,同时它可以与CMS GC配合,所以,更加有理由将它置于Server端。
3)Parallel Scavenge GC(并行回收GC):在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。以下给出几组使用组合:
4)CMS (Concurrent Mark Sweep)收集器(并发):该收集器目标就是解决Serial GC的停顿问题,以达到最短回收时间。常见的B/S架构的应用就适合用这种收集器,因为其高并发、高响应的特点。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrenr mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美。
CMS收集器主要有三个显著缺点:
- CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
- CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
- 最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
5)G1收集器:相比CMS收集器有不少改进,首先基于标记-整理算法,不会产生内存碎片问题,其次,可以比较精确的控制停顿。
6)Serial Old:Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
7)Parallel Old:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
8)RTSJ垃圾收集器:用于Java实时编程。
Java堆内存分配策略
1)对象优先在Eden区分配
2)大对象直接进入年老代
大对象是指需要大量连续空间的java对象,如长字符串、数组等。虚拟机提供了一个-XX:PretenureSizeThreshold参数(只对Serial和ParNew两款收集器有效),令大于这个设置值的对象直接在年老代中分配,目的是避免在Eden区以及两个Survivor区之间发生大量的内存拷贝。
3)长期存活的对象将进入年老代
虚拟机给每个对象定义了一个对象年龄计数器,Minor GC至首次从Eden到Survivor时年龄设为1,在Survivor中每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定程度时,就会被晋升到年老代中。年龄阀值可以通过参数-XX:MaxTenuringThreshold来设置。
4)动态对象年龄判断
虚拟机不总是要求对象年龄必须达到MaxTenuringThreshold才能晋升到年老代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入年老代,无须等到MaxTenuringThreshold中要求的年龄。
5)空间分配担保
在发生Minor GC时,虚拟机会检测之前每次晋升到年老代的平均大小是否大于年老代的剩余空间大小,如果大于,则改为直接进行一次Full GC,如果小于则查看HandlePromotionFailure设置是否允许担保失败:如果允许就只会进行Minor GC,如果不允许则要改为进行一次Full GC。大部分情况还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
class、常量等信息jvm直接加载进持久代
常见JVM启动参数:
-Xmn -年轻代的大小 -Xmx -设置JVM Heap大小最大值,这里的heap = New Generation + Old Generation,但不包括PermGen -Xms -设置JVM Heap大小初始值 -XX:NewRatio -New/Old的大小比率 -XX:NewSize -New Generation Heap的大小 -XX:MaxNewSize -可以通过NewRatio和-Xmx计算得到 -XX:SurvivorRatio -Eden/Survivor Space大小比率 -XX:PermSize -PermGen的初始值 -XX:MaxPermSize -PermGen最大值 -Xss: -设置每个线程的Stack大小 -XX:+UseParNewGC -表示多CPU下缩短Minor GC的时间 -XX:+UseParallelGC -设置后可以使用并行清除收集器【多CPU】 -XX:+ParallelGCThreads -并行收集器线程数。可用来增加并行度【多CPU】 -XX:+AggressiveOpts -是否激活最近的试验性性能调整 -XX:-Xnoclassgc -是否允许类垃圾收集,默认设置是允许类 GC -XX:+UseLargePages -是否支持大页面堆 -XX:+UseFastAccessorMethods -在指定了这个参数后,JDK会将所有的get/set方法都转为本地代码 -XX:+UseConcMarkSweepGC -缩短major收集的时间,此选项在Heap比较大而且Full GC时间较长的情况下使用更合适 另外,JVM的一些参数可以输出有效的日志文件: -verbose:gc -输出一些gc信息 -XX:+PrintGCDetails -输出gc详细信息 -XX:+PrintGCTimeStamps -包含时间戳信息 -XX:+PrintHeapAtGC -包括gc前后Heap状况 -XX:+PrintTenuringDistribution -输出对象存活时间和Tenured Generation的其他信息 -XX:+PrintHeapUsageOverTime -以时间戳输出heap利用率和容量信息 -Xloggc:filename -输出gc信息到日志文件
---Java和C++之间有一堵有内存分配和垃圾回收技术围成的墙,墙外的人想进去,墙里的人想出去!
C、C++程序员有时苦于内存泄露,内存管理是件令人头痛的事儿,但是Java程序员呢,又羡慕C++程序员,自己可以控制一切,这样就不会在内存管理方面显得束手无策。
资料: