1. Java内存区域(运行时数据区)

JDK1.8之后
JVM - 图1

JDK1.8之后方法区(HotSpot的永久代)被彻底移除了,取而代之的是元空间,元空间使用的是直接内存。

1.1. 程序计数器(线程私有)

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
程序计数器的作用

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道线程上次运行到哪儿了。

程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,他的生命周期随着线程的创建而创建,随着线程的结束而死亡。
**

1.2. 虚拟机栈(线程私有)

1.Java内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是虚拟机栈(中局部变量**表部分【方法中声明的变量】**,实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)。

2.局部变量表主要存放了编译期可知的各种数据类型(8个基本数据类型)、对象引用(reference类型,指向对象起始地址的指针或代表对象的句柄或其他)

3.Java虚拟机栈会出现的问题**StackOverFlowError****OutofMemoryError**

  • StackOverFlowError 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。

4.Java方法返回(栈帧被弹出)的两种方式:

  1. return语句
  2. 抛出异常

Java虚拟机栈生命周期与线程相同。

1.3. 本地方法栈(线程私有)

与虚拟机栈相比:

  • 相同点:本地方法栈的作用和虚拟机栈类似,都是创建栈帧,存放XX方法的局部变量表、操作数栈、动态链接、出口信息等;return或异常后也会弹栈。
  • 不同点:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

本地方法栈生命周期与线程相同,也会出现StackOverFlowError和 OutOfMemoryError 两种错误。

1.4. 堆(涉及到垃圾回收,有待完善)

堆是Java虚拟机管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一作用就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

JDK1.8之后方法区(HotSpot的永久代)被彻底移除,取而代之的是元空间,元空间使用的是直接内存。
image.png
会出现如下错误:**

  1. java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发该错误。(和本机物理内存无关,和你配置的内存大小有关!)

1.5. 方法区&元空间

image.png

1.5.1. 方法区

功能:方法区是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息,常量、静态变量、编译后的代码等。
方法区和永久代的关系
方法区是Java虚拟机规范中的定义(标准);
永久代是HotSpot的概念(实现)。
在JDK1.7前方法区是由永久代实现的,当永久代被移除后,方法区由元空间实现。

1.5.2. 元空间

在JDK1.8中HotSpot中的永久代已被移除,用元空间代替,元空间使用直接内存。
替换原因:

  1. 永久代有一个JVM本身设置固定大小的上限,无法调整;元空间使用的是直接内存,只受本机可用内存的限制,内存溢出的概率比原来小。(java.lang.OutOfMemoryError:MetaSpace)
  2. 元空间存放的是类的元数据,能加载多少类就由系统的实际可用内存空间来控制,能加载更多的类。
  3. JDK1.8 合并了 HotSpot 和 JRockit 代码, JRockit中没有永久代,所以合并后就不额外设置永久代了。

如果不设置大小,随着类的创建,虚拟机可能会耗完所有系统内存(默认只受本机可用内存限制)
调参

-XX:MetaspaceSize=N 设置Metaspace的初始(最小)大小
-XX:MaxMetaspaceSize=N 设置Metaspace的最大大小


1.6. 运行时常量池

运行时常量池是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)。

JDK1.7之前运行时常量池也包含了字符串常量池,在JDK1.7中将字符串常量池从方法区拿到了堆中,运行时常量池依然留在方法区中。

运行时常量池位于元空间中,受到元空间最大大小限制,会产生OutOfMemoryError错误。

2. Java的类加载

一个类的完整声明周期如下所示:
类加载的过程包含了上图所示的 加载、连接、初始化,其中连接分为 验证、准备、解析

2.1. 加载

  1. 通过全类名获取定义此类的二进制字节流;
  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口。
  • 非数组类的加载阶段(获取二进制字节流的动作)是可控性最强的阶段,这一步可以自定义类加载器去控制字节流的获取方式;
  • 数组类型不通过类加载器创建,有Java虚拟机直接创建。

2.2. 验证

2.3. 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区中分配。

  1. 进行内存分配的仅包括类变量(static),不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。
  2. 这里所设置的初始值”通常情况”下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:

比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。

2.4. 解析

虚拟机将常量池内的符号引用替换为直接引用的过程

2.5. 初始化

初始化时类加载的最后一步,是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()方法的过程。
对于<clinit>() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit>() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。
    • 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。
    • 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如Class.forname(“…”),newInstance()等等。 ,如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。
  6. 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

2.6. 卸载

卸载类即该类的Class对象被GC。
卸载类需要满足3个要求:

  1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被GC

所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了,jdk自带的BootstrapClassLoader,PlatformClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

3. Java对象的创建及访问

image.png

2.1. Java对象的创建流程:

类的加载-> 分配内存 -> 初始化零值 -> 设置对象头 -> 执行init方法
内存分配的两种方式:分别为指针碰撞空闲列表,采用哪种方式取决于Java堆是否规整,而Java堆是否规整取决于采用的垃圾收集器所是否带有压缩整理功能。
垃圾收集器算法

规整 复制算法
“标记-整理”(“标记-压缩”)
不规整 “标记-清除”

内存分配并发问题
虚拟机采用两种方式来保证创建对象的线程安全:

  1. CAS+失败重试:CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  2. TLAB(Thread Local Allocation Buffer,线程本地分配缓冲区): 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配(这是一个线程专用的内存分配区域),当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

    2.2. 对象的内存布局

    在HotSpot虚拟机中,对象在内存中的布局可以分为3个区域:对象头实例数据对齐填充。

  3. 对象头:

    1. 一部分存储对象自身的运行时数据(哈希吗、GC分代年龄、锁状态标志)
    2. 另一部分是类型指针,指向它的类的元数据
  4. 实例数据:对象真正存储的有效信息,程序中所定义的各种类型的字段内容。
  5. 对齐填充部分:这部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。

2.3. 对象的访问定位

对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄直接指针两种:

  1. 句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
  2. 直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

这两种对象访问方式各有优势:

  1. 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
  2. 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

    4. String类和常量池

    字符串常量池是位于堆内存中的
  • 使用双引号创建出来的字符串是存储在常量池中的;
  • 通过new创建的字符串是存储在堆中的;
  • 通过和堆中对象相加的方式创建的字符串对象是存储在堆中的;
  • 通过常量池中的对象相加的方式创建的字符串对象是存储在常量池中的;

String s = new String("123");通过这种方式会创建1到2个对象

  1. 首先,如果常量池中没有”123”对象,则会在常量池中创建该对象,如果有了则不创建;
  2. 其次,会在堆中创建该对象。

    1. public static void main(String[] args) {
    2. String s1 = "dog"; // 利用双引号创建出来的字符串是存储在常量池中的
    3. String s2 = new String("dog"); // 通过对象创建的方式创建的字符串是存放在堆中的
    4. String temp = "d";
    5. String temp1 = "og";
    6. String s3 = temp + temp1; // 创建堆中对象
    7. String s4 = "d" + "og"; // 创建常量池中对象
    8. System.out.println(s1 == s2); // false
    9. System.out.println(s1 == s3); // false
    10. System.out.println(s3 == s4); // false
    11. System.out.println(s1 == s4); // true
    12. //这个过程会创建1-2个对象
    13. //首先,如果常量池中没有"abcdefg"对象,则会在常量池中创建该对象
    14. //其次,会在堆内存中创建该对象
    15. String s5 = new String("abcdefg");
    16. }

    5. 8种基本类型的包装类和常量池

image.png
8个基本数据类型是存放在元空间的运行时常量池中的;
8个基本数据类型的包装类中:
Byte,Short,Integer,Long 的[-128, 127] 是存储在运行时常量池中的;
Character 的[0,127] 是存储在运行时常量池中的;
Boolean 直接返回[true or false];
Float和Double没有实现常量池技术。