Java内存与对象

497 阅读13分钟

JVM内存区域 - 运行时数据区域

内存区域

名称作用生命周期所有者
程序计数器占用较小的空间,可以看作当前线程所指向的字节码的行号指示器。是程序控制流的治时期:分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。跟随独立线程的生命周期线程私有
Java虚拟机栈描述Java方法执行的线程内存模型。方法执行的时候VM创建栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的开始与结束对应这栈帧的入栈和出栈。跟随独立线程的生命周期线程私有
本地方法栈作用与虚拟机栈一致,只是服务对象变为了本地(Native)方法。在Hotspot虚拟机中与虚拟机栈合二为一了。跟随独立线程的生命周期线程私有
Java堆存放对象实例。其中所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。同Java虚拟机所有线程共享
方法区存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。需要注意,不能将方法去等同于“永久代”,而且在JDK 7就开始了移除永久代(PermGen)[在堆上分代]的工作,在JDK 8中已经不存在永久代。-- 后续有同学讲解GC可以详细讨论这个问题。同Java虚拟机所有线程共享
运行时常量池是方法区的一部分。存放类被加载后的class文件中的常量池表(Constant Pool Table) // 下面的对象布局可以看到。同Java虚拟机所有线程共享

Class 文件结构

  1. Main.java :
package xyz.co1ne.example.effective;

public class Main {
    public static void main(String[] args) {
        System.out.println("hello world!");
    }
}
  1. javac编译后的Main.class :
PS H:\CameraTools\Effective\src\main\java\xyz\co1ne\example\effective> javap -verbose .\Main.class
Classfile /H:/CameraTools/Effective/src/main/java/xyz/co1ne/example/effective/Main.class
  Last modified 2020-10-22; size 442 bytes
  MD5 checksum 8418410f5174104fb4339bace7364369
  Compiled from "Main.java"
public class xyz.co1ne.example.effective.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // hello world!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // xyz/co1ne/example/effective/Main
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Main.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               hello world!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               xyz/co1ne/example/effective/Main
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public xyz.co1ne.example.effective.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
SourceFile: "Main.java"
  1. 十六进制class文件

    PS H:\CameraTools\Effective\src\main\java\xyz\co1ne\example\effective> format-hex .\Main.class
    
    
               路径: H:\CameraTools\Effective\src\main\java\xyz\co1ne\example\effective\Main.class
    
               00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
    
    00000000   CA FE BA BE 00 00 00 34 00 1D 0A 00 06 00 0F 09  Êþº¾...4........
    00000010   00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07  ................
    00000020   00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29  .....<init>...()
    00000030   56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E  V...Code...LineN
    00000040   75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69  umberTable...mai
    00000050   6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67  n...([Ljava/lang
    00000060   2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75  /String;)V...Sou
    00000070   72 63 65 46 69 6C 65 01 00 09 4D 61 69 6E 2E 6A  rceFile...Main.j
    00000080   61 76 61 0C 00 07 00 08 07 00 17 0C 00 18 00 19  ava.............
    00000090   01 00 0C 68 65 6C 6C 6F 20 77 6F 72 6C 64 21 07  ...hello world!.
    000000A0   00 1A 0C 00 1B 00 1C 01 00 20 78 79 7A 2F 63 6F  ......... xyz/co
    000000B0   31 6E 65 2F 65 78 61 6D 70 6C 65 2F 65 66 66 65  1ne/example/effe
    000000C0   63 74 69 76 65 2F 4D 61 69 6E 01 00 10 6A 61 76  ctive/Main...jav
    000000D0   61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 10  a/lang/Object...
    000000E0   6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D  java/lang/System
    000000F0   01 00 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69  ...out...Ljava/i
    00000100   6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00  o/PrintStream;..
    00000110   13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74  .java/io/PrintSt
    00000120   72 65 61 6D 01 00 07 70 72 69 6E 74 6C 6E 01 00  ream...println..
    00000130   15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72  .(Ljava/lang/Str
    00000140   69 6E 67 3B 29 56 00 21 00 05 00 06 00 00 00 00  ing;)V.!........
    00000150   00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1D  ................
    00000160   00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00  ........*·..±...
    00000170   01 00 0A 00 00 00 06 00 01 00 00 00 03 00 09 00  ................
    00000180   0B 00 0C 00 01 00 09 00 00 00 25 00 02 00 01 00  ..........%.....
    00000190   00 00 09 B2 00 02 12 03 B6 00 04 B1 00 00 00 01  ...²....¶..±....
    000001A0   00 0A 00 00 00 0A 00 02 00 00 00 05 00 08 00 06  ................
    000001B0   00 01 00 0D 00 00 00 02 00 0E                    ..........
    
    
  • class 文件标识符

0xCAFEBABE - Magic Number

  • 版本号

0x0000 - Minor Version

0x0034 - Major Version = 52 (jdk1 = 45)

  • 常量池

0x001D - 常量池数量 = 28项 (从1开始计数,1到1D即29为28项)[常量池有包括CONSTANT_Utf8_info、CONSTANT_Integer_info、CONSTANT_Class_info、CONSTANT_NameAndType_info等17种常量类型]

常量池中主要存放两大类常量:字面量(Literal)和符合引用(Symbolic References)。字面量比较接近Java语言层面的常量概念,如文本字符串、被生命final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  1. 被模块到处或者开放的包(Package)
  2. 类和接口的全限定名(Fully Qualified Name) ,如 Ljava/lang/String;
  3. 字段的名称和描述符(Descriptor)
  4. 方法的名称和描述符,方法名称: #11 ,方法的描述类似于JNI动态注册时的“方法签名”,也就是参数类型+返回值类型:
  5. 方法句柄和方法类型(Method Handle、Method Type)

“I” "V" "" “LineNumberTable” “LocalVaribaleTable”是编译器自动生成的,会被字段表(field_info)、方法表(method_info)、属性表(attribute_info)所引用。

  • 访问标识
标识名称标识值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许invokespecial字节码指令搜索最近的父类调用。
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,此标识值为真,其他类型值为假。
...
  • 字段表

用于描述接口或者类中的声明的变量。

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

Java 对象布局

Java的对象布局主要由三个部分构成:对象头、实例数据、对齐填充。

  • 对象头(Header)

对象头主要有两部分(数组对象有三组分)组成。 Markword, Klass 指针(数组对象的话,还有一个 length)。

MarkWord

标记字主要存储对象运行时的一部分数据。主要内容有 hashcode,GC 分代年龄,锁状态标志位,线程锁标记,偏向线程ID,偏向时间戳等。MarkWord 在32位和64位虚拟机上的大小分别位32bit 和 64bit,它的最后 2 bit 是锁标志位,用来标记当前对象的状态,具体如下:

状态标志位存储内容
未锁定01对象哈希码/对象分代年龄
轻量级锁定00指向锁记录的指针
膨胀(重量级锁定)10执行重量级锁定的锁指针
GC 标记11空(不需要记录信息)
可偏向01偏向线程id, 偏向时间戳,对象分代年龄

Klass指针

对象指向它的类型元数据的指针。jvm通过这个指针来确定该对象是哪个类的实例。


接下来我们看个例子:

  • Java源代码:
package xyz.co1ne.example.effective;

import org.openjdk.jol.info.ClassLayout;

public class Phone {
    private String name;
    private int price;

    public Phone(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public static void main(String[] args) {
        Phone iPhone12 = new Phone("iPhone 12",6299);
        Phone galaxyS20 = new Phone("Samsung galaxy S20",4999);
        System.out.println("Phone.main object iPhone12 : \n "+ ClassLayout.parseInstance(iPhone12).toPrintable());
        System.out.println("Phone.main object galaxyS20\n "+ ClassLayout.parseInstance(galaxyS20).toPrintable());
    }
}

看看运行的结果:

Phone.main object iPhone12 : 
 xyz.co1ne.example.effective.Phone object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4                int Phone.price                               6299
     16     4   java.lang.String Phone.name                                (object)
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
Phone.main object galaxyS20
 xyz.co1ne.example.effective.Phone object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4                int Phone.price                               4999
     16     4   java.lang.String Phone.name                                (object)
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

字节序与大小端:

字节序

字节序通常是指从计算机存放/取出字节时与先前取出/存放的序列顺序或规则。一般的将字节序按照字节存储顺序分为大端和小端两种。

大端(Big Endian) 是指将高位字节存放在内存的低地址端,低位字节放在内存的高地址端;

数字0x12345678

低地址 高地址 -----------------------------------------> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 12 | 34 | 56 | 78 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

**小端(Little Endian)**是指将高位字节存放在内存的高地址端,低位字节放在内存的低地址端;

0x12345678

lsb msb -----------------------------------------> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 78 | 56 | 34 | 12 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

线程安全相关概念

  • 什么是线程安全?

《Java并发编程实战》的做个Brain Goetz的定义是:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

而在我们Java中一般讨论的线程安全是指保证对象单次的操作是正确的。一般情况下我们调用的时候不用进行额外的保障措施,允许对于一些特定顺序的连续调用可能需要在调用端使用额外的同步手段来保证调用的正确性。那么我们称这个对象是线程安全的。常见的类例如:VectorHashTableCollections#synchronizedCollection()方法包装的集合等。

  • 线程安全的实现方法
  1. 阻塞同步
  2. 非阻塞同步(无锁编程)
  3. 无同步方案
  • 原子性、可见性与有序性
  1. 原子性(Atomicity)

    直接保证原子性变量的操作指令包括read(读取[作用于主内存])、load(载入)、assign(赋值)、use(使用)、store(存储)和write(写入)这个六个。我们可以大致认为基本数据类型的访问、读写都是具备原子性的。

  2. 可见性(Visibility)

    可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

  3. 有序性(Ordering)

    如果在本线程内观察,所有的操作都是有序的。如果在一个线程中观察另一个线程,所有操作都是无序的(指令重排和工作内存与主内存同步延迟)。

  • CAS操作

为了提高性能,JVM很多操作都依赖CAS实现,一种乐观锁的实现。

CAS:Compare and Swap。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。如果A=V,那么把B赋值给V,返回V;如果A!=V,直接返回V。