Fork me on GitHub

Java 内存区域与内存溢出异常

注意:所有文章除特别说明外,转载请注明出处.

[TOC]

Java 内存区域与内存溢出异常

1.运行时数据区域

线程共享:方法区,堆

线程私有:虚拟机栈,本地方法栈,程序计数器

1.程序计数器(线程私有)

可以看做是当前线程所执行的字节码的==行号指示器==。

==选取下一条需要执行的的字节码指令==,基础功能都需要依赖这个计数器来完成。

2.Java虚拟机栈(线程私有的)

**虚拟机栈描述的是Java方法执行的内存模型**

每一个方法在执行的时候都会创建一个栈帧,每一个方法从调用直至执行完成的过程,就对应着==一个栈帧在虚拟机栈中的出栈入栈的过程==。

局部变量表:

    其中存放了编译期可知的各种**基本数据类型**(八大基本数据类型),**对象引用**(它不同于对象本身,可能是一个指向对象**起始地址**的引用指针,也可能是指向一个**代表对象的句柄**或者其他与此对象**相关的位置信息**)和 **returnAddress类型**(指向了一条字节码指令的地址)

==其中64位长度的long 和 double类型会占用两个局部变量空间==其余只占一个

3.本地方法栈(线程私有)

本地方法栈是为虚拟机使用到的是Native方法服务。

4.Java堆(线程共享)

Java堆是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块区域,==在虚拟机启动时创建==

其唯一目的就是:==存放对象实例,几乎所有的对象实例都在这里分配内存==,所有的对象实例以及数组都要在堆上分配内存。

Java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。Java堆还可以细分为==新生代和老年代==

Java堆中还可以划分指出**多个线程私有的分配缓冲区**(TLAB)

Java堆可以存储在物理上不连续的内存空间之中

5.方法区(线程共享)

用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。

 用于存储已被虚拟机加载的类信息,常量,静态变量,

方法区可以选择不实现垃圾收集,他的**内存回收目标主要是针对常量池的回收和对类型的卸载!**

6.运行时常量(方法区的一部分)

用于**存放编译期生成的字面量和符号引用**,这部分内容在**类加载**后进入方法区的运行时常量池中存放。

**运行期间也可能将新的常量放入池中**, 例如:String 类中的intern()方法。

7.直接内存

 他可以使用Native函数库直接分配堆外内存。然后通过一个存储在Java堆中的DirectByteBuffer对象**作为这块内存的引用**来进行操作

这样做避免了在Java堆中和Native堆中**来回复制数据**

2.HotSpot虚拟机对象探秘:

==对象的创建==

1. 虚拟机再碰到一条 new 指令的时候,先去检查这个指令的参数在常量池中**能否定位到一个类的引用**,并检查这个类**是否被加载**,解析和初始化。如果**没有**那么先**执行相应的类加载**过程

2. 在类加载检查通过之后,那么虚拟机将为对象分配空间。

3. ==指针碰撞==:假设堆是绝对规整的,所有用过的内存放在一边,空闲的内存放在一边,**中间放着一个指针作为分界点的指示器**,那么分配对象仅仅就是将这个指针往空闲的那边挪动一段大小和对象相等的距离。这种分配方式称为指针碰撞。**往空闲的空间挪动称为指针碰撞**

4. ==空闲列表==:假设内存不是规整的,空闲区域和内存区间相互交错。此时虚拟机必须维护一个表,**记录那些内存块是可以使用的**,再分配的时候从列表中找出一段足够大的空间划分给对象实例,并更新列表上的记录。这种分配方式称为空闲列表。**在空闲的区间挑选一块**

5.  使用哪种分配方式**取决于垃圾收集器是否带有压缩整理**的功能。

如何解决创建对象是的线程安全问题

**方案一:**

    对分配内存空间的动作进行同步处理——实际上采用**CAS加上失败重试**的方式。

**方案二:**

    把内存分配的动作按照不同的线程划分在不同的空间上进行,即每个线程在Java堆中预先分配一小块内存,称为**本地线程分配缓冲(TLAB)**,那个线程需要分配内存,就在那个线程的TLAB上分配。**只有在TLAB用完并重新分配新的TLAB是才需要同步锁定。**

 6. 在内存分配完成之后,虚拟机要将分配到的内存空间都**初始化**为==零值==(保证了不赋初值可以直接使用)
 7. 给对象头赋值:虚拟机对对象进行必要的设置,那个类的,如何找到类的元数据信息,Hash码,对象的GC分代年龄等信息。==存储在对象头之中!!==
 8. 最后执行 init 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

对象的内存布局

对象在内存中存储的布局可以分为三块区域:**对象头,实例数据,和对齐填充。**

**对象头信息**:

    1)、用于存储对象自身的运行时数据。哈希吗,GC分代信息等。

    2)、类型指针,(==系统通过这个指针来确定是哪个类的实例==),如果是一个数组,那么在对象头中还会存储这个对象的数组长度。

**实例数据部分**:

    1)、是对象真正存储的有效信息。

    2)、无论是父类继承下来的还是子类中自己定义的都需要记录起来。

**对齐填充**:

    1)、对齐填充并不是必须存在的。也没有什么特别的含义

    2)、仅仅起着对齐的作用。(要求必须是8的倍数,如果实例数据不够8位,则会使用对齐填充来进行对齐补充)

对象的访问定位

前提:Java程序需要通过==栈上面的reference数据来操作堆==上面的具体对象

句柄访问:Java堆中将会划分出一块内存作为句柄池,句柄中包含了对象的实例数据与类型数据各自的具体位置信息。

直接指针:reference中存储的直接就是对象的地址。

好处:

**句柄访问**:使用句柄最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄之中的实例数据指针,而reference不需要改变。

**直接地址**:速度更快,节省了一次指针定位的时间开销(本书是使用直接地址的)

内存溢出异常(OutOfMeMoryError)

==除了程序计数器之外==别的其他几个运行时内存区域都有发生内存溢出的情况。
Java堆溢出
Java堆用于存储对象,只要**不断地创建对象,并且避免垃圾回收清除这些对象**,那么在对象数量达到**最大堆的容量限制**之后就会发生内存溢出异常。
虚拟机栈和本地方法栈溢出
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,则会抛出StackOverflowError异常

  • 如果虚拟机在扩展栈的时候无法申请到足够的内存空间,则会抛出OutOfMomeryError异常

    实验表明在单个线程之下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候,都会抛出StackOverflowError异常

    如果不断建立线程,倒是可以发生内存溢出异常,在这种情况下每个线程的栈分配的内存越大,反而更加容易产生内存溢出异常。

    如果是建立过多线程,从而出现的内存溢出异常,可以通过减少最大堆和栈的容量来换取更多的线程。

方法区和运行时常量池溢出
使用**intern**方法 无限放入常量。

产生大量的类去填满方法区
本机直接内存溢出

垃圾收集器和内存分配策略

前言:Java和C++之间存在一堵由==内存动态分配和垃圾收集技术==围成的高墙,墙外面的人想进来,墙里面的人却想出去。

3.1 概述

  • 那些内存需要回收?
  • 什么时候回收?
  • 如何回收?

3.2对象已死吗?

3.2.1引用计数算法

​ 概念:给对象添加一个引用计数器,每当有一个地方引用他的时候,计数器的值就加一,当引用失效的时候,计数器就减一,任何时候计数器为零的对象就是不可能再被使用的

3.2.2可达性分析法

​ 一句话,就是把对象挂在了GC Roots之上,如果没有挂上就回收。即使他们之间或许有关联,只要没有挂在GC Roots之上,就需要回收。

如图:对象 5和6 即使他们彼此有关联,但是没有挂在GC Roots之上,所以被判断为是可以回收的对象。

3.2.3再谈引用
3.2.4生存还是死亡

​ 可达性分析算法判断为可以回收的对象只是判了缓刑。

​ 两次标记过程:

​ 第一次:可达性分析法判断之后没有与GC Roots 相连接的引用链,那么将会进行第一次标记,并且进行一次筛选。如果这个对象被判定为需要执行finalize()方法,那么这个对象将会被放在 F - Queue的队列之中。

​ 第二次:GC 将会对F-Queue之中的对象进行第二次小规模的标记,还没有逃脱那么将会回收。

3.2.5回收方法区

​ 1)、回收废弃的常量

​ 例如:String

​ 2)、回收无用的类:

​ 满足如下三个条件:

  • 该类的所有实例都已经被回收了。
  • 加载该类的ClassLoader已经被回收
  • 改类的java.lang.class 对象没有在任何地方被引用,无法通过反射访问该类的方法

==3.3垃圾收集算法:==

3.3.1标记—清除算法

​ 首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。(先标记后清除

​ 两个不足:

​ 1)、效率不高,标记和清除两个过程的效率都不高。

​ 2)、空间问题,会产生大量的不连续的空间碎片。(再分配大对象时没有地方分配,提前触发GC)

3.3.2复制算法

​ 很简单:将内存分为大小相等的两块,每次只用一块,当这一块的内存用完了,直接把这一块上面存活的对象,复制到第二块之上。再把第一块直接清除掉。(可以避免内存碎片)

​ 代价:每次只是用一半,太高了。

​ 改良:因为新生代中的对象98%都是”朝生夕死“,所以可以将内存分为一块较大的Eden,和两块较小的Survivor空间。

​ Eden : Survivor = 8 : 1

​ 每次回收的时候,将Eden 和 一块Survivor 中还存活的对象,一次性的复制到另一块Survivor之中。这样只会浪费10%,但是无法保证每一次只有不多于10%的对象存活,因此还需要内存担保

3.3.3 标记—整理算法

​ 复制算法在对象存活率过高的时候,需要进行较多的复制操作,效率将会变得更低。

​ 更关键的是,万一100%的对象都存活了,怎么办?

​ 标记整理算法:

​ 先标记,在移动整理一下。就没有内存碎片了。

3.3.4分代收集算法

​ 新生代:每次都有大量的对象死去,只有少量存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

​ 老年代:对象存活率高,没有额外的空间进行空间担保,采用标记—清除,或者采用标记—整理算法。

3.4HotSpot的算法实现

  1. 通过可达性分析算法——是否有到达GC Roots的引用链来判断,对象是否可以被回收。

  2. 对象之间的引用在类的成员变量初始化以及类的方法中都会出现,如果逐个遍历,会消耗很多时间

  3. 使用一组OopMap来记录对象在栈中的引用地址,这样,HotSpot就可以快速找到GC Roots的对象集合。

  4. 另外,如果在进行判断分析的时候,有新的引用产生怎么办呢?

    ​ 这就要求在虚拟机执行垃圾收集的时候,需要将所有虚拟机暂停(“Stop the world”),以保持快照的一致性

    但是如果积攒了比较多的对象集中进行分析,那么这个暂停的时间就会比较长,一次收集的时间就会比较多。

    如果通过增加垃圾收集频次,减少每次垃圾收集分析工作量,那么垃圾收集占用总的时间也不少

  5. 安全点:由于为每一条指令都生成OopMap需要大量的空间,所以只再特定的位置记录这些信息,这些位置成为安全点。

  6. 长时间执行的的最明显特征是指令序列复用,方法调用啊,循环跳转,异常跳转等。

  7. 另外,虚拟机有两种中断方式。

    1. 抢先式中断:由虚拟机发起,所有线程全部中断,不在安全点上的线程,恢复运行至安全点上。

    2. 主动式中断:由线程去轮询是否中断的标志位,发现标识为真时,就自己将线程暂停挂起。

  8. 安全区域:专门用来处理当进行垃圾收集的时候,没有分配CPU时间的程序,比如线程处于Sleep状态Blocked状态,这些线程没办法响应JVM的暂停要求,对于这种状况,单独设置了一个安全区域

  9. 基本思路:

    1. 当线程执行到安全区域中的代码时,标识自己进入了安全区域。

    2. 当线程准备离开安全区域的时候,检查垃圾收集是否完成,如果结束了,线程继续执行;如果没结束,就等到结束之后再离开安全区域。

3.5垃圾收集器:

CMS(Concurrent Mark Sweep)

​ 并发标记清除收集器:以获取最短回收停顿时间为目标的收集器。

应用场景:希望系统停顿时间最短,已给用户较好的的体验。

运作过程:

  • 初始标记:仅仅是标记一下GC Roots 能直接关联的对象,速度很快
  • 并发标记:就是进行GC Roots Tracing(示踪)的过程
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除:

优点:==并发收集,低停顿==

原因:由于在整个过程中最耗时的并发标记和并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行

三大缺点:

1. CMS收集器对CPU资源非常敏感    在**并发阶段他会占用一部分线程**,从而**导致应用程序变慢**,**总的吞吐量会降低。**
2. CMS处理器无法处理浮动垃圾 伴随着程序运行,自然会产生一些**新的垃圾(在标记过后)**,CMS无法再本次收集中清除,这个称为浮动垃圾。
3. 标记清除算法的缺点。(内存碎片的浪费)
G1(G First)

​ G1 收集器是面向服务端应用的垃圾收集器。G1具备如下的特点:

  1. 并发和并行:G1能充分利用CPU、多核环境下的硬件优势使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  2. 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
  3. 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  4. 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内

运作过程:

​1、初始标记;2、并发标记;3、最终标记;4、筛选回收

​ 上面几个步骤的运作过程和CMS有很多相似之处。

​ 初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,

​ 并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。

​ 最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。

​ 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

第七章:类加载机制

类加载的过程

加载

  1. 通过一个类的全限定名来获取此类的二进制字节流
  2. 将静态存储结构转换为方法区的运行时数据结构
  3. 生成一个java.lang.class 对象,作为这个类的访问入口

验证

其实就是检查加载的 class 的正确性和安全性

准备

为类变量分配存储空间以及设置类变量初始值(附默认值)

解析

JVM 将常量池内的符号引用转换为直接引用

初始化

执行类变量的赋值 以及静态代码块

双亲委派模型

启动类加载器

​ 由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中

扩展类加载器

​ 负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。

应用程序类加载器

​ 负责加载用户类路径(classpath)上的指定类库

自定义类加载器

实现自己的类加载器
1. 继承ClassLOader这个类
2. 实现 findClass 这个方法
3. 在findClass 这个方法中调用 defineClass 这个方法即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName;

public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件
@Override
protected Class findClass(String name){
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
//用于加载类文件
private byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return out.toByteArray();
}
}

双亲委派模型的工作机制

如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

为什么需要双亲委派模型?

1. 保证了安全性。黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。!
2. **防止两份相同的字节码文件加入到内存中占用内存资源**

ClassLoader 和 Class.forName 的区别

Class.forName

Class.forName 会去加载并初始化这个类

其实他是调用了 forName 这个方法 里面的是否要初始化 为true 所以说他是完成了整个类加载过程

==所以静态代码块会被执行==

1
2
3
4
5
6
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

当我们在连接MySQL 数据库时 必须使用 Class.forName 才能完成对驱动的注册

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Driver extends NonRegisteringDriver implements java.sql.Driver {  

static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

ClassLoader

ClassLoader 只是完成了对这个类的加载过程,并没有进行初始化操作

在Spring IOC 中多见。用于懒加载,加快了初始化部署速度。留到实际使用是才去加载。

JVM 其他问题

可以作为GCroot的对象:

  1. 虚拟机栈中引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中的Native方法引用的对象

JVM参数:

1
2
3
4
-Xms
-Xmx
-Xss
...

JVM参数类型

标配参数:

​ java -version

​ java -help

X参数(了解):

​ java -Xint 解释执行

​ java -Xcomp 第一次使用就编译成本地代码

​ java -Xmixed 混合模式

XX参数:

Boolean 类型:

1
2
3
4
5
6
7
8
9
10
公式:
-XX: + 或者 - 某个属性值
+ : 表示开启
- : 表示关闭
case: 是否打印GC收集细节:
1. 运行程序
2. 添加JVM参数 -XX:+PrintGCDetails
3. jps -l 获取进程编号
4. jinfo -flag PrintGCDetails 进程编号
查看是否开启

KV设值类型:

1
2
3
4
5
公式:
-XX:属性key=属性值value
case
-XX:MetaspaceSize=128m (元空间大小)
-XX:MaxTenuringThreshold=15 (晋升老年代GC次数)

查看参数

一。正在运行的程序的参数

1
2
1. jps -l 获取进程id
2. jinfo -flags id

注意

​ -Xms:

​ 等价于 : -XX:InitialHeapSize

​ -Xmx:

​ 等价于: -XX:MaxHeapSize

二。初始参数:

java -XX:+PrintFlagsInitial

:= 人为修改了参数

= 初始参数

三:最终参数

java -XX:+PrintFlagsFinal

​ 运行java命令的同时打印出参数

四:查看究竟用那个GC收集器

​ java -XX:+PrintCommandLineFlags -version

JVM常用基本配置参数

-Xms 初始堆内存

-Xmx 最大堆内存

常用参数:

1
2
3
4
5
6
7
8
9
-Xms 初始堆大小内存
-Xmx 最大堆分配内存
-Xss 设置单个线程的大小 一般为 521K—1024K (0 代表 使用默认值)
等价于:-XX:ThreadStackSize
-Xmn 设置年轻代大小
-XX:MetaspaceSize 设置元空间大小
-XX:SurvivorRatio 设置新生代老年代的比例
1:-XX:SurvivorRatio=4 4:11 (默认 8:1:1
-XX:MaxTenuringThreshold 设置垃圾最大年龄 (必须 0-15之间 不然会报错)

GC日志信息:

四大引用:

强引用:

​ 当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算是==出现了OOM也不会对该对象进行回收==,死都不回收!

​ 强引用是我们最常见的普通对象引用。只要强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态。是不可能被垃圾回收机制回收的,==即使该对象也后都不会被用到,JVM也不会回收==,==因此强引用是造成Java内存泄露的主要原因之一==

​ 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者是显式的将相应的强引用赋值为null,一般就可以被垃圾收集了。

1
2
3
4
5
6
7
8
9
10
public classDemo {

public static void main(String[] args) {
Object o1 = new Object(); //这样的定义默认是强引用
Object o2 = o1; //o2 引用赋值
o1 = null; //o1 置空
System.gc();
System.out.println(o2); //照样可以打印出结果
}
}

软引用(SoftReference)

​ ==内存足够的前提下不收,内存不够了就收了你!== java.lang.SoftReference 类来实现,可以让对象豁免一些垃圾收集!!

软引用通常在对内存敏感的程序之中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.lang.ref.SoftReference;

public classDemo {
//内存够用的情况下
public static void softRef_Memory_enough() {
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<Object>(o1);
System.out.println(o1);
System.out.println(softReference.get());

o1 = null;
System.gc();

System.out.println(o1);
System.out.println(softReference.get());
}

//内存不够用
/**
* 故意产生大对象并配置小的内存,让他内存不够用了导致OOM,看软引用的回收情况
*/
public static void softRef_Memory_NotEnough() {
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<Object>(o1);
System.out.println(o1);
System.out.println(softReference.get());

o1 = null;

try {
byte[] bytes = new byte[2000 * 1024 * 1024];
} catch (Exception e) {
// TODO: handle exception
} finally {
System.out.println(o1);
//这个 打印不出来了
System.out.println(softReference.get());
}
}

public static void main(String[] args) {
//softRef_Memory_enough();
softRef_Memory_NotEnough();

}
}

弱引用(WeakReference)

​ ==只要发生GC一定回收,不管JVM内存是否充足==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.lang.ref.WeakReference;

public classDemo {

public static void main(String[] args) {
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<Object>(o1);

System.out.println(o1);
System.out.println(weakReference.get());

System.out.println("=============");

o1 = null;
System.gc();
System.out.println(o1);
System.out.println(weakReference.get());
}
}
输出: 内存足够
java.lang.Object@15db9742
java.lang.Object@15db9742
=============
null
null

软引用和弱引用的使用场景

​ 假设有一个应用需求读取大量的本地图片

1
2
* 如果每次读取图片都从硬盘读取则会严重影响性能
* 如果一次性加载到内存中又可能造成内存溢出

设计思路是:用一个HashMap 来保存图片的路径和相应图片对象的关联之间的映射关系,在内存不足的情况下,JVM会自动回收这些缓存图片的对象所占用的空间,从而有效的避免了OOM的问题!

==Map<String, SoftRefence> images = new HashMap<>();==

WeakHashMap

==只要发生GC就回收==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.HashMap;
import java.util.WeakHashMap;

public class WeakHashMapDemo {
public static void main(String[] args) {
myHashMap();
System.out.println("===================");
myWeakHashMap();
}

private static void myHashMap() {
HashMap<Integer, String> map = new HashMap<>();
Integer key1 = new Integer(1);
String value1 = "HashMap";
map.put(key1, value1);
System.out.println(map);

key1 = null;
System.out.println(map);

System.gc();
System.out.println(map);
}

private static void myWeakHashMap() {
WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<>();
Integer key2 = new Integer(2);
String value2 = "HashMap-WeakHashMap";
weakHashMap.put(key2, value2);
System.out.println(weakHashMap);

key2 = null;
System.out.println(weakHashMap);

System.gc();
System.out.println(weakHashMap);
}
}
输出:

{1=HashMap}
{1=HashMap}
{1=HashMap}
===================
{2=HashMap-WeakHashMap}
{2=HashMap-WeakHashMap}
{}

虚引用(PhantomReference)

​ 虚引用需要java.lang.ref.PhantomReference类来实现。

顾名思义,就是==形同虚设==,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

==如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收==,==**它不能单独使用,也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用**==

虚引用的主要作用是**跟踪对象被垃圾回收的状态**。**仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。**

**PhantomReference的get方法总是返回null,**   **因此无法访问对应的引用对象**。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

==换句话说,设置虛引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理==。Java技术允许使用finalize()方法在垃圾收集器==将对象从内存中清除出去之前做必要的清理工作==。 ==类似于Spring中的后置通知==。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public classDemo {

public static void main(String[] args) {
Object o1 = new Object();
ReferenceQueue<Object> referentQueue = new ReferenceQueue<Object>();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(o1, referentQueue);

System.out.println(o1);
System.out.println(phantomReference.get());
System.out.println(referentQueue.poll());

System.out.println("===============GC之前=====================");
o1 = null;
System.gc();

System.out.println(o1);
System.out.println(phantomReference.get());
System.out.println(referentQueue.poll());
}
}

输出:
java.lang.Object@7852e922
null
null
=====================================
null
null
java.lang.ref.PhantomReference@4e25154f

强软弱虚总结:

==软引用和虚引用再被回收后,会放入引用队列中,便于后续处理==

==对OOM的认识:==

StackOverflow:

栈内存不够

1
2
3
4
5
6
7
8
public class DemoStackOverflow {
public static void main(String[] args){
myStackOverflow();
}
private static void myStackOverflow() {
myStackOverflow();
}
}

HeapSpace

堆内存不够

1
2
3
4
5
6
public class HeapSpace {
//配置参数 -Xms10m -Xmx10m
public static void main(String[] args){
byte[] bytes = new byte[80 * 1024 * 1024];
}
}

GC Overhead

​ GC回收时间过长时会抛OutOfMemroyError.过长的定义是,==超过98%的时间用来做GC并且回收了不到2%的堆内存====连续多次GC都只回收了不到2%的极端情况下才会抛出==。

假如不抛出GC overhead limit 错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环, CPU使用率一 直是100%, 而GC却没有任何成果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GCOverHead {
//配置参数 -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m(最大直接内存大小)
public static void main(String[] args){
int i = 0;
ArrayList<String> list = new ArrayList<>();
try {
while (true){
list.add(String.valueOf(++i).intern());
}
} catch(Throwable e) {
System.out.println("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");
System.out.println("I = " + i);
e.printStackTrace();
throw e;
}
}
}

Direct Buffer(直接内存溢出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* 描述:
NIO 里面的
* ByteBuffer.allocate(); 第一种方式是分配 JVM 堆内存,属于 GC 管辖,需要拷贝到直接内存速度慢
* ByteBuffer.allocateDirect(); 第二种方式分配本地 OS 内存,不属于GC管辖,不需要拷贝速度快。 性能更好,但是不断地分配本地内存,而堆内存很少使用,
JVM 不会进行 GC,就会爆出 OOM, 程序直接崩溃
*
* @author 小纸人
* @create 2019-08-02 1:10
*/
public class JVMDemo {
public static void main(String[] args){
//查看直接内存大小
System.out.println((VM.maxDirectMemory() / (double)1024 / 1024 ) + "MB");
ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
}

unable to create new native thread(无法创建更多线程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 描述:
* 你的应用创建了太多的线程, linux 默认一个 应用程序 最多创建 1024个线程
*
* @author 小纸人
* @create 2019-08-02 1:10
*/
public class JVMDemo {
public static void main(String[] args){
for (int i = 0; ; i++) {
System.out.println("i = " + i);
new Thread(() -> {
//暂停一会线程
try { TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); } catch (Exception e) { e.printStackTrace(); }
},String.valueOf(i)).start();
}
}
}

Metaspace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

/**
* JVM 参数 :
* -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
* 描述:
* Java 8 之后使用了 元空间Metaspace 代替了 永久代
* 主要存放 :
* 虚拟机加载的类信息
* 常量池
* 静态变量
* 模拟 Metaspace 空间溢出,不停的生成类往 Metaspace 里面灌
* @author 小纸人
* @create 2019-08-02 1:10
*/
public class JVMDemo {
static class OOMTest{
}
public static void main(String[] args){
int i = 0;
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("多少次后发生了异常:" + i);
e.printStackTrace();
}
}
}

本文标题:Java 内存区域与内存溢出异常

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:07:49

原始链接:http://bangjinhu.github.io/undefined/JVM Java虚拟机/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.