前言
本节首要环绕下面三个方向进行知识点的解说,带你从架构师角度认识内存溢出
虚拟机中目标的创建过程
当 JVM 遇到一条字节码 new 指令的时分,它首先会进行:
检查加载
检查对应的类有没有加载进来,假如没有加载进来,则要从头进行类加载,直到类加载成功,成功之后继续进行检查加载,详细检查什么呢?
Object obj = new Object();
例如上面这段代码,它会检查经过设置的 new 的参数(new Object)是否能在办法区的常量池中找到这个类的符号引证,
什么是类的符号引证?
用一组符号来描绘你所引证的目标,例如 NBA 球员 James,这个 James 便是一个符号引证,假定 James 不来中国,那么你是看不到他的,你只看到了这个符号;那么关于类来说,这个类的前面加了 com.american.James,一起它要检查这个 James 类有没有被加载过;
分配内存
检查加载成功之后,就要开始分配内存,那么 JVM 是怎么区分内存的呢?目标请求内存空间流程是怎样的呢?
区分内存有两种办法,一种是指针磕碰、一种是空闲列表
指针磕碰
假定咱们的堆内存比较规整,赤色代表已经分配了内存,白色代表未分配的内存,这个堆内存比较规整,咱们能够运用一个指针指向堆内存中的最终一个目标的偏移量,当咱们给一个目标请求内存空间的时分,这个指针就会依据这个目标的 size 挪动到指定的方位来放下创建的这个目标,这个就叫做指针磕碰(移动一个目标巨细的间隔);别的这种指针磕碰只能在堆空间比较规整的情况下,可是经过废物收回之后,就会变成零散的,不规整的,那么指针磕碰在这种情况下就不适宜了,这种时分,JVM 就会保护一个空闲列表;
空闲列表
JVM 用空闲列表来符号对应的方位是否有目标存在,在分配方位的时分,假定目标需求一个方位巨细,就分配到 1 的方位,假如需求三个巨细,就分配到 3-5 这个方位;
上面两种区分办法都是由于:在 JVM 中目标要占有的内存必定要是连续的;
JVM 用哪种办法来区分,取决于堆的规整程度;堆空间的规整度 又是由废物收回器决议的,废物收回器是否带有整理功用;
不管运用 指针磕碰 仍是 空闲列表,JVM 为了提高功率,同样运用了多线程,那么就会带来多线程安全问题,那么 JVM 是怎么解决并发安全的问题呢?
CAS加失利重试
A B 两个分配内存的时分,都会去抢同一块内存,会进行查询操作,查询下这块空间是不是空的,A B 两个线程拿到的都是空的,就会进行 CAS 操作,由于 CAS 操作是由 CPU 确保线程的履行次序,假定 A 比 B 先履行,当 A 进行 CAS 操作的时分,判别这块空间是空的,就进行交流占有这块内存,当 B 进行 CAS 操作的时分,比较发现这块区域不空了,就会进行重试,直到找到一块为空的区域,然后进行交流操作;
CAS原理能够检查之前的解说:怎么应对Android面试官->CAS基本原理
本地线程分配缓冲
CAS 比较而且交流,比较和交流,必定消耗功能,所以 JVM 供给了第二种办法:本地线程分配缓冲(Thread Local Allocation Buffer)简称 TLAB;
本地线程分配缓冲类似 ThreadLocal,堆中的 eden 预先给每个线程区分单独的一块区域,当线程履行的时分,直接分配,就不需求采取安全措施,这便是本地线程分配缓冲,可是 TLAB 比较小,只占用 eden 区的 1%;
什么时分 CAS,什么时分分配缓冲?
分配缓冲默认开启,假如要禁用,能够运用下面的装备选项
-XX:-UseTLAB
内存空间初始化
内存空间的初始化不是结构办法,而是在内存分配之后,区分了一块区域,可是这块区域是空的,需求把里边的一些数据设置为 **零值,**这一步确保了目标在分配完内存后,在代码里边不需求赋值就能够直接运用,程序越早运用目标,它的功率就越高,这便是内存空间初始化;
什么是零值?
比方 int 类型,那么它的零值便是 0,boolean 类型,它的零值便是 false,
设置
目标归于哪个实例,需求设置一下,以及设置目标头;
目标的初始化
调用结构办法进行目标的初始化;
以上过程,针对的是 一般的目标(也便是咱们编写的程序),由于 Java 中万物介目标;
虚拟机中目标的布局
HotSpot 中目标能够分为三块:目标头、实际数据、对齐补充
目标头
Mark Word(存储目标本身的运转时数据)
哈希码、GC分代年纪、锁状态符号、线程持有的锁、倾向线程ID、倾向时间戳;
类型指针
指向类目标信息的指针;
Person p = new Person();
p 是一个引证目标,存在栈中(java虚拟机栈中的栈桢),new Person() 存在于堆中, 假定有一个 A 类,A 类中有一个 Person 目标,这个目标存储于堆区,那么它就会指向办法区的这个 A类(办法区存储类的描绘信息),指向的过程便是这个 Class Pointer;
若为目标数组,还应有记录数组长度的数据
lenght 数据长度,只针对数组目标;
实例数据
包括目标一切成员变量,依据变量类型决议巨细;
对齐填充
为了让目标的巨细为8字节的整数倍;
为什么要对齐填充?
由于在 HotSpot 中,它对管理的目标的巨细是有要求的,有必要是 8 字节的整数,可是 目标头和实例数据是没有办法控制的,假定目标头 + 实例数据刚好 38 字节,那么对齐填充就会填充 2 个字节,假如目标头和实例数据加起来刚好是 8 的整数倍,那么这个对齐填充就不需求了;填充的话随便用一些值填充就能够了;
虚拟机中目标的拜访定位
一切的虚拟机中(包括 HosSpot)目标的拜访定位都有两种办法:运用句柄、直接指针
运用句柄
什么是句柄?
句柄便是在堆空间划一块区域,叫作句柄池,句柄池中寄存的是什么呢?目标的拜访经过 reference ,这个 reference 中就不会寄存目标的地址了,而是寄存一个叫作目标实例的指针,句柄其实便是做了一次中转,经过句柄池找到真实的目标实例数据,这样做的好处便是:假如目标进行了移动,句柄池不需求修正,仍是能够经过句柄池找到对应的目标实例;
例如:句柄池中寄存的是 Kobe 的目标实例指针,实例池中寄存的是 Kobe 的实例,可是 Kobe 离开之后,句柄池的这个指针不需求修正,当有一个新的 Kobe 实例被替换的时分,仍是能够经过这个指针找到对应的 Kobe 实例;
可是这样做的害处是需求经过 Auth 查找;经过这个句柄池再映射一次,会有一次额外的指针定位开销,尽管这个开销比较小,可是 JVM 中目标的创建是比较疯狂的,这块会存在一个积少成多,那么虚拟机又供给了别的一种办法:直接指针;
直接指针
Person p = new Person();
在 HotSpot 中 运用的便是直接指针,这个 p 便是一个引证,这个引证就会指向真实的地址;这样做尽管带来了功率的提高,可是假如目标一直被移来移去,目标在物理区 移来移去,那么这个 reference 就会进行改变;
怎么判别目标的存活
在 JVM 中目标是能够被收回的,首先目标是在堆中进行分配的,假如堆空间满了,就会触发废物收回,可是在进行废物收回之前咱们要确认哪些目标是存活的;怎么判别呢?大部分都是选用的下面这两种办法
引证计数法
用一个计数器来统计目标被引证,目标被引证了,计数器就+1,假如这个引证失效了,就 -1,假如等于 0 阐明这个目标不被引证了;
这里会存在一个问题:目标的彼此引证;
上图中的两个目标就存在彼此引证,可是又跟运转办法里边的不相关,外部没有可用的地方与它进行连接,它其实也是死的;
可达性剖析(根可达)
JVM 中用的便是可达性剖析法,本质上是依据一条链路来追寻的,这条链路以 GC Roots 的变量(静态变量、线程栈变量、常量池变量、JNI指针变量)或许目标(class、Exception、OOM、类加载器、加锁 synchronized 目标、JMXBean、临时性)为根节点,构成引证链路的则为存活目标,没有被 GC Roots 直接引证或许直接引证的都是能够收回的目标;
经过下面的代码能够验证 HotSpot 运用的是可达性剖析法
// -XX:+PrintGC
public class ReliabilityAnalysisTest {
public Object instance = null;
// 辅助效果,占有内存,用来可达性剖析
private byte[] bigSize = new byte[10 * 1024 * 1024];
public static void main(String[] args) {
ReliabilityAnalysisTest test = new ReliabilityAnalysisTest();
ReliabilityAnalysisTest test1 = new ReliabilityAnalysisTest();
// 彼此引证
test.instance = test1;
test1.instance = test;
// 免除引证
test.instance = null;
test1.instance = null;
// 收回内存
System.gc();
}
}
VM参数加上注释的装备信息-XX:+PrintGC 运转之后能够看到,内存进行了收回,阐明引证计数法的办法在 HotSpot 中没有被运用;假如是可达性剖析的话,这两个目标必然不会被收回;
可达性剖析算法之后,没有引证链,可是相互引证的目标,也不是立马就会被收回,它们其实处于缓刑状态,仍是能够被抢救的,可是这个抢救是需求开发者经过代码完成的,可是 finalize 只能履行一次,能够看下面的代码示例;
public class FinalizeTest {
public static FinalizeTest instance;
public void isAlive() {
System.out.println("is Alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize execute");
FinalizeTest.instance = this;
}
public static void main(String[] args) throws InterruptedException {
instance = new FinalizeTest();
// 第一次 GC
instance = null;
System.gc();
Thread.sleep(1000); // 等待 finalize 办法履行
if (instance != null) {
System.out.println("第一次GC后,目标实例不为空");
instance.isAlive();
} else {
System.out.println("第一次GC后,目标实例为空");
}
// 第2次 GC
instance = null;
System.gc();
Thread.sleep(1000); // 等待 finalize 办法履行
if (instance != null) {
System.out.println("第2次GC后,目标实例不为空");
instance.isAlive();
} else {
System.out.println("第2次GC后,目标实例为空");
}
}
}
能够看到第一次 GC 后,履行了 finalize 办法,进行了解救,可是第2次 GC 之后,就被收回了;
这里为什么要加 sleep,是由于 finalize 的线程优先级十分低,假如去掉 sleep 则解救不成功;
能够看到第一次 GC 之后就被收回了;
所以 finalize 尽量不要运用,这个办法太不可靠了;
JVM中的引证类型
强引证
Object obj = new Object();
这种便是强引证,只需 GC Roots 还在,那么强引证的就不会被收回;
软引证
内存不足,将要产生OOM的时分,会被收回;能够检查下面的代码示例
// -Xms20M -Xmx20
Mpublic class SoftReferencesTest {
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
public static void main(String[] args) {
User user = new User("张三", 18);
SoftReference<User> softReference = new SoftReference<>(user);
System.out.println("GC 前读取:" + softReference.get());
user = null; // 置空,确保只要软引证指向该目标
System.gc(); // 手动触发GC
System.out.println("GC 后读取:" + softReference.get());
// 结构内存溢出
List<byte[]> list = new ArrayList<>();
try {
for (int i = 0; i < 10000; i++) {
list.add(new byte[1024 * 1024]);
}
} catch (Throwable e) {
System.out.println("内存溢出:" + softReference.get());
}
}
}
能够看到,当产生内存溢出的时分,被收回掉了,这个时分咱们获取弱引证中的数据是拿不到的;
弱引证
GC 扫描到了就会收回;
public class WeakReferencesTest {
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
public static void main(String[] args) {
User user = new User("张三", 18);
WeakReference<User> weakReference = new WeakReference(user);
System.out.println("GC 前读取:" + weakReference.get());
user = null; // 置空,确保只要软引证指向该目标
System.gc(); // 手动触发GC
System.out.println("GC 后读取:" + weakReference.get());
}
}
能够看到 GC 的时分,就被收回掉了;
虚引证
随时都会被收回,不知道什么时分就被收回了;首要用来监控废物收回器是否正常工作,一般业务开发中用不到;
目标请求内存空间流程
目标的分配准则
- 目标优先在Eden分配;
- 空间分配担保;
- 大目标直接进入老时代;
- 长时间存活的目标进入老时代;
- 动态目标年纪判定;
目标分配时的优化技能
当咱们 new 一个目标的时分,JVM 的第一个优化便是:是否栈上分配?
一般咱们总是说:几乎一切目标都是堆中分配,但不是 100%,它也能够在栈上分配,而且在栈上分配的目标,就不需求废物收回,这也是为什么办法要在栈中履行的原因,功率高,栈的内存是跟从线程的,线程履行完了,这个栈也就完毕了;
假如想在栈上分配目标,HotSpot 需求一项技能:逃逸剖析技能;
逃逸剖析:判别办法的目标有没有逃逸,便是剖析这个目标的效果域
- 是不是能够逃逸出办法体;
- 是不是能够逃逸出其他线程;
能够看下面代码示例
// -XX:+PrintGC
// -XX:-DoEscapeAnalysis
public class EscapedAnalysisTest {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 6_000_000_0; i++) {
allocate();
}
System.out.println("Escaped Analysis: " + (System.currentTimeMillis() - start));
Thread.sleep(60_000);
}
static void allocate() {
Person person = new Person(1000L, 2000L);
}
static class Person {
private long age;
private long height;
public Person(long age, long height) {
this.age = age;
this.height = height;
}
}
}
Person person = new Person(1000L, 2000L);
person 就会被分配到栈上,它满意 不会逃逸出办法体(办法外没有调用),也不会逃逸出其他线程(只要一个main线程);
-XX:-DoEscapeAnalysis // 关闭逃逸剖析
假如不想运用栈上分配(不做逃逸剖析)的运转结果,能够加上上面的装备信息;履行结果如下
能够看到触发了 GC;
JVM的第二个优化便是:堆中本地线程分配缓冲,
上面已经介绍了;
目标优先在 Eden 区分配
假如不支持 本地线程分配缓冲,会判别是不是大目标,假如不是大目标,则在 Eden 区分配,满意了目标优先在 Eden 区分配的准则之一,假如是大目标,则直接分配到老时代(满意了大目标直接进入老时代准则之一)
大目标:一般是很长很长的字符串、数组;
假如咱们经过参数 -Xms30M -Xmx30M 来设置咱们的JVM 堆区为 30M,那么老时代就会分配20M,Eden区分配 8M,From区分配 1M, To区分配 1M;
也便是说重生代只占堆内存的三分之一,所以说大目标放到老时代能够防止废物收回;
JVM 能够经过参数设置是否为大目标,-XX:+PretenureSizeThresold10M,大于等于10M的目标则以为是大目标,直接分配到老时代;
Eden 上分配之后还会遵从一个准则:长时间存活的目标进入老时代;
当触发废物收回的时分,由于 Eden 区只寄存重生目标,Eden 中一切存活的目标都将被移动到 From 区,目标的目标头中的 age(Mark Word 区域的 GC分代年纪) 就会 +1,然后 Eden 被清空,Eden 被清空,当再次充溢的时分,一切存活的目标和 From survivor 中一切存活的目标都被移动到 To survivor,然后 Eden 和 From survivor 被清空,这个时分 To 中的目标的目标头中的 age 会再次 +1 (=2),当再次充溢触发废物收回的时分,会把存活的目标和 Tosurvivor中一切存活的目标都被移动到 Fromsurvivor,然后 Eden 和 To survivor 被清空,这个时分 From 中的目标的目标头中的 age 会再次 +1(=3),From 和 To 循环往复,当 age = 15 的时分,会被移动到老时代(满意了长时间存活目标进入老时代准则之一),这种循环往复选用的便是 仿制收回 算法;
JVM 为什么不把 Eden、From、To 合并成两个,只保存 From和 To呢?
这是由于仿制收回算法要浪费一半的空间,为什么要浪费一半呢?如果仿制曩昔的满是存活目标,比方从 From 仿制到 To 的都是存活目标,可是 To 中没有满足的空间包容下这些目标了;所以往往仿制算法的空间都是一分为二,导致内存利用率只要50%;Oracle 和 Sun 公司做过大数据统计,90% 的目标在被废物收回的时分都能收回掉,只剩 10% 的存活目标,这 10% 的存活目标放入 From 区,那么就需求一个对等的 To 区,所以选用这样的一种办法的废物收回,那么浪费的只要 10% 的空间,空间利用率能够到达 90%;所以就没必要选用规范的仿制收回,把堆区一分为二,而是分红三份区域,第一次废物收回的时分,移动到 From 区,后续选用规范的仿制收回算法,从 From 仿制到 To 区;
JVM 的仿制收回算法为什么是 15 次,才会移动到老时代,能够修正这个值吗?
JDK 供给的 markOop.hpp(也便是 Mark Word) 文件中有提及到:
不管是 32 位的虚拟机仍是 64 位的虚拟机,这个 age 都是寄存的 4 位,从二进制来看寄存的最大值便是 1111,按照十六进制转化,便是 15,所以说仿制收回 age 的最大次数默认是 15 次;
JVM 也供给修正参数,能够修正这个值:
-XX:MaxTenuringThresold = 10 // 就能够修正这个值;
进入老时代的目标,age 就不会在被符号 +1;
废物收回的两个概念
在进行废物收回的时分,它其实是有两个概念的,在进行分代的时分,它能够选用两种 GC,废物收回器收回重生代称之为 Minor GC,收回老时代称之为 Major GC;
空间分配担保
经过堆中的目标分配准则,目标在分配的时分有 Eden 区 进入 From 区或许 To 区,最终进入 Tenured 区,大部分情况下老时代的目标都是由重生代晋级来的,可是假定老时代就只剩余 1M 的空间了,然后还有从 From 或许 To 区做一个目标的晋级,或许经过大目标分配,可是在进行目标晋级或许大目标分配,不能确保必定会有满足的空间来寄存,所以在每一次晋级或许大目标分配的时分,本身要做一次Major GC,这种办法比较安全,可是 JVM 以为这种很影响功率,所以 JVM 就提出了一个概念叫作:空间分配担保,这个担保由 JVM 来担保,放心分配,假如的确不够了,再进行一次Major GC,而不必每次晋级都要触发,这就满意了目标分配空间分配担保准则之一;
动态年纪判别
为了优化 From 区和 To 区,由于这两个区域本身也不大,假定 From 区中有三个目标,这三个目标的年纪加起来仅仅是5,可是这三个目标占有了 From 区的一半,那么这个时分它会走一个动态年纪判别,并不必定非要到达15,就会让这个几个目标提前晋级到老时代,这些目标就不需求等到15之后再进入老时代;
整体请求内存空间流程
-
先去 eden 区看看是否有满足的空间;
-
有,直接分配
-
无,JVM 开始收回废物目标,收回完成之后,判别 eden 是否有满足空间;
-
有,直接分配;
-
无,s 区域是否有满足空间;
-
有,eden 区的存活目标移动到 s 区,新目标就能够在 eden 请求成功;
-
无,启用担保机制,old 区是否满足空间;
-
有,将s区的存活目标移动到 old 区,eden将存活目标放到s区,请求成功;
-
无,JVM 触发 full gc,gc 之后检查 old 区是否有满足空间;
-
有,将s区的存活目标移动到old区,eden将存活目标放到s区,请求成功;
-
无,OOM;
简历润色
简历上可写:深度了解JVM内存分配原理,能基于分配原理进行深度优化;
下一章预告
带你玩转废物收回;
欢迎三连
来都来了,点个赞,点个重视吧~~~