1、JVM入门
1. JVM分区
Java内存模型(JMM)
Java内存结构(JVM虚拟机存储空间)
class文件——>ClassLoader
方法区:static修饰、常量信息,所有线程共享,存在线程安全
堆:创建对象、new创建数组、调优策略,所有线程共享
栈:定义局部变量、代码运行完毕自动释放内存,线程私有
2. 堆内存结构
分区的原因:根据使用的频率来进行使用垃圾回收
堆:存放new的对象
堆分为两个区:新生代、老年代
新生代:eden\s0\s1
eden:存放刚创建的对象(垃圾回收的主要)
s0/s1:与s1大小相同
老年代:存放频繁使用的对象
3. 参数配置
堆的初始值与堆内存的最大值保持一致(减少垃圾回收次数)
获取最大堆内存配置:
Runtime.getRuntime().maxMemory()
当前可用内存:
Runtime.getRuntime().freeMemory()
已使用内存:
Runtime.getRuntime().totalMemory()
垃圾回收的次数由初始值的大小相关,初始值越小回收越频繁
让出空闲来使用
4. 提高新生代的回收频率
设置新生代与老年代为:1/3或1/4
2、堆结构
1. JVM中的堆结构
- 垃圾回收的特点:使用的内存空间快占所分配的较大时
2. 设置新生代与老年代比例
提高新生代的回收次数
通常设置新生代与老年代比例:1:3或1:4
2.1、jvm参数说明
-Xms10m -Xmx10m -Xmn1m -XX:SurvivorRatio=2
-XX:+PrintGCDetails -XX:+UseSerialGC
说明:
-XX:+PrintGC 每次触发GC的时候打印相关日志
-XX:+UseSerialGC 串行回收
-XX:+PrintGCDetails 更详细的GC日志
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值
-XX:SurvivorRatio 用来设置新生代中eden空间和from或to空间的比例.
如例:-XX:SurvivorRatio=2,代表eden区是from或to的2倍
2.2、配置新生代与老年代比例
-Xms20m -Xmx20m -XX:NewRatio=2 -XX:SurvivorRatio=2
-XX:+PrintGCDetails -XX:+UseSerialGC
-XX:NewRatio 老年代与新生代比例,2代表老年代是新生代2倍
通常设置为3或4
public static void main(String[] args){
// 占用10M内存
byte[] b = null;
for (int i=0;i<10;i++){
b= new byte[1*1024*1024];
}
}
3、堆溢出
2. 堆内存配置
2.1 堆内存较小时
public static void main(String[] args){
List<Object> objectList =new ArrayList<>();
for(int i=0;i<10;i++){
System.out.println("i:"+i);
objectList.add(new byte[1*1024*1024]);
}
System.out.println("创建完成");
}
设置堆大小为10M
-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
出现堆溢出情况
2.2 调整堆大小
设置堆大小为30M
-Xms10m -Xmx30m -XX:+HeapDumpOnOutOfMemoryError
正常运行
2.3、服务器端堆溢出
在catalina.sh配置:
JAVA_OPTS=’'server -Xms800m -Xmx800m -XX:Persize=256m
-XX:MaxPersize=512m -XX:MaxNewsize=512m
4、栈溢出
1. 栈溢出原因
方法无限递归调用导致
方法的循环调用不会产生栈溢出
2、栈溢出示例
2.1 方法递归调用
public class Test{
private static int count = 0;
public static void getCount(){
try {
count ++;
getCount();
}catch (Throwable e){
System.out.println("最大深度:"+ count);
e.printStackTrace();
}
}
public static void main(String[] args){
getCount();
}
}
- 显示错误如下,栈的最大深度为15647667
2.2 方法循环调用
public class Test{
private static int count = 0;
public static void getCount(){
try {
count ++;
}catch (Throwable e){
System.out.println("最大深度:"+ count);
e.printStackTrace();
}
}
public static void main(String[] args){
for(int i=0 ;i<10000000;i++){
getCount();
}
}
}
方法循环调用不会产生栈溢出
3. 栈溢出的解决
设置栈大小5M:-Xss5m
5、垃圾收集器
1. 垃圾收集器分类
- 串行收集器
单线程,效率低
- 并行收集器
多线程,效率高,常用的方案
- 其他
CMS、G1垃圾收集器
2. 压力测试
- 吞吐量:每秒成功次数
1. 串行垃圾收集器测试
开发环境:设置虚拟机参数
生成环境:配置tomcat
基础测试:
-XX:+PrintGCDetails -Xmx32M -Xms32M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseSerialGC
-XX:PermSize=32M
扩大内存测试:
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseSerialGC
-XX:PermSize=32M
结论:
内存加大,吞吐量加大
-Xms越大垃圾回收的次数越少
-Xms不能大于-Xmx
2. 并行垃圾收集器测试
参数配置
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseParNewGC
-XX:PermSize=32M
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseParallelGC
-XX:+UseParallelOldGC
## 通常线程数=核数*2
-XX:+UseParallelThreads=8
-XX:PermSize=32M
6、内存溢出与内存泄漏
1、内存溢出与内存泄漏
1.1、区别
- 内存溢出:
超出分配的内存大小
需要4g资源,仅有3g内存
- 内存泄漏:
定义很多静态变量,存放在永久区
不能被回收
2、垃圾回收算法
1.1、引用计数法
计数:默认引用15次数
被引用的次数为0时,可以被回收
不被引用时减去1,引用时加1
大于15进入s0或s1
继续加进入老年区
- 循环引用问题
1.2、复制算法
新生代的s0、s1
先创建对象user,user晋升到s0
只后创建user1,user1晋升到s0
s0、s1大小相等,每次一个存放存活对象
一个放要回收的对象
在s1与s0交替回收
不会产生碎片,比较占空间
1.3、标记清除法
缺点: 碎片化
0可达,1不可达
1.4、标记压缩法
不可达对象压缩成块清理
0可达,1不可达
1.5、分代算法
新生代多、老年代少
回收的区域进行压缩到一起清除
计数法:edn区
复制法:s0、s1
老年代: 标记压缩
3. 减少垃圾回收
进行垃圾回收时,会暂停其他线程
影响程序的执行效率
7、垃圾回收机制
垃圾回收机制(GC)
1、垃圾回收算法
- 引用计数法
- 标记清除:
- 标记压缩:老年代
- 复制算法:新生代s0、s1
- 分带算法: 新生代、老年代
2、垃圾回收机制
不定时,清理堆内存不可达对象
public class Test{
public static void main(String[] args){
// 初始化堆,设置小有利于回收
Test test = new Test();
// 指向空引用有利于回收
test = null;
// 手动回收,只起通知作用
System.gc();
}
@Override
protected void finalize() throws Throwable{
//gc 回收垃圾前调用
System.out.println("垃圾回收机制");
}
}
垃圾回收
1、理论
- GC 的几种主要的收集方法:标记清除、标记整理、复制算法的原理与特点,各自的优劣势
- 为啥会有 Serial ,CMS, G1等各式样的回收器,各自的优劣势是什么,为啥没有一个统一的万能的垃圾回收器
- 新生代为啥要设置成 Eden, S0,S1 这三个区,基于什么考虑呢
- 堆外内存不受 GC 控制,那该怎么释放呢
- 对象可回收,就一定会被回收吗?
- 什么是 SafePoint,什么是Stop The World
2、实战
- GC 日志格式怎么看
- 主要有哪些发生 OOM 的场景
- 发生 OOM,如何定位,常用的内存调试工具有哪些
3、几方面来阐述垃圾回收
1、JVM 内存区域
2、如何识别垃圾
- 引用计数法
- 可达性算法
3、垃圾回收主要方法
- 标记清除法
- 复制法
- 标记整理法
- 分代收集算法
4、垃圾回收器对比
详细
JVM 内存区域
-
虚拟机栈:描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈桢(下文会看到),主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈出栈的时机很明确,所以这块区域不需要进行 GC。
-
本地方法栈:与虚拟机栈功能非常类似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地方法时服务的。这块区域也不需要进行 GC
-
程序计数器:线程独有的, 可以把它看作是当前线程执行的字节码的行号指示器,比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容
记录这些数字(指令地址)有啥用呢,我们知道 Java 虚拟机的多线程是通过线程轮流切换并分配处理器的时间来完成的,在任何一个时刻,一个处理器只会执行一个线程,如果这个线程被分配的时间片执行完了(线程被挂起),处理器会切换到另外一个线程执行,当下次轮到执行被挂起的线程(唤醒线程)时,怎么知道上次执行到哪了呢,通过记录在程序计数器中的行号指示器即可知道,所以程序计数器的主要作用是记录线程运行时的状态,方便线程被唤醒时能从上一次被挂起时的状态继续执行,需要注意的是,程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域,所以这块区域也不需要进行 GC
-
本地内存:线程共享区域,Java 8 中,本地内存,也是我们通常说的堆外内存,包含元空间和直接内存,注意到上图中 Java 8 和 Java 8 之前的 JVM 内存区域的区别了吗,在 Java 8 之前有个永久代的概念,实际上指的是 HotSpot 虚拟机上的永久代,它用永久代实现了 JVM 规范定义的方法区功能,主要存储类的信息,常量,静态变量,即时编译器编译后代码等,这部分由于是在堆中实现的,受 GC 的管理,不过由于永久代有 -XX:MaxPermSize 的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM,有人说可以把永久代设置得足够大,但很难确定一个合适的大小,受类数量,常量数量的多少影响很大。所以在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也因此提升了性能(发生 GC 会发生 Stop The Word,造成性能受到一定影响,后文会提到),也就不存在由于永久代限制大小而导致的 OOM 异常了(假设总内存1G,JVM 被分配内存 100M, 理论上元空间可以分配 2G-100M = 1.9G,空间大小足够),也方便在元空间中统一管理。综上所述,在 Java 8 以后这一区域也不需要进行 GC
-
堆:前面几块数据区域都不进行 GC,那只剩下堆了,是的,这里是 GC 发生的区域!对象实例和数组都是在堆上分配的,GC 也主要对这两类数据进行回收,这块也是我们之后重点需要分析的区域
如何识别垃圾
引用计数法
最容易想到的一种方式是引用计数法,啥叫引用计数法,简单地说,就是对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收
String ref = new String("Java");
它无法解决一个主要的问题:循环引用!啥叫循环引用无法回收,也正是由于无法解决循环引用的问题,所以现代虚拟机都不用引用计数法来判断对象是否应该被回收。
可达性算法
现代虚拟机基本都是采用这种算法来判断对象是否存活,可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。。。(这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收
如图示,如果用可达性算法即可解决上述循环引用的问题,因为从GC Root 出发没有到达 a,b,所以 a,b 可回收
.
a, b 对象可回收,就一定会被回收吗?并不是,对象的 finalize 方法给了对象一次垂死挣扎的机会,当对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
注意: finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!
那么这些 GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢,有以下几类
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
如下代码所示,a 是栈帧中的本地变量,当 a = null 时,由于此时 a 充当了 GC Root 的作用,a 与原来指向的实例 new Test() 断开了连接,所以对象会被回收。
public class Test {
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
- 方法区中类静态属性引用的对象
如下代码所示,当栈帧中的本地变量 a = null 时,由于 a 原来指向的对象与 GC Root (变量 a) 断开了连接,所以 a 原来指向的对象会被回收,而由于我们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!
public class Test {
public static Test s;
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}
- 方法区中常量引用的对象
如下代码所示,常量 s 指向的对象并不会因为 a 指向的对象被回收而回收
public class Test {
public static final Test s = new Test();
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
垃圾回收主要方法
标记清除算法(碎片)
1、先根据可达性算法标记出相应的可回收对象(图中黄色部分
2、
操作起来确实很简单,也不用做移动数据的操作,那有啥问题呢?仔细看上图,没错,内存碎片!假如我们想在上图中的堆中分配一块需要连续内存占用 4M 或 5M 的区域,显然是会失败,怎么解决呢,如果能把上面未使用的 2M, 2M,1M 内存能连起来就能连成一片可用空间为 5M 的区域即可,怎么做呢?
复制算法(大小减半)
把堆等分成两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用以上所说的标记法把存活的对象标记出来(下图有误无需清除),然后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了。
不过复制算法的缺点很明显,比如给堆分配了 500M 内存,结果只有 250M 可用,空间平白无故减少了一半!这肯定是不能接受的!另外每次回收也要把存活对象移动到另一半,效率低下(我们可以想想删除数组元素再把非删除的元素往一端移,效率显然堪忧)
标记整理法(频繁移动)
前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列(如图示),再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。
但是缺点也很明显:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。
分代收集算法
分代收集算法根据对象存活周期的不同将堆分成新生代和老生代(Java8以前还有个永久代),默认比例为 1 : 2,新生代又分为 Eden 区, from Survivor 区(简称S0),to Survivor 区(简称 S1),三者的比例为 8: 1 : 1,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)。
分代收集工作原理
1、对象在新生代的分配与回收
大部分对象在短时间内都会被回收, 所以经过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区(这就是为啥空间大小 Eden: S0: S1 = 8:1:1, Eden 区远大于 S0,S1 的原因,因为在 Eden 区触发的 Minor GC 把大部对象(接近98%)都回收了,只留下少量存活的对象,此时把它们移到 S0 或 S1 绰绰有余)同时对象年龄加一(对象的年龄即发生 Minor GC 的次数),最后把 Eden 区对象全部清理以释放出空间,
.
若再触发下一次 Minor GC,则重复上一步,只不过此时变成了 从 Eden,S1 区将存活对象复制到 S0 区,每次垃圾回收, S0, S1 角色互换,都是从 Eden ,S0(或S1) 将存活对象移动到 S1(或S0)。也就是说在 Eden 区的垃圾回收我们采用的是复制算法,因为在 Eden 区分配的对象大部分在 Minor GC 后都消亡了,只剩下极少部分存活对象(这也是为啥 Eden:S0:S1 默认为 8:1:1 的原因),S0,S1 区域也比较小,所以最大限度地降低了复制算法造成的对象频繁拷贝带来的开销。
2、对象何时晋升老年代
当对象的年龄达到了我们设定的阈值,则会从S0(或S1)晋升到老年代
年龄阈值设置为 15, 当发生下一次 Minor GC 时,S0 中有个对象年龄达到 15,达到我们的设定阈值,晋升到老年代!
- 大对象 当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代,因为如果把大对象分配在 Eden 区, Minor GC 后再移动到 S0,S1 会有很大的开销(对象比较大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,所以干脆就直接移到老年代.
- 还有一种情况也会让对象晋升到老年代,即在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。
3、空间分配担保
在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。
4、Stop The World
如果老年代满了,会触发 Full GC, Full GC 会同时回收新生代和老年代(即对整个堆进行GC),它会导致 Stop The World(简称 STW),造成挺大的性能开销。
什么是 STW ?所谓的 STW, 即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。
现在我们应该明白把新生代设置成 Eden, S0,S1区或者给对象设置年龄阈值或者默认把新生代与老年代的空间大小设置成 1:2 都是为了尽可能地避免对象过早地进入老年代,尽可能晚地触发 Full GC。想想新生代如果只设置 Eden 会发生什么,后果就是每经过一次 Minor GC,存活对象会过早地进入老年代,那么老年代很快就会装满,很快会触发 Full GC,而对象其实在经过两三次的 Minor GC 后大部分都会消亡,所以有了 S0,S1的缓冲,只有少数的对象会进入老年代,老年代大小也就不会这么快地增长,也就避免了过早地触发 Full GC。
垃圾收集器种类
在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge
在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old
同时在新老生代工作的垃圾回收器:G1
图片中的垃圾收集器如果存在连线,则代表它们之间可以配合使用,接下来我们来看看各个垃圾收集器的具体功能。
新生代收集器
- Serial 收集器: 单线程垃圾收集器
- ParNew 收集器( 并发CMS收集器配合工作): 停顿时间,ParNew 收集器是 Serial 收集器的多线程版本
- Parallel Scavenge :吞吐量
老年代收集器
- Serial Old 收集器: 老年代单线程垃圾回收器
- Parallel Old 收集器: 标记整理法
- CMS 收集器: 标记清除法
- G1(Garbage First) 收集器:标记-整理法