Wilder's Blog.

JVM学习笔记: GC(1)

字数统计: 2.1k阅读时长: 7 min
2018/05/28 Share

JVM GC (1)

最近我想慢慢把我学到的JVM知识整理到笔记中,不然看书看了之后就忘记真的是凉凉。

在接触GC之前,我们先来看一下 Java 内存区域

运行时数据区域

JVM运行数据分为几部分:

  • 程序计数器
  • Java 虚拟机栈
  • 本地方法栈
  • Java 堆
  • 方法区(永久代)
  • 运行时常量
  • 直接内存

数据区

程序计数器

​ 在代码执行的过程中,当执行完某一行代码之后我们需要执行下一个指令,这个指令有可能是循环、跳转、异常处理等,而程序计数器的功能就是选取下一条需要执行的字节码指令。

​ 程序计数器是线程特有的,各条线程之间计数器互不影响,独立存储,这称为“线程私有”。

​ 程序计数器通过改变计数器的值来选取下一条需要执行的字节码指令。

Java 虚拟机栈

​ 和程序计数器一样,虚拟机栈也是线程私有的,线程之间互不影响。

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

​ 局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

这里写图片描述

本地方法栈

​ 本地方法栈(Native Method Stack) 于虚拟机所返回的作用时非常相似的,他们之间的区别是不过虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。而虚拟机规范中对本地方法栈中方法使用的语言,使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(HotSpot)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflow 异常和 OOM 异常。

Java 堆(GC堆)

​ Java Heap 是 Java 虚拟机所管理的内存中最大的一块。Java 堆并不是线程私有的,所有的线程都共享一个 Java 堆。几乎所有的对象实例都在 Java 堆中进行分配,与此同时 Java 堆也是垃圾收集管理的主要区域。

​ 从内存回收角度来看的话,Java 堆可以细分为 新生代和老年代 ,更细的话新生代还可以划分为 Eden 区、From Survivor区、To Survivor区。

​ 从内存分配角度来看的话,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区。

​ Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果Java 堆中新生代或者老年代的内存没有被 GC 掉导致内存溢出的话会产生OOM异常(我就遇到了这样的情况)。

方法区

​ 方法区也是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也有人称为 “永久代” 。

​ Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域是比较少见的,但并非数据进入了方法区就如同永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分的区域的回收确实是由必要的。

(1)运行时常量池

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

​ 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OOM 异常。

​ 运行时常量池相对于 Class 文件常量池的另外一个重要特种是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预制入 Class 文件中常量池的内容才能既然怒方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的便是 String 类的 intern 方法。

(2)String.intern() 方法

String.intern() 这是一个 native 方法,它的作用是:如果字符串常量池中已经包含了一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象,否则将此 String 对象包含的字符串添加到常量池中,并返回此字符串的引用。在 JDK1.6 和 JDK1.7中常量池在内存中的位置不一样,我们来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

String s3 = new String("1")+new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}

​ 这段代码在 JDK1.6 的运行结果是都为false,在 JDK1.7 以上结果是 false 和 true

JDK1.6解释

JDK1.6

​ JDK 1.6 的常量池是放在Perm区,Perm区和 Java Heap 区是完全分开的。引号声明的字符串是在常量池中生成的,而new出来的字符串是存放在 Java Heap 中的。因此两种不同声明方式的字符串存放的位置完全不相同,所以结果都为 false 。

JDK 1.7解释

​ JDK 1.7 的常量池放在了 Java Heap 的一个区域中,并不放在 Perm 区中

JDK 1.8

我们来分析一下上面那段代码:

  • String s = new String(“1”) 之后在 Java Heap 区存放了这个对象,在常量池中存放了“1”,调用了intern()方法之后发现“1” 已经在常量池中,s 便不会指向常量池中的常量。当声明String s2 = “1” ,s2 会指向常量池中的字符串“1”,而s并没有指向常量池中的字符串,所以返回false。
  • String s3 = new String(“1”)+new String(“1”) ,常量池中会存放“1”,Java Heap区存放了“11”,调用intern() 方法之后发现常量池并没有“11”,所以将“11”保存到常量池中并指向该常量,最后在声明 s4 的时候会指向常量池的“11”,因此比较后的结果是 true

直接内存

​ 直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存被频繁使用,也可能出现OOM异常。

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

CATALOG
  1. 1. JVM GC (1)
    1. 1.1. 运行时数据区域
    2. 1.2. 程序计数器
    3. 1.3. Java 虚拟机栈
    4. 1.4. 本地方法栈
    5. 1.5. Java 堆(GC堆)
    6. 1.6. 方法区
      1. 1.6.1. (1)运行时常量池
      2. 1.6.2. (2)String.intern() 方法
        1. 1.6.2.1. JDK1.6解释
        2. 1.6.2.2. JDK 1.7解释
    7. 1.7. 直接内存