java 虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统
JVM Heap 分两大块: 一块是 NEW Generation,另一块是 Old Generation.
在 NewGeneration 中,有一个叫 Eden 的空间,主要是用来存放新生的对象,还有两个 Survivor Spaces(from,to),它们的大小总是一样,它们用来存放每次垃圾回收后存活下来的对象。
在 OldGeneration 中,主要存放应用程序中生命周期长的内存对象。
在NewGeneration 块中,垃圾回收一般用 Copying 的算法,速度快。每次 GC 的时候,存活下来的对象首先由 Eden 拷贝到某个 SurvivorSpace, 当 Survivor Space 空间满了后, 剩下的 live 对象就被直接拷贝到OldGeneration 中去。因此,每次 GC 后,Eden 内存块会被清空。
在 OldGeneration 块中,垃圾回收一般用 mark-compact 的算法,速度慢些,但减少内存要求.
垃圾回收分多级:
Full GC:会回收 OLD 和 Perm中的垃圾;
Major GC:会回收OLD 中的垃圾;
Minor GC:只回收 NEW 中的垃圾;
内存溢出通常发生于 OLD 段或 Perm 段垃圾回收后,仍然无内存空间容纳新的 Java对象的情况。
内存申请过程如下:
1. JVM 会试图为相关 Java 对象在 Eden 中初始化一块内存区域
2. 当 Eden 空间足够时,内存申请结束。否则到下一步
3. JVM 试图释放在 Eden 中所有不活跃的对象(这属于minor级别垃圾回收),释放后若 Eden 空间仍然不足以放入新对象,则试图将部分 Eden 中活跃对象放入 Survivor 区
4. Survivor 区被用来作为 Eden 及 OLD 的中间交换区域,当to空间放不下eden和from的对象,且OLD 区空间担保成功时,from区的对象会被移到 Old 区,否则会被移到to 区
5. 当 OLD 区空间不够时,JVM 会在 OLD 区进行full GC级
6. 完全垃圾收集后,若 to 及 OLD 区仍然无法存放从 Eden 复制过来的部分对象,导致 JVM无法在 Eden 区为新对象创建内存区域,则出现”out of memory 错误”
hotspot jvm结构如下(虚拟机栈和本地方法栈合一起了):
JDK8 永久代变化如下图:
1.新生代:Eden+From Survivor+To Survivor
2.老年代:OldGen
3.永久代(方法区的实现) : PermGen—–>替换为Metaspace(本地内存中)
为什么废弃永久代:由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen
深入理解元空间(Metaspace):元空间的内存大小:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
在jdk8中:
1.字符串常量由永久代转移到堆中。
2.持久代已不存在,PermSize MaxPermSize参数已移除。
3.类加载(方法区的功能)已经不在永久代PerGem中了,而是Metaspace中
VM 调优建议:
在用户生产环境上一般将以下三组值设为相同,以减少运行期间系统在内存申请上所花的开销。
1.ms/mx:定义 YOUNG+OLD 段的总尺寸,ms 为 JVM 启动时 YOUNG+OLD 的内存大小;mx 为最大可占用的 YOUNG+OLD 内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。(进程启动时申请ms内存,当运行过程中需要的内容超过ms时,进程需要挂起来等待申请到更新内存时才能执行。为了避免这种情况发生,最好定义ms和mx相同)
2.NewSize/MaxNewSize:定义 YOUNG 段的尺寸,NewSize 为 JVM 启动时 YOUNG 的内存大小;MaxNewSize 为最大可占用的 YOUNG 内存大小。
3.由于heap大小值相同,Yound大小值相同、所以剩下的Old大小值应该也相同
1. OLD 段溢出
这种内存溢出是最常见的情况之一,产生的原因可能是:
1) 设置的内存参数过小(ms/mx, NewSize/MaxNewSize)
2) 程序问题
单个程序持续进行消耗内存的处理,如循环几千次的字符串处理,对字符串处理应建议使用 StringBuilder。此时不会报内存溢出错,却会使系统持续垃圾收集,无法处理其它请求,相关问题程序可通过 Thread Dump获取单个程序所申请内存过大,有的程序会申请几十乃至几百兆内存,此时 JVM也会因无法申请到资源而出现内存溢出,对此首先要找到相关功能,然后交予程序员修改,要找到相关程序,必须在 Apache 日志中寻找。当 Java 对象使用完毕后,其所引用的对象却没有销毁,使得 JVM 认为他还是活跃的对象而不进行回收,这样累计占用了大量内存而无法释放。
2. Perm 段溢出
通常由于 Perm 段装载了大量的 Servlet 类而导致溢出,目前的解决办法:
1) 将 PermSize 扩大,一般 256M 能够满足要求
2) 若别无选择,则只能将 servlet 的路径加到 CLASSPATH 中,但一般不建议这么处理
3. Heap 溢出
系统对 Heap 没有限制,故 Heap 发生问题时,Java 进程所占内存会持续增长,直到占用所有可用系统内存
4.其他:
JVM 有 2 个 GC 线程。第一个线程负责回收 Heap 的 Young 区。第二个线程在 Heap 不足时,遍历 Heap,将 Young 区升级为 Older 区。Older 区的大小等于-Xmx 减去-Xmn,不能将-Xms 的值设的过大,因为第二个线程被迫运行会降低 JVM 的性能。
为什么一些程序频繁发生 GC?有如下原因:
程序内调用了 System.gc()或 Runtime.gc()。 一些中间件软件调用自己的 GC 方法,此时需要设置参数禁止这些 GC。
Java 的 Heap 太小,一般默认的 Heap 值都很小。
频繁实例化对象,Release 对象。此时尽量保存并重用对象,例如使用 StringBuffer()和 String()。
如果你发现每次 GC 后,Heap 的剩余空间会是总空间的 50%,这表示你的 Heap 处于健康状态。许多 Server端的 Java 程序每次 GC 后最好能有 65%的剩余空间。
典型设置:
- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小(jdk8以后废弃)。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 - java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
2.增加 Heap 的大小虽然会降低 GC 的频率,但也增加了每次 GC 的时间。并且 GC 运行时,所有的用户线程将暂停,也就是 GC 期间,Java 应用程序不做任何工作。
4.Heap 大小并不决定进程的内存使用量。进程的内存使用量要大于-Xmx 定义的值,因为 Java 为其他任务分配内存,例如每个线程的 Stack 等。
5 . ystem.gc()显示调用会触发 Full GC。对整个堆进行整理,包括 Young、Tenured 和 Perm。要尽量避免。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/16916.html