1、java内存区域
·程序计数器:当前线程所执行字节码的行号指示器,如果线程执行的是一个java方法,则记录的是正在执行的虚拟机字节码指令的地址。
·java虚拟机栈:栈帧,存储局部变量表、操作数栈、方法出口等,每个方法调用到结束,就对应一个栈帧在虚拟机栈从入栈到出栈的过程。
·本地方法栈:为虚拟机使用到的本地(Native)方法服务。
·堆:所有的对象实例都应当在堆上分配(也有栈上分配等),是垃圾管理器管理的区域,可以固定大小,也可以拓展(通过参数-Xmx -Xms)。
·方法区:存储已被虚拟机加载的类型信息、常量、静态变量等。java 8以前,使用永久代实现方法区,该区域的内存回收主要是针对常量池的回收及对类型的卸载,java 8以后用元空间实现。
为什么要使用元空间:
①字符串常量池存在于永久代中,容易出现性能问题和内存溢出。(1.7就把常量池从方法区移动到了堆里面)
②类的方法的信息大小又难以确定,因此给永久带的大小指定比较困难,两者最大的区别是元空间使用本地内存,而永久代使用的是JVM的内存,使用本地内存的好处就是当遇到Java.lang.outofMemoryError:PermGen Space,将不会存在,因为默认的类的元数据分配只受本地内存大小的限制,也就是说,本地内存剩余多少理论上metaspace就可以有多大,这解决了空间不足的问题。
2、对象内存布局
对象在堆中分为三个部分:
对象头:包含两类信息,mark word和类型指针。mark word存储对象运行时数据,如哈希码、gc分代年龄、锁标示、持有的锁等;类型指针用来确定该对象是哪个类的实例。
实例数据:对象真正存储的信息,即我们自定义的各个字段内容。
对齐填充:不是必须的,起占位符作用。
*这里就有一个常见的面试题:分配一个对象会占用多大的内存空间?
答案:一个Java对象到底占用多大内存? - zhanjindong - 博客园
3、垃圾收集和内存分配
四大引用:
强引用:只要有强引用存在,垃圾收集器就不会回收,可以直接访问对象,会导致内存泄露。
软引用:java.lang.ref.SoftReference,一些有用,但非必须的对象。在系统将要出现内存溢出前,会将这些对象列为第二次回收对象,如果此次回收还没有足够内存,则抛出OOM。
实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。使用软引用能防止内存泄露,增强程序的健壮性。
弱引用:java.lang.ref.WeakReference,在系统GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收
虚引用:不能get获取对象,并且虚引用必须和引用队列一起使用,它的作用在于检测对象是否已经从内存中删除,跟踪垃圾回收过程。仅用于对象被回收时收到一个系统通知,或后续添加进一步处理。
判断对象是否存活的算法:
·引用计数法:对象中添加引用计数器;原理简单。效率高,但是有循环依赖问题。
·可达性分析算法:GC Roots起始节点,找出所有引用链。
可作为GC Roots的对象:
·全局性引用:例如常量或类静态变量等
·可执行上下文:栈帧中本地变量表
在可达性分析中判定为不可达的对象,还会看对象是否有必要执行finalize()方法,假如没有覆盖finalize()方法或者已经调用过,则是没必要执行;如果有必要执行,则会把对象放在一个F-Queue队列中,由一个单独的线程执行,执行后如果还是没引用则真的被回收。
方法区回收:废弃的常量和不再使用的类型。
判断类型不再使用的三个条件:
·类所有实例已被回收。
·类的类加载器已经被回收。
·类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射获取该类方法。
垃圾收集算法:
分代收集理论基础(2大理论):绝大多数对象都是朝生夕死;熬过越多次垃圾收集算法的对象越不容易消亡。
java一般把堆分为新生代和老年代。
跨代引用:老年代对象引用新生代对象,没必要为了少量跨代引用去扫描整个老年代。在新生代建立一个全局的数据结构(记忆集),这个结构把老年代划分为若干小块,标示出老年代哪一块内存存在跨代引用。这部分内存中的对象会被加入GC Roots进行扫描。
标记-清除算法:
最早出现也是最基础的算法。标记需要回收对象,统一回收。
·执行效率不稳定,堆中对象多,则需要标记很多对象,效率低。
·空间碎片化,可能导致下次分配大对象,找不到连续区域,再次触发垃圾收集
标记-复制算法(复制算法): 新生代优先采用这种算法
将可用内存划分为大小相等的两块,每次只使用一块,当一块内存用完了,将存活的对象复制到另一块上,再把当前块清除。运行高效,浪费空间(可用空间只有一半)。
标记-整理算法:老年代优先采用
将存活对象移动到空间一端,清理边界外内存。 移动对象需要"stop the world",但是可以解决空间碎片化问题。
HotSpot算法细节:
根节点枚举:
固定可作为GC Roots的主要是全局性引用(常量或类静态属性等)与执行上下文(栈帧中本地变量表),这一过程必须暂停用户线程,在一个能保障一致性的快照中进行。
借助于Oomap(类加载完成后,用一组称为Oomap的数据结构存储什么对象内什么偏移量上是什么类型的数据计算出来)快速完成GC Roots枚举。
安全点:
用户线程只能到达安全点后才能暂停开始垃圾收集;安全点是以“是否具有让程序长时间执行的特征”选定的,最明显特征就是指令序列的复用,如方法调用、循环跳转等。
如何在垃圾收集时让线程跑到最近安全点:主动式中断,设置一个标志位,线程执行过程中不断轮询标志位,一旦发现中断为真就在最近的安全点上挂起。(轮询标志和安全点重合)
安全区域:在某一段代码区域,引用关系不会发生变化。用户线程进入安全区域时会标示自己进入了安全区域,如果这段时间要垃圾收集,就不用管当前线程;离开安全区域时检测是否完成根节点枚举,完成就不管,未完成就等待,等收到可以离开为止。
三色标记法:
白色:对象未被垃圾收集器访问过,可达性分析刚开始都是白色,结束时仍是白色的表示不可达。
黑色:对象已被垃圾收集器访问过,且对象的所有引用都已经扫描了。
灰色:对象被垃圾收集器访问过,但是至少存在一个引用没扫描过。
记忆集和卡表:
分代收集理论的时候,会存在对象跨代引用的问题。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。记忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现。卡表中每个记录精确到一块内存区域,表示该区域中有对象包含跨代指针。
卡表是使用一个字节数组实现:CARD_TABLE[this addredd >>9]=0
,每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。hotSpot使用的卡页是2^9大小,即512字节
垃圾收集器:
并行(Parallel):多条垃圾收集器线程之间的关系。默认用户线程处于等待状态。
并发(Concurrent):垃圾收集器线程与用户线程之间的关系,同一时间同时运行。
新生代:
·Serial收集器:
新生代收集器,单线程工作的收集器,在进行垃圾收集时,必须暂停其他所有线程。采用复制算法。
·ParNew收集器:
新生代收集器,实质上是Serial收集器的多线程并行版本,只有它能和CMS收集器配合工作。采用复制算法。
·Parallel Scavenge收集器:关注吞吐量
新生代收集器,基于标记-复制算法,关注吞吐量(处理器用于运行用户代码与处理器总消耗时间的比值)。
老年代:
·Serial Old收集器:
Serial收集器的老年版本,单线程收集器,使用标记-整理算法。
·Parallel Old收集器:
Parallel Scavenge的老年代版本,基于标记-整理算法,Parallel Scavenge+Parallel Old配合使用。
·CMS收集器:获取最短停顿时间为目标
基于标记-清除算法,分为四个步骤:
·初始标记:仅仅标记一下GC Roots能直接关联到的对象,耗时短,需要暂停用户线程。
·并发标记:从GC Roots直接关联的对象开始遍历整个对象图,耗时长,不需要暂停用户线程。
·重新标记:为了修正并发标记期间和用户线程并发导致标记变化的那部分对象,采用增量更新,需要暂停用户线程。
·并发清除:清理删除标记节点已经死亡的对象,因为不需要移动对象,所以不需要暂停用户线程。
3个缺点:
·并发占用cpu资源,处理器资源少的,影响很大。CMS 默认启动的回收线程数是 (CPU数量+3)/ 4, 如果 CPU 数量只有一两个,那吞吐量就直接下降 50%,显然是不可接受的。
·并发运行导致会产生“浮动垃圾”,可能出现 「Concurrent Mode Failure」而导致另一次 Full GC 的产生,由于在并发清理阶段用户线程还在运行,所以清理的同时新的垃圾也在不断出现,这部分垃圾只能在下一次 GC 时再清理掉(即浮动垃圾),同时在垃圾收集阶段用户线程也要继续运行,就需要预留足够多的空间要确保用户线程正常执行,这就意味着 CMS 收集器不能像其他收集器一样等老年代满了再使用,JDK 1.5 默认当老年代使用了68%空间后就会被激活,当然这个比例可以通过 -XX:CMSInitiatingOccupancyFraction 来设置,但是如果设置地太高很容易导致在 CMS 运行期间预留的内存无法满足程序要求,会导致 Concurrent Mode Failure 失败,这时会启用 Serial Old 收集器来重新进行老年代的收集,而我们知道 Serial Old 收集器是单线程收集器,这样就会导致 STW 更长了。
·CMS 采用的是标记清除法导致产生内存碎片,如果不整理的话,则会有大量不连续的内存空间存在,无法放入一些进入老年代的大对象,导致老年代频繁垃圾回收。所以CMS存在一个默认的参数 “-XX:+UseCMSCompactAtFullCollection”,意思是在Full GC之后再次STW,停止工作线程,整理内存空间,将存活的对象移到一边。还要一个参数是“-XX:+CMSFullGCsBeforeCompaction”,表示在进行多少次Full GC之后进行内存碎片整理,默认为0,即每次Full GC之后都进行内存碎片整理。
注:浮动垃圾是在并发阶段预留的资源不够导致产生「Concurrent Mode Failure」,这个时候会会启用 Serial Old 收集器来重新进行老年代的收集;而内存碎片是gc完后连续空间不够分配大对象,触发full gc,可以进行碎片整理解决。
G1收集器:
开创了收集器面向局部收集的思路和基于Region的内存布局,把java堆划分为多个大小相等的独立区域(Region,不需要连续),Region是单次回收的最小单元,即每次收集的空间是Region大小的整数倍。具体的就是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收获取的空间及回收所需时间的经验值,维护一个优先级列表,每次根据用户设定的允许停顿时间(-XX:MaxGCPauseMillis指定,默认200毫秒)优先处理回收价值最大的Region,也是用了卡表维护多个Region之间的对象引用。
存在的问题:
·耗费大约堆容量10%-20%的内存维持收集器工作。
·每个Region会划分部分区域用于回收过程中新对象的分配,设计了TAMS指针,新分配的对象都在TAMS指针之上。
·不容易建立可靠的停顿预测模型,价值不好估算。
G1收集器运行过程划分为以下4个步骤:(除了并发标记,均需暂停用户线程)
·初始标记:仅仅标记一下GC Roots直接关联的对象,修改TAMS指针的值,让下一阶段并发运行时,能正常新分配对象,需暂停用户线程。
·并发标记:从GC Roots开始对堆进行可达性分析,扫描对象图,找出回收对象,可与用户线程并发执行。
·最终标记:暂停用户线程,处理并发阶段遗留的少量记录,采用原始快照方法。
·筛选回收:对各个Region回收价值、成本进行排序,根据用户期望停顿时间制定回收计划,自由选择多个Region进行回收,将回收的Region中存活对象复制到空的Region中,清理旧的Region空间,因为涉及移动对象,所以需要暂停用户线程。
CMS与G1对比:
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
区别四: 垃圾回收的过程不一样
CMS收集器 G1收集器
- 初始标记 1.初始标记
- 并发标记 2. 并发标记
- 重新标记 3. 最终标记
- 并发清除 4. 筛选回收
实战:内存分配与回收策略
·对象优先在Eden分配:大多数情况,对象在新生代Eden分配,当Eden区没有足够空间进行分配,虚拟机发起一次Minor GC。
·大对象直接进入老年代:大对象(很长的字符串或元素数量很大的数组等),占用过多新生代空间,容易经常触发垃圾收集,以及两个Survivor区来回复制算法带来的高额复制开销。(-XX:PretenureSizeThreshold参数指定,大于该值直接在老年代分配,但只对Serial和ParNew生效)
·长期存活对象直接进入老年代:根据对象年龄判断,经过一次gc仍存活则年龄+1,默认增加到15则进入老年代。(-XX:MaxTenuringThreshold=15)
·动态对象年龄判断:如果在Survivor空间中相同年龄所有对象和大于Survivor空间的一半,则年龄大于等于该年龄的对象就直接进入老年代。
·空间分配担保:发生Minor GC前,虚拟机检查老年代最大可用的连续空间是否大于新生代对象总空间,如果大于,则这次gc安全。如果不大于,则先查看-XX:HandlePromotionFailure参数设置是否允许担保失败;允许,则再检查老年代最大可用的连续空间是否大于历次晋升到老年代的平均大小,如果大于,则尝试进行一次Minor GC,尽管这次gc有风险;如果小于,或者-XX:HandlePromotionFailure参数不允许冒险,则进行一次Full GC。
常用jvm相关命令和工具
jps:列出正在运行的虚拟机进程
-l 输出主类的全名
-v 输出虚拟机进程启动时的jvm参数
jstat:监视虚拟机各种运行状态信息 【jstat -gcutil 19212】
-gc 监视java堆状况,包括Eden区、2个Survivor区、老年代、永久代等的容量,已用空间。垃圾收集时间合计等信息。
-gcutil 监视内容与-gc相同,但输出主要关注已使用空间占总空间的比例
jinfo:实时查看和调整虚拟机各项参数
jmap:生成堆快照
-dump:生成java堆快照【jmap -dump:format=b,file=eclipse.bin 3400】
-heap:显示java堆详细信息,如使用哪种回收器、参数配置、分代状况等。
jstack:生成虚拟机当前时刻的线程快照(一般配合top使用)
可视化工具:JConsole、VisualVM、JMC等。
线上cpu占用高问题排查:
1、Top看哪个进程占用cpu高
2、top -Hp pid 查进程的线程
3、jstack -l 线程id > 文件名 导出文件
4、线程id转换为十六进制,定位查原因
类加载
点我跳转-->Java类加载器及加载范围、双亲委派模型及破坏双亲委派模型
内存泄漏和内存溢出 --->什么叫内存溢出?内存泄漏的定义又是什么?使用中如何避免?-51CTO.COM
内存溢出(out of memory):简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。
内存泄漏( memory leak):内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其他程序,于是就发生了内存泄漏。
memory leak会最终会导致out of memory
锁升级及降级
synchronized锁升级 :无锁->偏向锁->轻量级->重量级锁
锁降级:锁降级指的是写锁降级为读锁的过程,他的过程是持有写锁,获取读锁,然后释放写锁。