Java 虚拟机简介
本文是阅读《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》的笔记
推荐学习资料:
- 《The Java Virtual Machine Specification, Java SE 7 Edition》
http:// hllvm. group. iteye. com/
,高级语言虚拟机圈子
概述
Java 技术体系 4 平台
- Java card
- Java ME(Micro Edition),手机、PDA 等,对 Java API 有所精简
- Java SE(Standard Edition),桌面级应用
- Java EE(Enterprise Edition),除桌面功能外提供企业级功能,如 ERP、CRM等,Java EE 以前称为 J2EE
从 JDK 1.5 以后,官方就不再使用这种命名方式,而是使用 JDK 5、JDK 6 这种形式
Java 虚拟机实现
- Sun HotSpot VM
- Hot 指的是热点代码探测,例如一段被频繁调用的函数将会触发编译器优化以提高这段代码的执行速度
- Sun Mobile-Embedded VM/Meta-Circular VM,用于嵌入式平台
BEA JRockit/IBM J9 VM
、Azul VM/BEA Liquid VM
、Microsoft JVM
(因侵权而放弃),等等
混合语言
Clojure、Jruby、Groovy 等语言都基于 Java 虚拟机,且各有特点,分别适用于不同的场景
其他 Java 特性
- Fork/Join 多线程模型
- 函数式编程,Java 8,lambda
- OpenJDK 是 sun 2006 年把 Java 开源而形成的项目。OpenJDK 对 windows 的支持并不友好
JVM 内存
- 线程私有
- 程序计数器
- 与 CPU 的程序计数器类似,不过在 Java 中这个计数器可能指向的是字节码位置(不同实现可能不同)
- 不同线程都有自己的程序计数器
- 不会抛出 OutOfMemoryError 异常
- Java 虚拟机栈
- 与 OS 中进程的栈概念相似,每个 Java 方法都有自己的栈帧用于保存局部变量、动态链接等信息,例如基本数据类型(boolean、byte、char等)、对象引用和 returnAddress
- 编译时已知的各种基本类型数据保存在栈中的局部变量表中,局部变量表的大小在编译时确定
- StackOverflowError or OutOfMemoryError,Java 允许固定长度的虚拟机栈
- 本地方法栈
- 与 Java 虚拟机栈的功能类似,不过这个栈由 Java 中所调用的 Native 方法使用
- 有些 JVM 将本地方法栈和虚拟机栈合并,Java 方法和 Native 方法都使用相同的栈空间
- A native method is a Java method whose implementation is provided by non-java code
- 程序计数器
- 线程共享
- Java 堆(GC 堆)
- 虚拟机启动时创建,保存绝大部分对象实例与数组。随着 JIT 和逃逸分析等技术的发展对象未必一定在堆上创建
- Java 堆是垃圾搜集器管理的主要区域
- Java 虚拟机规范中规定堆可以位于物理上不连续的内存空间,不过主流的实现都按照可扩展来实现
- 方法区
- 保存已经被虚拟机加载的类信息、常量、静态变量、JIT 编译后的代码等书籍
- 方法区的内存回收是必要的,虽然很少使用,有些 BUG (如内存泄漏)是因为方法区没有回收内存
- HotSpot 虚拟机称之为永久代,因为此虚拟机将 GC 扩展到了方法区,其他虚拟机不存在这个概念
- 运行时常量池
- 存储编译时生成的字面值和符号引用,Java 中常量不一定非要是编译期才能产生,所以这个区域是动态的
- Java String 中的 intern (native 方法)方法会尝试将字符串放到常量池中并返回对常量池的引用,JDK 1.6 和 JDK 1.7 之间的实现有区别
- 直接内存
- 为了避免拷贝数据,Java 提供直接操作非虚拟机管理的内存区域,这些内存被称为直接内存
- Java 堆(GC 堆)
JVM 内存 & OS 内存
- 32位 OS 给每个进程可用的内存空间是有限的, windows/linux 为 2GB(1.9GB)
- JVM 提供了设定 Java 堆(-Xms/-Xmx)、方法区、每个线程栈帧(-Xss)内存大小的机制
对象的创建(HotSpot 为例)
虚拟机内存概念
指针碰撞
假设 Java 堆中内存是绝对规整的,所有用过的内存都放在一边, 空闲的内存放在另一边, 中间放着一个指针作为分界点的指示器, 那所分配内存就仅仅是把那个指针向空闲空间那边挪动 一段与对象大小相等的距离,这种分配方式称为“ 指针碰撞”( Bump the Pointer)
空闲列表
如果 Java 堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞 了,虚拟机就必须 维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的录, 这种分配方式称为“ 空闲列表”( Free List)
TLAB
用于多线程对象的创建,多线程同时在堆上创建对象时无论使用指针碰撞还是空闲列表,都会涉及同步问题
每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲( Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存, 就在哪个线程的 TLAB 上分配, 只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定
可以使用 -XX:+/-UseTLAB
来指定虚拟机是否使用TLAB
对象存储
对象头
HotSpot 对象头由两部分组成:
- 运行时数据,例如哈希码( HashCode)、 GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这些数据一般保存在固定的 32bits 或者 64bits 中
- 对象类型指针,对应于 C++ 中的 vptr
实例数据 & 对齐填充
实例数据就是 Java 对象的有效信息,例如类中的成员变量等
HotSpot 规定对象的起始地址必须是 8 字节的整数倍,这个和 C/C++ 中内存对齐的概念是相似的
对象访问
对象访问分两类,使用句柄和直接指针,如下图所示
使用句柄的好处是便于垃圾回收与内存整理,只要修改句柄中对象的指针就可以移动对象
使用直接指针的好处是速度快,其比句柄方法少了一次指针定位的开销
溢出实例
堆溢出
import java.util.*;
// args: Args:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError
public class test {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
使用工具(Eclipse memory analyzer 分析 dump 文件)判断是内存泄漏还是内存溢出,也可以使用工具查看对象到 GC Roots 的引用链,从而定位对象的来源
栈溢出
两种异常
- StackOverflowError,栈溢出
- OutOfMemoryError,JVM 在扩展栈时无法申请到足够的内存空间
// VM Args:-Xss128k
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++; // 递归时记录溢出时 栈帧个数
stackLeak(); // 每个函数调用都有自己的栈帧,用于保存返回地址等信息
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println(" stack length:" + oom.stackLength);
System.out.println(e);
System.exit(0);
}
}
}
String.intern() 简介
String.intern() 是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用
JDK 6 和 JDK 7 中 intern 的实现有一定区别。JDK 6 将字符串对象保存在常量池中;JDK 7 将字符串对象保存在堆中而将字符串引用保存在常量池中
方法区溢出 & CGlib & JSP
可修改字节码以生成新的类,Spring、Hibernate 等框架都基于此类字节码技术
运行时使用 CGLIB 可以创建大量的类从而出现方法区溢出的异常
方法区溢出的另一种场景就是包含大量 JSP 文件的场景,JSP 需要被编译为 Java 类才能运行