1. Java内存区域划分
JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用处,各有各的创建与销毁时间,有的区域随着JVM进程的启动而存在,有的区域则依赖用户线程的启动和结束而创建与销毁。一般来说,JVM所管理的内存将会包含以下几个运行时数据区域:
-
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
-
线程共享区域:Java堆、方法区、运行时常量池
1.1 线程私有区域
程序计数器
记录的是当前线程所执行的字节码行号指令器地址。如果执行的是一个native
方法,计数器为空。程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!
Java虚拟机栈
描述的是方法执行的内存模型,每个方法执行的同时都会创建一个栈帧用于存储局部变量、操作数、动态链接、方法出口等信息。我们通常说的栈区域实际上就是此处的虚拟机栈,即局部变量表。
- 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出
StackOverFlowError
- 虚拟机在动态扩容时无法申请到足够的内存,堆空间不够了,将会抛出
OutOfMemoryError(OOM)
异常
本地方法栈
跟虚拟机栈作用基本一样。不同的就是,本地方法栈为虚拟机使用的native
方法服务,而虚拟机栈为JVM执行的Java方法服务。
- 在
HotSpot
虚拟机中,本地方法栈和虚拟机栈是同一块区域
1.2 线程共享区域
Java堆
Java堆(Java Heap
)是垃圾回收器管理的主要区域,又叫“GC堆”。可以处于物理上不连续的内存空间中。
- 如果在堆中没有足够的内存完成实例分配并且堆也无法再进行拓展时,将会抛出
OOM
异常
方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。此区域的回收主要是针对常量池的回收及堆类型的卸载。
- 当方法区无法满足内存分配需求时,将会抛出
OOM
异常
JDK1.8以前该空间叫“永久代”,JDK1.8以后叫做元空间(MetaSpace)。
运行时常量池(方法区的一部分)
存放字面量和符号引用。
- 字面量:字符串、final常量、基本数据类型的值
- 符号引用:类和结构的全限定名、字段的名称和描述符、方法的名称和描述符 (运行时常量池
JDK1.7
后移到了堆中!!!)
2. Java内存溢出异常
1.1 Java堆溢出
产生原因:不断地创建对象
JVM参数:-Xms
设置堆
的最小值 -Xmx
设置堆
的最大值
内存泄漏(Memory Leak
):本应该回收的对象没有回收,泄露对象无法被GC
内存溢出(Memory Overflow
):内存不够了
解决方法:堆内存放大/生命周期变短
1.2 虚拟机栈和本地方法栈溢出
HotStop虚拟机将虚拟机栈与本地方法栈合二为一
栈容量
只需要由-Xss
参数来设置
产生原因:递归调用;递归无出口
关于虚拟机栈会产生的两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出
StackOverFlow
异常 - 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出
OOM
异常
3. 垃圾回收算法
主要针对堆和方法区,因为线程私有的部分,随线程生而生,随线程灭而灭,线程私有的三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。所以只有线程共享的部分需要回收!!!
3.1 Java堆回收
- 回收对象就要如何判断对象已死?
对象计数法
给对象增加一个计数器,有人引用计数器加一,没人引用时计数器减一,计数器为 0 时表示对象已死。
缺陷:解决不了循环引用问题
- 问题:java中用的是不是计数法?
创建两个对象,相互引用,然后再释放,垃圾回收一下,看有没有释放掉,如果释放了,说明java用的不是计数法。
public class Test {
private Object instance;
private byte[] bigSize = new byte[2 * 1024 * 1024];
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
//相互引用
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
System.gc();
}
}
JVM内存回收策略不是计数法,是可达性分析算法!
可达性分析算法
工作流程:
通过一系列通过“GC Roots”的对象作为起点,向下搜索,如果当前对象到GC Roots有路可走(GC roots到当前对象可达),认为此对象还存活;如果当前对象到GC Roots没有引用链,认为此对象不可用。
在Java语言中,可作为GC Roots的对象包含下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
强引用:能找到GC Roots就不会回收,对象不可达才会回收
除了强引用的其他引用都是:根据内存的情况,内存不够了就会回收,即便是可达的,保证内存不溢出
软引用:把某个对象赋给一个变量,但是需要这个对象可有可无的时候可以使用
把对象管理起来,如果本次GC内存不够,就GC回收,回收完之后还不够,OOM(二次回收)
弱引用:不需要对象的时候使用(ThreadLocal)
只要发生GC时就回收(一次回收)
虚引用:这个对象基本用不到
主要用于接收一个垃圾回收器的通知
- 生存还是死亡?
没有人引用时,它应该死掉,用户选择覆写finalize
方法,把这个对象再次赋给一个变量,再次活下来
如果一个类覆写了finalize方法(自救), GC在回收对象时,第一次会调用finalize方法,再次GC时就不再调用了。finalize只会被虚拟机执行一次!!
3.2 方法区回收
方法区(永久代,1.8元空间)的垃圾回收主要回收两部分内容:废弃常量、无用类。废弃常量的回收与Java堆对象回收类似。
什么类是无用的?(无对象、无ClassLoader、无反射调用)
- 该类所有的实例都已经被回收(Java堆中不存在该类的任何实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有任何在其他地方被引用,无法在任何地方通过反射访问该类的方法
JVM会对满足以上三点的无用类进行回收(类卸载),仅仅是“可能“。大量使用反射、动态代理的场景下,JVM会不定期对方法区进行回收来防止永久代溢出。
3.1 垃圾回收算法
针对的都是Java堆!!
标记-清除算法
工作流程:
算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象(标记阶段),在标记完成后统一回收所有被标记的对象(回收阶段)。
缺点:
a. 效率问题:标记和清除这两个阶段效率都不高
b. 空间问题:会产生大量不连续片段
复制算法(新生代垃圾回收算法)
工作流程:将内存划分为大小相等的两块,每次只使用其中的一块,另一块作为保留区域。当进行垃圾回收时,将使用区域的存活对象一次性复制到保留区域,而后一次性清空使用区域。
使用场景:新生代垃圾回收
新生代对象具有朝生夕灭的特性(新生代对象存活率低)
优点:实现简单,运行高效,无空间碎片问题
JVM采用的复制算法: 将内存(新生代内存)划分为一块较大的
Eden
区和两块大小相等、空间较小的Survivor
区。
(Eden : Survivor = 8 :1)。
每次使用Eden区和其中一块Survivor区域(Survivor区域一块叫From区,另外一块叫做To区)
在进行垃圾回收(Eden区快满的时候)时:
- 第一次进行GC时,将Eden区中的存活对象移动到From区。
- 第二次再进行GC时,将Eden区和From区存活对象移动到To区,然后一次性清理掉Eden区的From区,以此循环。
- 对象在From区和To区来回移动15次(默认),将此对象移动到老年代
特殊情况: 当Survivor放不下存活对象时,会从老年代进行分配担保
标记-整理算法(老年代垃圾回收算法)
复制算法在对象存活率较高时,会有大量对象复制操作,效率很低。因此,老年代不采用复制算法。
工作流程:
标记阶段和标记-清除阶段一样,标记出无用对象。
整理阶段:将存活对象向一端移动,而后一次将存活对象边界以外的空间清理掉。
分代回收算法
Java采用分代回收算法。将内存划分为新生代和老年代,新生代对象存活率低,老年代对象存活率高,新生代采用复制算法,老年代采用标记-整理算法。
4. 垃圾回收器
并行:指的是多条垃圾回收线程并行工作,而用户线程等待
并发:指的是用户线程与垃圾回收线程同时执行(不一定并行,可能会交替执行),用户线程继续执行,垃圾回收线程运行在另外的内核上
吞吐量:CPU运行用户代码时间/CPU总时间(用户代码时间 + 垃圾回收时间)
新生代垃圾回收器:Serial、ParNew、Parallel Scavenge
老年代垃圾回收器:Serial Old、Parallel Old、CMS
全区域垃圾回收器:G1
Serial收集器(新生代垃圾回收,串行收集器)
a. 特性:Serial是一个单线程收集器。在Serial收集器进行垃圾回收时,必须暂停其他所有工作线程,直到Serial收集器收集结束。(STW:stop the world 用户线程等待,垃圾回收线程在工作)
b. 应用场景:Serial是JVM运行在Client模式下默认新生代收集器
c. 优点:简单而高效。对于单核CPU来说,Serial收集器由于没有线程交互开销,可以获得最高的单线程收集效率。
ParNew收集器(新生代收集器,并行GC)
a. 特性:ParNew收集器就是Serial收集器的多线程版本
b. 应用场景:ParNew是许多运行在Server模式下JVM首选新生代收集器
c. 优点:随着可使用CPU数量(>2)的增加,对于GC时系统资源的利用有较大帮助
parallel Scavenge收集器(新生代收集器,并行GC)
a. 特性:更关注吞吐量
有两个参数控制吞吐量:
1)-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
2)-XX:GCRatio:直接设置吞吐量大小
b. 应用场景:高吞吐量场景,适合需要与用户交互的程序(B/S架构),良好的响应速度能提升用户体验
c. 优点:与ParNew收集器比较:高吞吐量、自适应调节策略(-XX:UseAdaptiveSizePolicy)
Serial Old收集器(老年代收集器,串行GC)
a. 特性:Serial Old是Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。
b. 应用场景:
1)Client模式:Serial Old主要也在于给Client模式下虚拟机使用
2)Server模式:作为CMS收集器的后备预案,当CMS发生并发失败问题时使用
Parallel Old收集器(老年代收集器,并行GC)
a. 特性:Parallel Old是Parallel Scavenge老年代版本
b. 应用场景:注重吞吐量以及CPU资源敏感场合
CMS收集器(老年代垃圾收集器,并发GC)
a. 特性:CMS收集器是以获取最短系统停顿时间为目标的垃圾回收器(标记-清除算法)
b. 应用场景:B/S系统服务端。B/S系统重视服务的响应速度,希望系统停顿时间尽可能短,CMS收集器非常符合此类应用需求。
c. 优点:并发收集、低停顿;缺点:对CPU资源敏感、产生大量空间碎片
G1(全区域垃圾回收器)
a. 特性:在堆区域很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收
b. 应用场景:应用低停顿场景
5. 内存分配策略
对象优先在Eden区分配
大多数情况下对象优先在新生代的Eden区分配,当Eden区没有足够空间记性对象分配时,JVM会发生Minor GC。
大对象直接进入老年代
大对象指的是需要大量连续空间的对象
-XX:PretenureSizeThreshold 字节大小
JVM会将大小 >PretenureSizeThreshold 的对象直接放入老年代
长期存活的对象进入老年代
JVM给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden区出生,并经历一次Minor GC后仍然存活并且能被Survivor空间容纳的话,把此对象年龄置为1.每当对象在Survivor空间经历一次Minor GC,年龄就增加一岁,当对象年龄增加到一定程度(默认15),就将晋升到老年代中。
-XX:MaxTenuringThreshold:设置晋升老年代年龄阈值
-XX:+PrintTenuringDistribution:打印年龄
动态对象年龄判定
JVM并不永远要求对象年龄必须达到PretenureSizeThreshold才能晋升到老年代。
如果在Survivor空间中相同年龄所有对象大小总和 >= Survivor空间的一半,年龄 >= age的对象直接直接进入老年代,无需等到MaxTenuringThreshold要求的年龄。
-XX:+PrintGCDeatils:打印GC详细日志
-XX:+UseSerialGC:使用Serial+Serial Old组合
-Xms20M -Xmx20M:设置堆大小
-Xmn10M:设置新生代大小
-XX:SurvivorRatio=8(Eden:Survivor = 8 : 1):设置Eden区和Survivor区比例
-XX:PretenureSizeThreshold=??:大于该值的对象直接进入老年代
空间分配担保
在发生Minor GC之前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象总空间:
- 如果大于,认为此次Minor GC是安全的(担保成功)。
- 如果小于,则JVM检查HandlePromotionFailure设置值是否允许担保失败。
HandlePromotionFailure = true(允许担保失败),则JVM会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象平均大小,大于,则尝试进行Minor GC;如果小于或者HandlePromotionFailure = false,则改为进行Full GC。