首页 > 代码库 > JVM技术部分总结
JVM技术部分总结
1、JVM内存模型
1.1 JVM内存模型图解
Java虚拟机在执行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁,我们可以将这些区域统称为Java运行时数据区域。
如下图是java虚拟机运行时数据区:
该区域一共分为5个区域:堆(heap),栈(stack)、本地方法栈(native method area),方法区(method area),程序计数器(program count register)
1.2 程序计数器
程序计数器(program count register) 是一块较小的内存空间,可以看做当前线程所执行的字节码的信号指示器。
1.3 堆(Heap)
Java 堆是java虚拟机所管理内存中最大的一块。该区域被所有线程共享,在虚拟机启动时创建,用来存放对象的实例,几乎所有的对象以及数组都在这里分配内存。
Java堆是GC管理的主要区域。
按分代收集算法:分为新生代和老年代
新生代又分为:Eden区,From Survivor区,To Survivor区
Java堆可以处于物理上不连续的内存空间,只要逻辑连续即可
通过-Xmx和-Xms控制
例如:-Xms8g -Xmx8g 初始堆内存8g,最大堆内存也是8g
1.4 栈(Stack)
Java虚拟机栈是线程私有的,生命周期与线程相同,每个方法执行时都会创建一个栈帧(Stack Frame),描述的是java方法执行的内存模型,用于存储局部变量,操作数栈,方法出口等。每个方法的调用都对应的出栈和入栈
图中可以看到每一个栈帧中都有局部变量表。局部变量表存放了编译期间的各种基本数据类型,对象引用等信息。
1.5 本地方法栈(Native Stack)
本地方法栈(Native Stack)与Java虚拟机站(Java Stack)所发挥的作用非常相似,他们之间的区别在于虚拟机栈为虚拟机栈执行java方法(也就是字节码)服务,而本地方法栈则为使用到Native方法服务。
1.6 方法区(Method Area)
方法区(Method Area)与堆(Java Heap)一样,是各个线程共享的内存区域,它用于存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
也称为 Non-Heap(非堆)为与堆区分来
叫法:Permanent generation(永久代),是因为GC通过使用永久代来实现方法区的收集,希望通过管理堆一样管理这部分内存,其实两者并不等价,或者说使用永久代来实现方法区而已.
通过-XX:MaxPermSize来设置上限
例如:-XX:MaxPermSize=256m 方法区的上限是256M
1.7 直接内存(Direct memory)
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机定义内存的一部分,该内存也被频繁的使用,也可能导致OutOfMemoryError异常
直接内存不会受到受java堆限制,但会受本机总内存大小限制,还是会根据内存设置-Xmx,若忽略直接内存,使得各个内存总和大约物理内存限制,从而导致动态扩容的时候出现OutOfMemory异常
1.8 对象创建过程
当遇到new的指令时,首先去常量池中检查一下该类的符号被引用,并且检查该符号的类是否被加载,解析和初始化过,若没有就要先执行类加载的过程,
类加载检查通过之后,就已经确定了对象所需内存,虚拟机将为新生对象分配内存,
内存分配完成之后,虚拟机将内存空间初始化为零值,
虚拟机将该类的元数据,hash码,GC分代年龄等信息放入对象头,
执行<init>方法,一个真正可用的对象产生出来了。
2、GC算法
垃圾收集(garbage collection,GC)
Java虚拟机并没有引用计数法算法来管理内存,主要原因是因为很难解决对象之间的相互引用。
可达性分析算法:通过GC Roots的对象作为起点,从这些节点往下搜索,搜索走过的路径被称为引用链,当一个对象到GC Roots没有引用链,则称为该对象不可达
2.1 标记—清除算法(mark-sweep)
1、标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
2、在标记完成后统一回收所有被标记的对象
缺点:一个是效率问题,标记和清除两个过程的效率都不高;
另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中
需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.2 复制算法(Copying)
1、将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
2、当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:内存分配不用考虑内存碎片的情况
缺点:1、代价是将内存缩小为原来的一半
2、复制收集算法在对象存活率较高时就要进行较多的复制操作。效率会变低,由于需要额外的空间进行担保,所以老年代不能直接选用这种算法
现在商业虚拟机都采用这种收集算法来回收新生代
IBM公司研究表明:新生代的西爱过你98%都是“朝生夕死”,所以并没必要1:1分配内存,而是将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden和其中一块Survivor,当回收的时候,将Eden和Survivor中还存活的对象一次性的复制到另外的Survivor上,最后清理Eden区和刚才使用的Survivor区,HotSpot虚拟机默认Eden:Survivor=8:1 ,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%会浪费,
由于我们没法保证每次回收都之后不超过10%的对象存活,当Survivor空间不够,则需要依赖老年代,进行分配担保(handle promotion)
2.3 标记-整理算法(mark-compact)
1、标记
2、让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
2.4 分代收集算法
1、根据对象存活周期的不同将内存划分为几块。
2、一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
3、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
4、老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
3、垃圾回收器
如果有一种放之四海皆准,任何场景下都适合的完美收集器存在,那么hotspot虚拟机就没必要实现那么多不同的收集器
3.1 新生代收集器
3.1.1 Serial收集器
1、是一个单线程的收集器,“Stop The World”
2、对于运行在Client模式下的虚拟机来说是一个很好的选择
3、简单而高效
3.1.2 ParNew收集器
1、Serial收集器的多线程版本
2、单CPU不如Serial
3、Server模式下新生代首选,目前只有它能与CMS收集器配合工作
4、使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
5、-XX:ParallelGCThreads:限制垃圾收集的线程数。
6、除了Serial收集器外,只 有ParNew才能与CMS收集器配合工作
3.1.3 Parallel Scavenge 收集器
1、吞吐量优先”收集器
2、新生代收集器,复制算法,并行的多线程收集器
3、目标是达到一个可控制的吞吐量(Throughput)。
4、吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
5、两个参数用于精确控制吞吐量:
-XX:MaxGCPauseMillis是控制最大垃圾收集停顿时间
-XX:GCTimeRatio直接设置吞吐量大小
-XX:+UseAdaptiveSizePolicy:动态设置新生代大小、Eden与Survivor区的比例、晋升老年代对象年龄
6、并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
7、并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
8、停顿时间越短越适合需要用户交互的程序,良好的响应速度能提升用户体验,而吞吐量则可以高效的利用cpu时间,尽快的完成运算任务,主要适合后台不需要太多交互任务
3.2 老年代收集器
3.2.1 Serial Old收集器
1、Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
2、主要意义也是在于给Client模式下的虚拟机使用。
3、如果在Server模式下,那么它主要还有两大用途:
一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],
另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
3.2.2 Parallel Old收集器
1、Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
2、在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
3.2.3 CMS收集器
1、以获取最短回收停顿时间为目标的收集器。
2、非常符合互联网站或者B/S系统的服务端上,重视服务的响应速度,希望系统停顿时间最短的应用
3、基于“标记—清除”算法实现的
4、CMS收集器的内存回收过程是与用户线程一起并发执行的
5、它的运作过程分为4个步骤,包括:
初始标记,“Stop The World”,只是标记一下GC Roots能直接关联到的对象,速度很快
并发标记,并发标记阶段就是进行GC RootsTracing的过程
重新标记,Stop The World”,是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,但远比并发标记的时间短
并发清除(CMS concurrent sweep)
6、优点:并发收集、低停顿
7、缺点:
对CPU资源非常敏感。
无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
一款基于“标记—清除”算法实现的收集器
3.3 G1收集器
1、当今收集器技术发展的最前沿成果之一
2、G1是一款面向服务端应用的垃圾收集器。
3、优点:
并行与并发:充分利用多CPU、多核环境下的硬件优势
分代收集:不需要其他收集器配合就能独立管理整个GC堆
空间整合:“标记—整理”算法实现的收集器,局部上基于“复制”算法不会产生内存空间碎片
可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
4、G1收集器的运作大致可划分为以下几个步骤:
初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短
并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
5、它将整个java堆划分成了多个大小相等的独立区域(region)虽然还保留有新生代和老年代的概念,不再是物理隔离,都是region集合
3.4 垃圾收集器参数总结(*)
垃圾收集器参数总结
收集器设置:
-XX:+UseSerialGC:年轻串行(Serial),老年串行(Serial Old) (Serial+Serial Old)
-XX:+UseParNewGC:年轻并行(ParNew),老年串行(Serial Old) (ParNew+Serial Old)
-XX:+UseConcMarkSweepGC:年轻并行(ParNew),老年串行(CMS),备份(Serial Old)(ParNew+CMS)
-XX:+UseParallelGC:年轻并行吞吐(Parallel Scavenge),老年串行(Serial Old)
-XX:+UseParalledlOldGC:年轻并行吞吐(Parallel Scavenge),老年并行吞吐(Parallel Old)
垃圾回收统计信息:
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置:
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
4、JVM参数列表(*)
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-Xmx3550m:最大堆内存为3550M。
-Xms3550m:初始堆内存为3550m。
此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k设置每个线程的堆栈大小。:
JDK5.0以后每个线程堆栈大小为1M,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000左右。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。
设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=15:设置垃圾最大年龄。
如果设置为0的话,则年轻代对象不经过Survivor区,直 接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象 再年轻代的存活时间,增加在年轻代即被回收的概论。
5、理解GC日志
阅读GC日志是处理java虚拟机内存问题的基础技能,如下:
- 33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
- 100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
最前面的数字“33.125:”和“100.667:”代表了GC发生的时间
GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的。
接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。
后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量-> GC后该内存区域已使用容量 (该内存区域总容量)”。而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)”。
再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据
[Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs]
新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC (System)”。
6、JVM案例演示
6.1 java堆OOM
/** * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存异常时Dump出当前的内存堆 * 转储快照以便事后进行分析 * @author ll-t150 * */ public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } } }
结果:
解决方法:
这个时候需要明确是内存泄露(memory Leak)还是内存溢出(Memory OverFlow)
如果是内存泄露,可以进一步通过工具查看泄露对象到GC Roots的引用链,找到对象的类型信息,比较好定位泄露代码的位置
若不存在泄漏,就应该检查堆参数(-Xms与-Xmx),与机器物理内存对比是否可以调大
6.2 虚拟栈和本地方法栈OOM
抛出StackOverflowError异常,:线程请求的栈深度大于虚拟机所允许的深度
抛出OutOfMemoryError异常:虚拟机可以动态扩展,当扩展无法申请到足够的内存
/** * 虚拟机栈和本地方法栈OOM测试 * VM Args:-Xss128k */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }
运行结果:
原因:在单线程下,无论是帧栈太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机就会抛出StackOverFlowError异常
创建线程导致内存溢出异常:
如果不是单线程,通过不断地建立线程的方式可能产生内存溢出异常(Exception in thread “main” java.lang.OutOfMemoryError:unable to create new native thread)
但是这样产生的内存溢出异常与栈空间是否足够大不存在任何联系,该情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常
产生异常的原因:操作系统分配给每个线程的内存是有限制的,虚拟机提供了参数来控制java堆和方法区这两部分内存的最大值
譬如:虚拟机可用内存是2GB,减去Xmx(最大堆内存),再减去MaxPermSize(最大方法区内存),程序计数器消耗内存很小,可以忽略掉,剩下的内存就分配给虚拟机栈和本地方法栈,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时容易将剩下的内存耗尽
解决方法:如果建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机情况下,只能通过减少最大堆和减少栈容量来换取更多的线程
可以通过Kylin处理的例子为例
7、JVM监控工具
例如:Jconsole
jconsole是一种集成了上面所有命令功能的可视化工具,可以分析jvm的内存使用情况和线程等信息。
通过JDK/bin目录下的“jconsole.exe”启动Jconsole后,将自动搜索出本机运行的所有虚拟机进程,不需要用户使用jps来查询了,双击其中一个进程即可开始监控。也可以“远程连接服务器,进行远程虚拟机的监控。”
查看某个进程的内存消耗 jmap -heap Pid;
8、关于JVM应用场景
1、关于String的一些知识
字符串是常量,存在于方法区,创建之后不能更改,且是共享区域
字符串池,初始为空,它由类String私有维护,它把程序中的String对象放到池中,只要为们用到值相同的String,就是同一个String对象,便于节省空间,但也不是所有时候所有String对象都在这个池中,有可能在堆中
理解字面常量:
例如:int i=6 ;6就是一个整数型字面常量
String s=”abc”; “abc” 就是一个字符串字面常量,
所有的字符串字面常量都在字符串常量池中,可以共享
例如:String s1=”abc”;String s2 =”abc” ;s1和s2都引用同一个字符串对象,且这个对象在常量池中,以此 s1==s2
字符串字面常量在类加载的时候实例化放到字符串池中
1、任何类任何包,值相同的字符串常数都引用同一个对象
2、常量表达式(constant expression)计算出来的字符串,也在常量池中
3、在程序运行时通过连接符(+)计算出来的字符串对象,是新创建的,他们不是字符串字面常量,不再池中,而是在堆内存中,因此引用对象不同
4、String类的intern方法,返回一个值相同的String对象,然后放入常量池中
什么是常量表达式:简而言之,编译时能确定的就是,运行时才能确定的就不是
什么是常量变量:被final修饰的,并且通过常量表达式初始化的变量
final String s2 = getA();//s2 不是常量变量,但是s2引用的“a”在常量池中
public String getA(){return "a";} // s2不是常量变量,因为getA()不是常量表达式
String s3=s2+”abc”;//此时s3不算常量表达式,因为s2不是常量变量
综合例子:
public class StringTest { public static void main(String[] args) { String s = new String("abc"); //创建了几个对象 String s1="abc"; String s2="a"; final String s21 ="a"; //这个时候 s21是一个常量变量 final String s22 = getA(); //s22不是常量变量 String s3 =s2+"bc"; String s31 =s21+"bc"; String s32 =s22+"bc"; String s4 = "a"+"bc"; //编译时确定 String s5 =s3.intern(); //返回一个值相同的String对象,放入常量池中 System.out.println("s1==s3:"+(s1==s3));//false 运行时确定,new的对象放入堆中 System.out.println("s1==s31:"+(s1==s31));//true System.out.println("s1==s32:"+(s1==s32));//false System.out.println("s1==s4:"+(s1==s4));//true System.out.println("s1==s5:"+(s1==s5));//true } public static String getA(){ return "a"; } }
JVM技术部分总结