Java 学习笔记:JVM

JVM 基本概念

  • 可运行 Java 代码的假想计算机,其包括字节码指令集、寄存器、栈、垃圾回收器、堆和存储方法域
  • 运行在操作系统之上,与硬件没有直接交互

在这里插入图片描述

  • Java 源文件 -> 编译器 -> 字节码文件
  • 字节码文件 -> JVM -> 机器码

线程

此处及下文所说的线程是指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。HotSpot JVM 中 Java 线程与原生操作系统线程有直接的映射关系。

当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收,操作系统负责调度所有线程,并将其分配到任何可用的 CPU 上。当原生线程创建完毕,就会调用 Java 线程的 run() 方法。当线程结束时,会释放原生线程和 Java 线程的所有资源。

HotSpot JVM 后台运行的系统线程主要有以下几个:

线程 特性
虚拟机线程 VM thread 该线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有: stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
周期性任务线程 该线程负责定时器事件(即中断),用于调度周期性操作的执行
GC 线程 该线程支持 JVM 中不同的垃圾回收活动
编译器线程 该线程在运行时将字节码动态编译成本地平台相关的机器码
信号分发线程 该线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理

运行时数据区域

在这里插入图片描述

除了被紫色线程框隔离的数据区外,堆与方法区由所有线程共享。

程序计数器

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空),可看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变该计数器的值来选取下一条需要执行的字节码指令。

唯一没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈

是描述 Java 方法执行的内存模型,每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用、动态链接、方法出口等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

栈帧是用于存储数据和部分过程结果的数据结构,同时也被用于处理动态链接、方法返回值和异常分派。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

在这里插入图片描述
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:

java -Xss512M HackTheJava

所需内存空间在编译期间完成分配,大小在运行期间不会发生改变。

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过虚拟机所允许的最大值,会抛出 StackOverflowError 异常
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常

特另:

  • 在单个线程情况下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时均抛出 StackOverflowError 异常。
  • 不断建立线程的情况下,为每个线程的栈分配的内存越大,越容易产生内存溢出异常。需要考虑减少最大堆和栈容量来换取更多线程的情况。

本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地(Native)方法服务,虚拟机栈为虚拟机执行 Java 方法(即字节码)服务。

本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

在这里插入图片描述

用于存放对象实例与数组,几乎所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:

  • 新生代(Young Generation)

    用于存放新生的对象,一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁出发 MinorGC 进行垃圾回收。

    又分为 Eden 区、From Survivor 区 和 To Survivor 区(空间划分为 8 :1 :1 )三个区域。

    • Eden 区:Java 新对象的出生地。当 Eden 区内存不够时会触发 MinorGC,对新生代区进行一次垃圾回收。
    • SurvivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的扫描者。
    • SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者。
  • 老生代(Old Generation)

    主要存放应用程序中生命周期长的内存对象。

堆不需要连续物理内存,并且可以动态扩展其内存,无多余内存可完成实例分配且扩展失败时会抛出 OutOfMemoryError 异常。

可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

java -Xms1M -Xmx2M HackTheJava

方法区

用于存放已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常,可选择不实现垃圾收集。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是对类的卸载一般比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但 GC 不会在主程序运行期间对永久代区域进行清理,且很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。

为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至 “ 元数据区” (元空间),本质与永久代类似,与其最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,在默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory,字符串池和类的静态变量放入 Java 堆中。这样加载类的元数据就不再受 MaxPermSize 限制,而由系统的实际可用空间来控制

运行时常量池

运行时常量池是方法区的一部分。

Class 文件中的常量池(编译期生成的字面量和符号引用)会在类加载后被放入这个区域。

除了在编译期生成的常量,还允许存储运行期间动态生成的常量,例如 String 类的 intern ()。

  • 在 JDK 1.6 中, intern() 会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用。
  • 在 JDK 1.7 中,intern() 不会再复制实例,而只是在常量池中记录首次出现的实例引用。

本机直接内存

在 JDK 1.4 中新引入了 NIO 类,提供了基于 Channel 与 Buffer 的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

虽然本机直接内存分配不会受到 Java 堆大小的限制,但仍旧受本机总内存(包括 RAM 以及 SWAP 区或分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

  • 直接内存异常特征:在 Heap Dump 文件中不会看到明显的异常
  • 常出现于:OOM 之后 Dump 文件很小且程序中直接或间接地使用了 NIO

潜在问题:

在垃圾收集进行时,虚拟机虽然会对直接内存进行回收,但是直接内存无法像新生代、老生代那样,发现空间不足就通知收集器进行垃圾回收,它只能等待老生代满了后 Full GC 顺便清理直接内存中的废弃对象。否则其只能等到抛出内存溢出异常时先捕获异常并在异常处理语句中调用 System.gc(),若仍旧无法解决(虚拟机打开了 了 -XX:+DisableExplicitGC 开关等情况),则在堆中还有空闲内存的情况下会抛出内存溢出异常。

内存占用

从实践经验的角度出发,除了 Java 堆和永久代之外,下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。

  • 本机直接内存 Direct Memory

    可通过 -XX:MaxDirectMemorySize 调整大小,内存不足时抛出 OutOfMemoryError 或者OutOfMemoryError:Direct buffer memory。

  • 线程堆栈

    可通过 -Xss 调整大小,内存不足时抛出 StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者 OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。

  • Socket 缓存区

    每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出 IOException:Too many open files 异常。

  • JNI 代码

    如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。

  • 虚拟机和 GC

    虚拟机、GC的代码执行也要消耗一定的内存。

垃圾收集

垃圾收集主要是针对堆和方法区进行,其内存分配与回收是动态的。

程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,内存分配在编译期基本确定,分配与回收均具有确定性,因此不需要对这三个区域进行垃圾回收。

判断对象是否可被回收

引用计数算法

为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

public class Test {
   
    public Object instance = null;
    public static void main(String[] args) {
   
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        doSomething();
    }
}

在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。

可达性分析算法 / 根搜索算法

以 GC Roots 为起始点进行搜索,通过引用链(搜索所走过的路径)可到达的对象都是存活的,不可达的对象可被回收。

在这里插入图片描述

Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。

类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

finalize()

类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

当一个对象可被回收时,如果需要执行该对象的 finalize () 方法,那么就有可能在该方法中(等待队列中)让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize () 方法自救,后面回收时不会再调用该方法。

引用类型

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。

Java 提供了四种强度不同的引用类型。

强引用

把一个对象赋给一个引用变量,该引用变量就是一个强引用。被强引用关联的对象不会被回收。

使用 new 一个新对象的方式来创建强引用。

Object obj = new Object();

即使该对象永远不会被用到,JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

软引用

被软引用关联的对象只有在内存不够的情况下才会被回收,通常用在对内存敏感的程序中。

使用 SoftReference 类来创建软引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联

弱引用

被弱引用关联的对象一定会被回收,即它只能存活到下一次垃圾回收发生之前。

使用 WeakReference 类来创建弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

虚引用

又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。它不能单独使用,必须和引用队列联合使用。

为一个对象设置虚引用的主要作用是跟踪对象被垃圾回收的状态,在这个对象被回收时收到一个系统通知。

使用 PhantomRefe

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值