JVM 内存结构
Java栈(java stack): 线程私有, 存储局部变量表、操作数栈、动态链接、方法出口等信息;方法的执行对应着栈帧出站入栈的过程;存放着基本数据类型和各种对象的引用。
本地方法栈(native method stack) 线程私有, 保存native方法的信息。当调用native方法时,不会为其操作栈帧,而是简单的通过动态链接直接调用native方法。
程序计数器(program counter register)线程私有, 保存当前线程执行的行号。
堆(Heap):存放对象、以及属于对象的基本类型数据。
方法区(Method Area):
也叫永久区,存储已经被虚拟机加载的类信息、常量、静态变量等数据。
JDK1.7将原本存放在方法区中的运行时常量池(字符串池和类的静态变量)存储在Heap堆中。
JDK1.8中没有方法区,继续将原方法区中的class数据(元数据)放在本地内存中。
本地内存:操作系统内存,里面包含直接内存DirectBuffer(在NIO中会频繁使用,可以通过MaxDirectMemorySize来设置,默认与堆内存最大值一样。),JDK1.8以后还包含元数据区。
堆和栈的区别是什么?
功能
以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
线程独享还是共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
空间大小
栈的内存要远远小于堆内存,栈的深度是有限制的(默认大小1M),如果递归没有及时跳出,很可能发生StackOverFlowError问题。
你可以通过-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值。
JDK1.7、JDK1.8内存结构的变化:
JDK1.8将原本存放在方法区中的运行时常量池(字符串池和类的静态变量)存储在Heap堆中。
JDK1.8中没有方法区,继续将原方法区中的class数据(元数据)放在本地内存中。
为什么去除方法区:
永久代来存储类信息、常量、静态变量等数据不是个好主意, 很容易遇到永久区内存溢出的问题。JDK8的实现中将类的元数据放入native memory, 将字符串池和类的静态变量放入java堆中。可以使用MaxMetaspaceSize对元数据区大小进行调整;
对永久代进行调优是很困难的,同时将元空间与堆的垃圾回收进行了隔离,避免永久代引发的Full GC和OOM等问题;
有哪些java内存溢出异常?
OutOfMemeoryError:当堆、栈(多线程情况)、方法区(JDK1.7及以前)、元数据区(JDK1.8)、直接内存中数据达到最大容量时产生;
StackOverFlowError:如果线程请求的栈深度大于虚拟机栈允许的最大深度,将抛出StackOverFlowError,其本质还是数据达到最大容量;
什么情况下出现堆溢出?怎么解决?
产生原因
堆用于存储实例对象,只要不断创建对象,并且保证GC Roots到对象之间有引用的可达,避免垃圾收集器回收实例对象,就会在对象数量达到堆最大容量时产生OutOfMemoryError异常。
java.lang.OutOfMemoryError: Java heap space
解决办法
使用-XX:+HeapDumpOnOutOfMemoryError可以让java虚拟机在出现内存溢出时产生当前堆内存快照以便进行异常分析,主要分析那些对象占用了内存;也可使用jmap将内存快照导出;一般检查哪些对象占用空间比较大,由此判断代码问题,没有问题的考虑调整堆参数;
什么情况下出现栈溢出?怎么解决?
产生原因
如果线程请求的栈深度大于虚拟机锁允许的最大深度,将抛出StackOverFlowError; 比如:方法死递归。
如果虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemeoryError; OutOfMemeoryError抛出之前会用尽所有操作系统内存空间。比如:new出无限多个线程,每个线程中有较多局部基本类型变量。
解决办法
StackOverFlowError 一般是函数调用层级过多导致,比如死递归、死循环。
OutOfMemeoryError 一般是在多线程环境才会产生:1、减少最大堆容量,为栈腾挪出更多空间。 2. 减少单个栈(默认单个容量是1M)容量。 3、增加物理内存。
什么情况下出现方法区或元数据区溢出?怎么解决?
产生原因
jdk 1.6以前,运行时常量池还是方法区一部分,当常量池满了以后(主要是字符串变量),会抛出OOM异常;
方法区和元数据区还会用于存放class的相关信息,如:类名、访问修饰符、常量池、方法、静态变量等;当工程中类比较多,而方法区或者元数据区太小,在启动的时候,也容易抛出OOM异常;
解决办法
jdk 1.7之前,通过-XX:PermSize,-XX:MaxPerSize,调整方法区的大小;
jdk 1.8以后,通过-XX:MetaspaceSize ,-XX:MaxMetaspaceSize,调整元数据区的大小;
什么情况下出现本机直接内存溢出?怎么解决?
产生原因
jdk本身很少操作直接内存,而直接内存(DirectMemory)导致溢出最大的特征是,Heap Dump文件不会看到明显异常,而程序中直接或者间接的用到了NIO;
解决办法
直接内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样)。
内存怎么样分配
新生代占堆大小1/3(其中Eden:from:to= 8/10 : 1/10 : 1/10),老年代占堆大小2/3
对象分配
优先在Eden区分配。当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域。如Minor GC时survivor空间不够,对象提前进入老年代,老年代空间不够时进行Full GC;
大对象直接进入老年代,避免在Eden区和Survivor区之间产生大量的内存复制, 此外大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间.
对象晋级
年龄阈值:VM为每个对象定义了一个对象年龄(Age)计数器, 经第一次Minor GC后仍然存活, 被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1.当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代.
提前晋升: 动态年龄判定;如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无须等到晋升年龄.
哪些对象要收回?生死判定
可达性分析算法
回收对象主要针对堆内存,JDK1.7以前还包括方法区中的常量和静态变量。JDK1.8中的元数据区不会被垃圾回收,元数据不断扩大会造成系统卡死。
通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的;
在Java, 可作为GC Roots的对象包括:
1.方法区: 类静态属性引用的对象;
2.方法区: 常量引用的对象;
3.虚拟机栈(本地变量表)中引用的对象.
4.本地方法栈JNI(Native方法)中引用的对象。
怎么回收?方法论
标记清除法(主要用于新生代)
该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.
缺点:
效率问题: 标记和清除过程的效率都不高;
空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集.
复制算法(主要用于新生代)
该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉.
优点:
由于是每次都对整个半区进行内存回收,内存分配时不必考虑内存碎片问题。
垃圾回收后空间连续,只要移动堆顶指针,按顺序分配内存即可;
特别适合java朝生夕死的对象特点;
缺点
内存减少为原来的一半,太浪费了; (jdk中对此做了优化:幸存区只占新生代的1/10,所以浪费的空间很小)
对象存活率较高的时候就要执行较多的复制操作,效率变低;、
如果不使用50%的对分策略,老年代需要考虑的空间担保策略
标记整理算法(主要用于老年代)
该算法分为“标记”和“整理”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后让所有存活的对象都向一端移动,然后清理掉端边界以外的内存;
优点:
不会损失50%的空间;
垃圾回收后空间连续,只要移动堆顶指针,按顺序分配内存即可;
比较适合有大量存活对象的垃圾回收;
缺点
标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
分代收集
将整个堆内存分为新生代、老年代,新生代又分为Eden区和两个survival区。每个区使用不同的收集策略,新生代使用复制算法,老年代使用标记整理算法。
优点:1. 根据每个区的特点分别发挥了不同算法的优势。
实现回收,谁来做?常用的垃圾回收器
Serial
收集对象和算法:新生代,复制算法
收集器类型:单线程
说明:进行垃圾收集时,必须暂停所有工作线程,直到完成;(stop theworld)
适用场景:简单高效;适合内存不大的情况;
ParNew
新生代,复制算法
并行的多线程收集器
ParNew垃圾收集器是Serial收集器的多线程版本
搭配CMS垃圾回收器的首选
Parallel Scavenge吞吐量优先收集器
新生代,复制算法
并行的多线程收集器
类似ParNew,更加关注吞吐量,达到一个可控制的吞吐量;
本身是Server级别多CPU机器上的默认GC方式,主要适合后台运算不需要太多交互的任务;更适用于做后台数据分析运算。
注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间),垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间
Serial Old
老年代,标记整理算法
单线程
jdk7/8默认的老生代垃圾回收器
Client模式下虚拟机使用
Parallel Old
老年代,标记整理算法
并行的多线程收集器
Parallel Scavenge收集器的老年代版本,为了配合Parallel Scavenge的面向吞吐量的特性而开发的对应组合;
在注重吞吐量以及CPU资源敏感的场合采用
CMS
老年代,标记清除算法
并行与并发收集器
尽可能的缩短垃圾收集时用户线程停止时间;缺点在于:
1.内存碎片
2.需要更多cpu资源
3.浮动垃圾问题,需要更大的堆空间
重视服务的响应速度、系统停顿时间和用户体验的互联网网站或者B/S系统。互联网后端目前cms是主流的垃圾回收器;
G1
跨新生代和老年代;标记整理+化整为零
并行与并发收集器
JDK1.7才正式引入,采用分区回收的思维,基本不牺牲吞吐量的前提下完成低停顿的内存回收;可预测的停顿是其最大的优势;
面向服务端应用的垃圾回收器,目标为取代CMS
垃圾回收默认配置及互联网后台推荐配置
在JVM的客户端模式(Client)下,JVM默认垃圾收集器是串行垃圾收集器
(Serial GC + Serial Old,-XX:+USeSerialGC);
在JVM服务器模式(Server)下默认垃圾收集器是并行垃圾收集器
(Parallel Scavaenge +Serial Old,-XX:+UseParallelGC)
基于不同使用场景,Server模式配置如下:
互联网更高的用户体验要求采用CMS,但是CMS的缺点是容易产生更多内存碎片同时对内存容量要求更高,所以当内存占满时需要SerialOld担保清理掉所有碎片。
ParNew + CMS + Serial Old(失败担保),-XX:UseConcMarkSweepGC;
后台数据分析需要更高的吞吐量
Parallel scavenge + Parallel Old,-XX:UseParallelOldGC
JVM垃圾回收机制,何时触发MinorGC或FullGC等操作
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,
对老年代GC称为Major GC,而Full GC是对整个堆来说;
Minor GC触发条件:
当Eden区满时,触发Minor GC。
Full GC触发条件:
System.gc() 或 Runtime.getRuntime().gc(); 前者调用了后者。
老年代空间不足
永生区空间不足
统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
堆中分配很大的对象
默认情况下,System.gc()会显式直接触发FullGC,同时对老年代和新生代进行回收。而一般情况下我们认为,垃圾回收应该是自动进行的,无需手工触发。如果过于频繁地触发垃圾回收对系统性能是没有好处的。因此虚拟机提供了一个参数DisableExplicitGC来控制是否手工触发GC。使用方法: -XX:+DisableExplicitGC
JVM Server模式与client模式启动,
最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升.
原因是:
当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器,
而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,服务起来之后,性能更高.
java -version 可以直接查看出你使用的是client还是 server:
java version "1.6.0_06"
Java(TM) SE Runtime Environment (build 1.6.0_06-b02)
Java HotSpot(TM) “Server” VM (build 10.0-b22, mixed mode)
两种模式的切换可以通过更改配置(jvm.cfg配置文件)来实现:
32位的虚拟机在目录JAVA_HOME/jre/lib/i386/jvm.cfg,
64位的在JAVA_HOME/jre/lib/amd64/jvm.cfg,
目前64位只支持server模式, 一般只要变更 -server KNOWN 与 -client KNOWN 两个配置位置先后顺序即可,前提是JAVA_HOME/jre/bin 目录下同时存在 server 与client两个文件夹,分别对应着各自的jvm.
java常用调优工具
jps
功能和ps命令类似,列出正在运行的虚拟机进程,可以显示执行主类名称以及LVMID
jstat
监视虚拟机各种运行状态信息,可以显示本地或远程虚拟机中类装载、内存、垃圾回收等运行数据;
eg: jstat -class pid
jinfo
实时查看和调整虚拟机各项参数;
eg: jinfo pid
jmap
用于生成对转存快照。还可以查询finalize执行队列,堆和永久代的详细信息;展示 –dump、-heap、-histo
eg: jmap -heap pid 显示垃圾回收器名称
jmap -histo pid 显示加载了多少类,每个类有多少实例
jmap -dump:format=b,file=filename.bin pid 导出堆快照
jstack
用于生成虚拟机当前时刻的线程快照,包含虚拟机中每一个线程正在执行的方法堆栈的集合,用于定位线程出现长时间停顿的原因,如死锁、死循环、外部资源长时间等待等;
Jconsole
一种基于JMX的可视化监控、管理工具。包括概述、内存(jstat)、线程(jstack)、类(jstat)、vm概要(jinfo)
jvisualvm
主体功能和jconsole差别不大,区别在于:1.具备插件扩展的能力;2.能生成堆、栈的存储快照;3.分析程序性能 Profile选项卡,查看占用最大CPU和内存的方法。
堆dump分析:堆dump分析主要目的是定位OOM异常的原因;解决oom问题 四部曲:
1. 分析OOM异常的原因,堆溢出?栈溢出?本地内存溢出?
2. 如果是堆溢出,导出堆dump,并对堆内存使用有个整体了解;
3. 找到最有可能导致内存泄露的元凶,通常也就是消耗内存最多的对象;
4. 使用辅助工具对dump文件进行分析;
注意其他几类造成OOM异常的原因:
1. Direct Memory
2. 线程堆栈:
单线程:StackOverflowError
多线程:OutOfMemoryError:unable to create new native thread
3. Socket 缓冲区:IOException:Too many open files。 打开了很多socket没有关闭
线程dump分析: 线程dump分析主要目的是定位线程长时间停顿的原因;
原因 线程状态 举例
等待外部资源 Runnable 数据库连接、网络资源、设备资源
死循环 Runnable 代码bug
锁 Waiting 活锁(线程一直waiting状态,没有其他线程去notify),死锁
应用故障你怎么样排除问题?
应用故障一般指应用运行缓慢、用户体验差或者周期性的出现卡顿,排除的思路:
1. 检查应用所在的生产环境的软硬件以及网络环境,排除外围因素;
2. 确定是否为OOM异常,这类异常影响最恶劣,但是比较容易排查;
3. 确定是否有大量长时间停顿的应用线程,非常占用cpu资源;
4. 周期性的卡顿很可能是垃圾回收造成,web后端系统建议使用cms垃圾回收器;