1、程序计数器(Program Counter Register)
程序计数器,是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里面,字节码解析器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来进行完成。
由于JAVA虚拟机的多线程是通过线程轮换分配CPU执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对多核处理器就是一个内核)都只会执行一条线程指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间互不影响,独立存储。这类内存就是“线程私有”的内存。
我们在写代码的过程中,开发工具一般都会给我们标注行号方便查看和阅读代码。那么在程序在运行过程中也有一个类似的行号方便虚拟机的执行,就是程序计数器,在c语言中,我们知道会有一个goto语句,其实就是跳转到了指定的行,这个行号就是程序计数器。存储的就是程序下一条所执行的指令。这部分区域是线程所独享的区域,我们知道线程是一个顺序执行流,每个线程都有自己的执行顺序,如果所有线程共用一个程序计数器,那么程序执行肯定就会出乱子。为了保证每个线程的执行顺序,所以程序计数器是被单个线程所独显的。程序计数器这块内存区域是唯一一个在jvm规范中没有规定内存溢出的。
2、堆
堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。
但是随着JIT编译器的发展与逃逸分区技术逐渐成熟,栈上分配、标量替换优化技术使得“所有的对象都分配在堆上面”没有那么绝对。
下面例子(默认都是基于JDK1.8)探索堆空间不够的时候异常错误(代码001)
import java.util.ArrayList;
import java.util.List;
/**
* VM Args:
* -verbose:gc
* -XX:+PrintCommandLineFlags
* -Xms2m
* -Xmx2m
* -XX:+HeapDumpOnOutOfMemoryError
*
* @author yuninglong
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
test1();
// test2();
}
private static void test1() {
try {
byte[] bytes = new byte[2 * 1024 * 1024];
} catch (Throwable e) {
e.printStackTrace();
}
}
private static void test2() {
try {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
执行上面代码,提示如下错误:
java.lang.OutOfMemoryError: Java heap space
GC的执行基本都是在堆上面进行
3、虚拟机栈
虚拟机栈线程私有,生命周期与线程相同。虚拟机栈描述的是方法执行的内存模型:每个方法在执行的同时,会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表用于存放8种基本数据类型(boolean,byte,char,short,int,float,long,double)和reference类型。
2.1)、单线程 StackOverflowError(代码003)
/**
* VM Args: -Xss1m
*
*
* @author yuninglong
*/
public class JavaVmStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVmStackSOF javaVmStackSOF = new JavaVmStackSOF();
try {
javaVmStackSOF.stackLeak();
} catch (Throwable e) {
System. out.println("Stack length:" + javaVmStackSOF.stackLength);
throw e;
}
}
}
执行上面代码,提示如下错误:
java.lang.StackOverflowError
不同版本对比:
jdk1.6的结果总是:Stack length:11434 (每次结果都一样)
jdk1.8的结果总是:Stack length:18673 (每次运行不一样)
后续研究这个奇异的结果
3.1)、多线程
如果是单线程的情况下,当内存无法分配的时候,都是:StackOverflowError
如果是多线程的情况,为每个线程的栈分配的内存越大,越容易产生内存溢出:OutOfMemoryError
因为
计算机内存=Xmx最大堆内存 + MaxPermSize方法区内存 + 程序计数器内存(可以忽略) + 虚拟机本身内存 + 虚拟机栈和本地方法栈
每个线程的栈分配的内存越大,可以建立的线程自然越少,建立线程时候越容易把剩下的内存耗尽。
4、方法区
线程共享区域,用于存储已被虚拟机加载的类信息(Class)、常量(final修饰)、静态变量(static)和即时编译器编译后的代码(code)。有一个别名,叫做Non-Heap(非堆),目的是与堆进行区分。方法区和永久区其实不是等价的,只是因为HotSpot虚拟机设计把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。其他虚拟机(BEA Jrockit,IBM J9)不存在永久代的概念。
下面借助CGLib直接操作字节码运行时生成大量动态类(代码004)
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* VM Args:
*jdk1.6:-XX:MaxPermSize=2m -XX:PermSize=2m
*jdk1.8:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* @author yuninglong
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invoke(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
jdk1.6异常:java.lang.OutOfMemoryError: PermGen space
jdk1.8异常:java.lang.OutOfMemoryError: Metaspace
5、运行时常量池
属于方法区的一部分,用于存放编译期生成的各种字面量和符号引用,在类加载后存放到方法区的运行时常量池中。
在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都会放在常量池里面。常量池就类似一个JAVA系统级别提供的缓存。
8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
直接使用双引号声明出来的String对象会直接存储在常量池中。如:String a="11";
如果不是用双引号声明的String对象,可以使用String提供的intern方法。
jdk1.6之前,常量池放在永久代。
从jdk1.7开始逐步“去永久代”,jdk1.8常量池已经放在堆空间里面。(代码004)
import java.util.ArrayList;
import java.util.List;
/**
* VM Args:
* jdk1.6: -Xms2m -Xmx2m -XX:MaxPermSize=10m -XX:PermSize=10m
* jdk1.8: -Xms2m -Xmx2m -XX:MaxPermSize=10m -XX:PermSize=10m
*
* @author yuninglong
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while (true ){
list.add(String. valueOf(i++).intern());
}
}
}
jdk1.6异常:
java.lang.OutOfMemoryError: PermGen space
jdk1.8异常:
java.lang.OutOfMemoryError: Java heap space
下面是典型JVM参数设置
-Xms3550m:JVM初始堆大小。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。
-Xmx3550m:JVM最大堆大小。默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5。Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-Xss1m:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-XX:PermSize=64m:设置持久代初始为64m。jdk1.8 后取消持久代
-XX:MaxPermSize=64m:设置持久代最大为64m。
-XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。只对串行回收器和ParNew有效,对ParallGC无效。
-XX:PretenureSizeThreshold,直接放到老年代的对象大小,也叫老年代年龄。只对串行回收器和ParNew有效,对ParallGC无效。默认该值为0,即不指定最大的晋升大小,一切由运行情况决定。例如:-XX:PretenureSizeThreshold=2621440 ,这个值必须是字节,不能用1m,1g之类的取值。
-XX:TLABWasteTargetPercent:TLAB占eden区的百分比
-XX:+CollectGen0First:FullGC时是否先YGC,默认false
-XX:+ScavengeBeforeFullGC:在执行FullGC之前执行MinorGC,VM会分2次停顿,可以缩短最大停顿时间。该项默认是开启的。
元空间限制:-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m
直接内存使用限制:-XX:MaxDirectMemorySize=128m
并行收集器相关参数
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
-XX:MaxGCPauseMillis,设置GC的最大停顿时间。仅仅在使用Parallel Scavenge收集器时生效。如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
-XX:GCTimeRatio,GC时间占比,默认值为99,也就是允许1%的GC时间,仅仅在使用Parallel Scavenge收集器时生效。
-XX:+UseAdaptiveSizePolicy,进行自适应调节(GC Ergonomics)。使用这个参数后,配合上面的 MaxGCPauseMillis或者GCTimeRatio,就不需要手工指定新生代大小(-Xmn),Eden和Survivor 的比例(-XX:SurvivorRatio),-XX:PretenureSizeThreshold等参数,虚拟机会根据当前的运行情况,动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量。
CMS相关参数
-XX:CMSFullGCsBeforeCompaction:由于CMS并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发垃圾收集。
-XX:+UseCMSInitiatingOccupancyOnly:如果没有 -XX:+UseCMSInitiatingOccupancyOnly 这个参数, 只有第一次会使用CMSInitiatingPermOccupancyFraction=65 这个值。后面的情况会自动调整。当该标志被开启时,JVM通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,而不仅仅是第一次。然而,请记住大多数情况下,JVM比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
-XX:+CMSParallelRemarkEnabled:降低标记停顿,为了减少第二次暂停的时间,开启并行remark。
-XX:+CMSScavengeBeforeRemark:开启在CMS重新标记阶段之前的清除尝试,如果第二次remark还是过长的话,除了上面的CMSParallelRemarkEnabled,还可以开启本标志。
G1常见参数
-XX:InitiatingHeapOccupancyPercent=45。开始一个标记周期的堆占用比例阈值,JDK1.8 默认45%,注意这里是整个堆,不同于CMS中的Old堆比例。JDK1.6这个值无效。
-XX:G1HeapRegionSize=n,每个分区的大小,默认值是会根据整个堆区的大小计算出来,范围是1M~32M,取值是2的幂,计算的倾向是尽量有2048个分区数。比如如果是2G的heap,那region=1M。16Gheap,region=8M。
-XX:MaxGCPauseMillis,设置允许的最大GC停顿时间(GC pause time),这只是一个期望值,实际可能会超出,可以和年轻代大小调整一起并用来实现。默认是200ms。
-XX:G1NewSizePercent,新生代最小值,默认值5%
-XX:G1MaxNewSizePercent,新生代最大值,默认值60%
-XX:ParallelGCThreads,STW期间,并行GC线程数
辅助信息
-verbose:gc
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGC:PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间.可与上面混合使用
-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用
-XX:+PrintHeapAtGC:打印GC前后的详细堆栈信息
-Xloggc:filename:把相关日志信息记录到文件以便分析,与上面几个配合使用
-XX:+PrintTLAB:查看TLAB空间的使用情况
-XX:+PrintTenuringDistribution:查看每次minor GC后新的存活周期的阈值
-XX:+UseGCLogFileRotation 启用GC日志文件的自动转储
-XX:NumberOfGClogFiles=1 GC日志文件的循环数目
-XX:GCLogFileSize=1M 控制GC日志文件的大小