成为一名合格的 Java 程序员,JVM 是避不开的知识点。

这篇学习笔记的内容来自《深入理解Java虚拟机:JVM高级特性与最佳实践》。

漫谈 JVM

JVM 全称是 Java Virtual Machine,就是说,Java 程序运行在虚拟的环境中,它屏蔽了与各大操作系统的交互难度,所有 Java 程序只对 JVM 负责,不像某些程序,居然要兼容浏览器。所以 Java 程序员是幸福的,不用担心运行效果不一致的问题。

一、发展史

回顾 Java 的历史,了解 JVM 的更新换代,这对我们解决“旧项目”故障,有一定的帮助。

二、结构

JVM 到底是什么,包含哪些部分?

三、运行时数据区

通过下面这张图片,我们来了解运行时数据区的状况:

运行时数据区

3.1 对象访问

简述 Object obj = new Object(); 这条语句将在 JVM 中发生哪些事情?

  1. new 关键字需要在堆中申请分配内存空间,得到 Object() 实例的结构化内存
  2. 假若 Object 还有父类、接口等信息,那么必须在方法区中能找到它们的信息
  3. 从方法区中找到 Object 的全部信息,这是 Java 类级别,实例级别的数据存储在堆中
  4. 最后 Object obj 会在当前栈帧的局部变量表中,作为一个 reference 类型指向堆里面的实例地址

有个问题:虚拟机栈中的局部变量如果是 reference 类型,那么它是如何定位到堆中的实例呢?

主流的虚拟机实现有两种方式:

两种方式的优势:

3.2 OutOfMemoryError

除了程序计数器,其他几个运行时数据区都有可能抛出内存溢出(OOM)异常,了解 JVM 就可以很快定位异常的具体位置。

3.2.1 堆溢出(Java heap space)

通常是对象创建过多,由于 GC Roots 到这些对象之间有可达路径,垃圾回收器不会回收这些对象,并且无法向堆申请更多的扩展空间,导致堆内存溢出。

解决办法:通过对堆转储快照(*.dump)进行分析,确认内存中导致溢出的对象是否有必要,相当于分辨到底是内存泄漏还是内存溢出。

3.2.2 栈溢出

对于栈来说,深度不够和内存不足都是导致抛出堆栈溢出异常的原因,它们俩从根本上是同一个问题,即内存不足导致栈深度不够;栈深度不够是因为内存不足。在单线程中,基本上会抛出 StackOverflowError,而在多线程中,抛出的 OutOfMemoryError 却是由于创建线程时无法申请到足够多的内存。

解决办法:内存不足就去加内存,这是一般人员的做法,有时候要换个思路,每个线程的内存分配过大,可用的线程数就相应变少,无法向系统申请到足够的内存用以创建线程,自然而然就会抛出 OOM 异常,所以就应该减少最大堆设置和栈容量,从而提高可用内存资源给线程使用。

3.3 运行时常量池溢出

String.intern() 方法会创建一个新的常量放入常量池,常量池在方法区中,在 JDK 1.7 版本里,JVM 的方法区设置在 Java 内存模型中,所以如果可用内存不足,则会导致方法区内存溢出,异常提示为:PermGen space

3.4 方法区溢出

方法区内存储的是类的信息,当加载足够多的类信息,并且可用方法区空间不足,则会抛出方法区溢出异常。

类如果要被垃圾收集器回收,生效的条件非常苛刻,除了 GCLib 字节码增强器会动态生成大量 Class 外,还有 JSP 文件会在第一次运行时被编译成 Java 类,以及基于 OSGi 的应用被不同的加载器加载会视为不同的类。

3.5 直接内存溢出

直接内存默认与 -Xmx 的值保持一致,可以通过 -XX:MaxDirectMemorySize 指定。

总结

了解运行时数据区,就相当于你清楚钱花在哪些地方,一旦手头紧,便可以“节省”一点。

下一篇文章,我们来了解一下垃圾收集模块。