本篇为学习JAVA虚拟机的第六篇文章,介绍线程共享区域。

一、内存模型–JAVA堆

  • java堆一般是java虚拟机所管理的内存中最大的一块。

  • java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

  • 堆上存放对象实例和数组。

  • java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

  • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

二、内存模型–方法区

image

方法区和堆一样,是各个线程共享的内存区域。

它用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

其中,类信息包含类的版本、字段、接口、方法

八、PermGen与Metaspace

其实,方法区可以理解为一个规范,jdk6的具体实现是PermGen,而后来的版本具体实现是Metaspace。它们有一定的区别。

HotSpot JVM 中,永久代中用于存放类和方法的元数据以及常量池,比如ClassMethod。每当一个类初次被加载的时候,它的元数据都会放到永久代中。

永久代是有大小限制的,它用的是JVM内存,即与堆内存等价的no heap区域,因此如果加载的类太多,很有可能导致永久代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen ,为此我们不得不对虚拟机做调优。

  1. 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM
  2. 移除 PermGen 可以促进 HotSpot JVMJRockit VM 的融合,因为 JRockit 没有永久代。

根据上面的各种原因,PermGen 最终被移除,方法区移至 Metaspace,字符串常量移至 Java HeapMetaspace并不在虚拟机中,而是使用本地内存,十分方便管理,不会出现永久带内存溢出问题,垃圾回收的时候这个单独区域方便处理。

三、运行时常量池

是方法区的一部分。

类文件中除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法的运行时常量池中存放。

这里尤其值得注意的是字符串的创建,会被扔到字符串常量池中。如果是new,那么还是在堆重创建的。当然,运行时也可以产生新的常量放入池中,比如讲new出来的字符串用intern()方法便可以在运行时将其放到常量池中。

举例

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";

System.out.println(str1 == str2); //true

String str3 = new String("hello");

System.out.println(str1 == str3); //false

System.out.println(str1 == str3.intern()); //true
}

说明

对于直接声明的内容相同的字符串,对于str2来说是不需要重新分配地址的,因为str1的hello这个常量已经存在于常量池中了。所以他们两个其实是一个东西。

对于new出来的str3,是不会直接扔到常量池中的,他是在堆中分配,地址不一样,所以显然是false。

String类的intern()方法,使得运行时将堆中产生的对象放入常量池中,所以是true。

这里我在java字符串核心一网打尽中已经详细说明了,不再赘述。

四、对象探秘

4.1 对象的创建过程
  • 类加载检查:检查该对象的类是否已经被加载、解析、初始化过,如果没有则先进行类加载操作。

  • 分配内存:如果内存规整使用“指针碰撞”分配,否则一般使用“空闲列表”分配,具体看垃圾回收器是否带有整理(Compact)空闲内存功能。

  • 初始化:将内存区初始化置零,不包含对象头,这一步保证了对象的实例字段在java代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  • 对象头设置:这个对象是哪个类的实例、如何找到类的元数据信息、哈希码、GC分代年龄信息等即为对象头

  • 对象的方法:即按照程序员的意愿进行初始化

4.2 对象的内存布局
  • 对象头

一部分称为Mark Word,存储对象自身运行时的数据,包含哈希码、GC分代年龄、锁状态标志等等。

采用压缩存储,压缩到虚拟机位数(32位/64位)。由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计为一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

另一部分为类型指针,指向它的类元数据,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息不一定要经过对象本身。

如果对象是一个java数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中却无法确定数组的大小。

  • 实例数据

    • 实例数据部分是对象真正存储的有效信息,也是在程序中定义的各种类型的字段内容。
    • 无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
    • 从分配策略中可以看出,相同宽度的字段总是分配在一起,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
  • 对齐填充

    • 非必需,只有前两者加起来非8的倍数时才会有。
    • 因为HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说,对象的大小必须是8字节的整数倍。不对齐的时候,需要通过它来填充对齐。

九、对象的访问定位

  • 通过句柄访问

image

通过句柄访问对象:当java虚拟机GC移动堆对象时,并不需要修改reference,只需修改句柄对象的实例数据指针。

  • 通过直接指针访问

image

通过直接指针访问对象:加快了对象访问速度,比间接访问少一次对象实例数据的访问,HotSpot则采用的这种访问方式。