一、JVM运行内存模型
JVM内存模型主要包含五部分的内容:堆、栈、本地方法栈、方法区(元空间)、程序计数器。
堆分为了新生代(年轻代)和老年代,新生代中又分为了Eden区和Survior区,Survior区分为S0和S1。
堆:JVM管理的最大一块内存空间,它是所有线程所共享的一块区域。在虚拟机启动的时候创建,该区域的唯一目的就是为了存放对象实例,几乎所有通过new创建的实例对象都会被分配在该区域。
栈(虚拟机栈):也可以称为虚拟机线程栈,它是JVM中每个线程所私有的一块空间,每个线程都会有这么一块空间。它的生命周期是与线程的生命周期是绑定的。虚拟机栈描述了Java中方法执行时的内存模型,即每个方法被执行的时候,线程都会在自己的线程栈中同步创建一个栈帧(Stack Frame),用于存放局部变量表、操作数栈、动态连接和方法出口等信息,每个方法从调用到完成的过程,就对应着一个栈帧在线程栈中从入栈到出栈的过程。
本地方法栈:本地方法栈与虚拟机栈的作用是相似,本地方法栈为JVM调用的本地方法服务。一般都是由C语言实现,用native修饰。
程序计数器:只需要占用一小块的内存空间,每个线程都会有自己独立的程序计数器,主要功能就是记录当前线程执行到哪一行指令了,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、线程恢复等都依赖于计数器。
方法区(元空间):在JDK 8之前,方法区也称之为永久代,这部分区域与堆一样,是所有线程所共享的,它主要用于存放被虚拟机加载的类型信息、常量、静态变量以及即时编译器编译后的代码缓存等数据。对于一个Class文件,除了版本、字段、方法、接口等描述信息外,还有常量池表,主要用于编译器生成的各种字面量和符号引用,而这部分内容在Class文件加载后是存放在方法区的运行时常量池中。这个运行时常量池自然还包括了字符串常量池,但需要注意的是,在JDK 7以后的版本中,字符串常量池和静态变量等被移至到了Java堆区,而到了JDK 8,抛弃了之前永久代的概念,通过在本地内存中实现了元空间(Meta-space)来代替永久代,并把JDK 7中永久代剩余内容(主要是类型信息)全部移至到了元空间。
小结:堆和方法区是全局共享的;栈是线程私有的,每个线程在运行的过程中new出来的对象都放到堆上面,在栈帧中通过地址引用的方式找到堆中的对象。
栈执行完了,栈帧上的局部变量会释放,但是堆的对象还未释放,这时候些对象在这时候就是无用的对象了,是一种垃圾对象,需要释放掉,如果不释放久而久之会导致堆内存溢出。因此jvm的垃圾回收机制主要就是对堆中的垃圾进行回收,我们在工作中常说的jvm调优,也就是如何配置堆内存的大小,减少GC频率和Full GC次数。
二、GC过程分析
新new对象的时候,最开始是存放在Eden区,当Eden区快满的时候会触发GC,这时的GC只会对新生代进行GC,称为youngGC(也称MinorGC)。youngGC采用的是复制算法,把Eden区根据GCRoot可达性分析算法找到存活的对象,标记的是存活对象,然后清除阶段清除的是未标记的对象,把未清除的数据移动到S0区;到下一次Eden区快满的时候,会把Eden区和S0区未清除的数据移动到S1区,到下一次Eden区快满的时候,会把Eden区和S1区未清除的数据移动到S0区。如此往复,直到某些数据在被标记达到了一定次数的时候,就会把数据从S0和S1直接放到老年代。在youngGC的过程中如果把S0和Eden区的对象复制到S1区存放不下的时候会直接放到老年区,这里面有个阈值,超过阈值后会直接放到老年区。 老年代还会存放一些大对象,比如大数组等,避免在新生代复制到老年代效率低的问题。老年代内存快满的时候会触发Old GC,在Old GC的过程中往往还会触发young GC, 这时候把这个过程称为Full GC。Full GC主要用标记清理或标记整理的算法对垃圾进行回收。
三、堆内存设置不当可能出现的问题:
1)新生代设置过小
一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入老年代,占据了老年代剩余空间,诱发Full GC
2)新生代设置过大
一是新生代设置过大会导致老年代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加
一般说来新生代占整个堆1/3比较合适
3)Survivor设置过小
导致对象从eden直接到达老年代,降低了在新生代的存活时间
4)Survivor设置过大
导致eden过小,增加了GC频率
另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收。
四、JVM性能调优思路
对JVM内存的调优主要通过设置堆内存的大小和垃圾回收器来减少GC的频率和Full GC的次数。
1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;
2.新生代和老年代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小。
比如新生代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止新生代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。
3.新生代和老年代设置多大才算合理
1)更大的新生代必然导致更小的老年代,大的新生代会延长普通GC的周期,但会增加每次GC的时间;小的老年代会导致更频繁的Full GC
2)更小的新生代必然导致更大老年代,小的新生代会导致普通GC很频繁,但每次的GC时间会更短;大的老年代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的新生代;如果存在相对较多的持久对象,老年代应该适当增大。但很多应用都没有这样明显的特性。一般说来新生代占整个堆1/3(也就是新生代与老年代的默认比例1:2)比较合适,具体情况要根据项目来调整,尽量不要出现新生代比老年代的内存大的情况。
小结:
(1)本着Full GC尽量少的原则,让老年代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。
(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给老年代至少预留1/3的增长空间。
4.在配置较好的机器上(比如多核、大内存),可以为老年代选择并行收集算法: -XX:+UseParallelOldGC 。
5.线程栈的设置:每个线程默认会开启1M的栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。-Xss256k
五、常见的JVM配置
1、堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置新生代最小大小
-XX:MaxNewSize=n:设置新生代最大大小
-Xmn256m 这个表示将-XX:NewSize和-XX:MaxNewSize都设置为256m
-XX:NewRatio=n:设置新生代和老年代的比值。如:为3,表示新生代与老年代比值为1:3,新生代占整个堆的1/4
-XX:SurvivorRatio=n:新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。例如:-XX:SurvivorRatio=8,表示Eden与Survivor的比是8:2,一个Survivor区占整个新生代的1/10。
-XX:PermSize=n: 永久代最小大小(jdk8之后没有永久代,而是元空间,这个参数不可用)
-XX:MaxPermSize=n:设置持久代最大大小(jdk8之后没有永久代,而是元空间,这个参数不可用)
-XX:MetaspaceSize,初始元空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不> 超过MaxMetaspaceSize时,适当提高该值。(jdk8后可用)
-XX:MaxMetaspaceSize,最大元空间,默认是没有限制的。(jdk8后可用)
2、收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
这些收集器在jdk8之后可以使用G1垃圾回收器替换,G1垃圾回收器效率更好。
3、垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
4、并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
5、并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。