jvm内存管理

在这里插入图片描述

JAVA内存模型

计算机硬件内存结构

CPU 计算速度非常快,协调 CPU 和各个硬件之间的速度
在单核情况下,基于缓存的交互可以很好的解决 CPU 与其它硬件之间的速度匹配,但是在多核情况下,各个处理器都要遵循一定的协议来保障内存中的各个处理器的缓存和主内存中的数据一致性问题,这类协议通常被称为缓存一致性协议。在这里插入图片描述
在这里插入图片描述

JVM 与 JMM 间的区别

JMM 是 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
而 JVM 则是描述的是 Java 虚拟机内部及各个结构间的关系。
小伙伴这时可能会有疑问,既然 JMM 是定义线程和主内存之间的关系,那么它的出现是不是解决并发领域的问题啊?没错,我们先回顾一下并发领域中的关键问题。

并发领域中的关键问题?

线程之间的通信
在编程中,线程之间的通信机制有两种,共享内存和消息传递。

  • 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
  • 消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在 java 中典型的消息传递方式就是 wait()和 notify()。

线程间的同步
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

总结:
事实上,Java 内存模型(JMM)的并发采用的是共享内存模型

Java 内存模型

JMM 的控制模型作图
在这里插入图片描述
由此可见,Java 内存模型(JMM)同 CPU 缓存模型结构类似,是基于 CPU 缓存模型来建立的。
JMM 的工作流程,有一台三核的计算机,cpu1 操作线程 A,cpu2 操作线程 B,cpu3 操作线程 C,当这三个线程都需要对主内存中的共享变量进行操作时,这三条线程分别会将主内存中的共享内存读入自己的工作内存,自己保存一份共享变量的副本供自己线程本身使用。

  • 主内存、工作内存的定义是什么?
  • 如何将主内存中的共享变量读入自己线程本身的工作内存?
  • 当其中的某一条线程修改了共享变量后,其余线程中的共享变量值是否变化,如果变化,线程间是怎么保持可见性的?

主内存、工作内存的定义

主内存
主内存主要存储的是 Java 实例对象,即所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。
工作内存
工作内存主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),即每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关 Native 方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
注意: 这里的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、方法区不是同一层次的内存划分,这两者基本上没有关系。

内存的交互操作

主内存与工作内存的交互操作有 8 种,虚拟机必须保证每一个操作都是原子的,这八种操作分别是:

  • Lock(锁定)
    作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁)
    作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取)
    作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
  • load(载入)
    作用于工作内存的变量,它把 read 操作从主存中变量放入工作内存中
  • use(使用)
    作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign(赋值)
    作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store(存储)
    作用于工作内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的 write 使用
  • write(写入)
    作用于主内存中的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中

操作流程图:
在这里插入图片描述

如果要把一个变量从内存中复制到工作内存中,就需要顺序的执行 read 和 load 操作,如果把变量从工作内存同步到主内存中,就需要执行 store 和 write 操作。
注意: Java 内存模型只要求上述操作必须按顺序执行,却没要求是连续执行。

我们以两个线程为例梳理下操作流程:

假设存在两个线程 A 和 B,如果线程 A 要与线程 B 要通信的话,首先,线程 A 把本地内存 A中更新过的共享变量刷新到主内存中去;然后,线程 B 到主内存中读取线程 A 之前已经更新过的共享变量。

如果多个线程同时读取修改同一个共享变量,这种情况可能会导致每个线程中的本地内存中缓存变量一致的问题,这个时候该怎么解决呢?

JMM 缓存不一致问题

解决 JMM 中的本地内存变量的缓存不一致问题有两种解决方案,分别是总线加锁和MESI缓存一致性协议
总线加锁
总线加锁是 CPU 从主内存读取数据到本地内存时,会先在总线对这个数据加锁,这样其它 CPU 就没法去读或者去写这个数据,直到这个 CPU 使用完数据释放锁后,其它的 CPU 才能读取该数据。在这里插入图片描述
总线加锁虽然能保证数据一致,但是它却严重降低了系统性能,因为当一个线程多总线加锁后,其它线程都只能等待,将原有的并行操作转成了串行操作。
通常情况下,我们不采用这种方法,而是使用性能较高的缓存一致性协议。
MESI 缓存一致性协议

MESI 缓存一致性协议是多个 CPU 从主内存读取同一个数据到各自的高速缓存中,当其中的某个 CPU 修改了缓存里的数据,该数据会马上同步回主内存,其它 CPU 通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。

在并发编程中,如果多个线程对同一个共享变量进行操作是,我们通常会在变量名称前加上关键在volatile,因为它可以保证线程对变量的修改的可见性,保证可见性的基础是多个线程都会监听总线。即当一个线程修改了共享变量后,该变量会立马同步到主内存,其余线程监听到数据变化后会使得自己缓存的原数据失效,并触发read操作读取新修改的变量的值。进而保证了多个线程的数据一致性。事实上,volatile的工作原理就是依赖于 MESI 缓存一致性协议实现的。

Java 内存模型的实现

在 Java 多线程中,Java 提供了一系列与并发处理相关的关键字,比如volatile、synchronized、final、concurren包等。其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字
事实上,Java 内存模型的本质是围绕着 Java 并发过程中的如何处理原子性、可见性和顺序性这三个特征来设计的,这三大特性可以直接使用 Java 中提供的关键字实现,它们也是面试中经常被问到的题目。

原子性

原子性的定义是一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

JMM 保证的原子性变量操作包括 read、load、assign、use、store、write

注意: 基本类型数据的访问大都是原子操作,long 和 double 类型的变量是 64 位,但是在 32 位 JVM 中,32 位的 JVM 会将 64 位数据的读写操作分为 2 次 32 位的读写操作来进行,这就导致了 long、double 类型的变量在 32 位虚拟机中是非原子操作,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。
对于非原子操作的基本类型,可以使用 synchronized 来保证方法和代码块内的操作是原子性的。

synchronized (this) {
  a=1; 
  b=2;
}

如一个线程观察另外一个线程执行上面的代码,只能看到 a、b 都被赋值成功结果,或者 a、b 都尚未被赋值的结果。

可见性

Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
Java 中的 volatile 关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用 volatile 来保证多线程操作时变量的可见性。
除了 volatile,Java 中的 synchronized 和 final 两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。

有序性

在 Java 中,可以使用 synchronized 和 volatile 来保证多线程之间操作的有序性。实现方式有所区别:

volatile 关键字会禁止指令重排。synchronized 关键字保证同一时刻只允许一条线程操作。

好了,这里简单的介绍完了 Java 并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像 synchronized 关键字是万 能的,他可以同时满足以上三种特性,这其实也是很多人滥用 synchronized 的原因。

但是 synchronized 是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。

JVM

Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。翻译的作用

在这里插入图片描述
在这里插入图片描述

JVM工作原理

javap -v HelloWorld
    
....
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                  // getstatic 获取静态字段的值
         3: ldc           #3                  // ldc 常量池中的常量值入栈
         5: invokevirtual #4                  // invokevirtual 运行时方法绑定调用方法
         8: return							  // //void 函数返回
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

着重看一下main方法中的被编译成的字节码文件。上面字节码文件中,0,3,5,8这些数字代表PC程序计数器,getstatic 表示获取静态的字段值,ldc表示常量池中的常量值入栈,比如程序中打印出的Hello World字符串,实际上是在常量池中的常量。

Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些字节码指令 ,就叫作 opcode(getstatic、ldc、invokevirtual、return 等)。

JVM 就是靠解析这些 opcode 和操作数来完成程序的执行的。当我们使用 Java 命令运行 .class 文件的时候,实际上就相当于启动了一个 JVM 进程 。然后 JVM 会翻译这些字节码,它有两种执行方式:解释执行和JIT。常见的就是解释执行,将 opcode + 操作数翻译成机器代码;另外一种执行方式就是 JIT,也就是我们常说的即时编译,它会在一定条件下将字节码编译成机器码

JVM结构

JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分

运行时数据区域
java 引以为豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。
在 Java 中,JVM 内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈。
在这里插入图片描述

程序计数器

较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

  • Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令,也就是CPU的时间片轮转调度
  • 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存 。

特点:
区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。

当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。

程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个 (每个线程执行进度不同,都需要一块内存存储自己执行到了哪一条指令,以及下一条指令的地址,因此程序计数器是线程私有的)。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
程序计数器也是JVM中唯一不会OOM(OutOfMemory)的内存区域
在这里插入图片描述

内存用完了
当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error,其他各个内存区域都可能会抛出OOM

虚拟机栈

线程私有,即生命周期和线程相同。

栈是什么样的数据结构?先进后出(FILO)的数据结构,
虚拟机栈在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址。
Java 虚拟机栈是基于线程的。哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。
在这里插入图片描述

栈帧

每个栈帧,都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)
是用于支持虚拟机进行方法调用和方法执行的数据结构。
每个栈帧对应于一个方法,那么方法是有参数、返回值、局部变量、调用外部类方法等属性的,因此栈帧中需要包含这些信息来供虚拟机识别。

局部变量表: 存放局部变量。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位(long和double类型)的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。
操作数据栈:存放方法执行的操作数,它就是一个栈,先进后出的栈结构,操作数栈。
动态连接:Java语言特性多态(需要类运行时才能确定具体的方法)。

假设A类调用了B类的方法,编译期间A对B的方法调用之间的地址关系无法确定,只有在类加载以及运行期间去调用的时候才能确定地址,由此就引入了符号引用和直接引用,动态链接就是存储他们直接的关系。

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。而转换成直接引用会分静态解析、动态连接两个部分

静态解析:在类加载阶段或者第一次使用的时候这些符号引用一部分就会转化为直接引用。静态解析的4中情形:

  • 1、静态方法
  • 2、私有方法(因为它是无法被重写的)
  • 3、构造方法
  • 4、父类方法

动态链接:还有一部分符号引用将在每一次运行期间转化为直接引用,这部分称为动态链接,这体现了Java的多态性

返回地址: 正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)

无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行

本地方法栈

本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的,服务的对象是 native 方法。
虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一 。
特点:
(1)native类方法存在的意义是填补java代码不方便实现的缺陷而。
(2)虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务
(3)是线程私有的,它的生命周期与线程相同,每个线程都有一个
在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常:

  • StackOverFlowError :线程请求的栈深度>所允许的深度。
  • OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存
线程共享的区域

方法区/永久代
很多开发者都习惯将方法区称为“永久代”,其实这两者并不是等价的。
HotSpot 虚拟机使用永久代来实现方法区,但在其它虚拟机中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一说。因此,方法区只是 JVM 中规范的一部分,可以说,在 HotSpot 虚拟机中,设计人员使用了永久代来实现了 JVM 规范的方法区。
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。
JVM 在执行某个类的时候,必须先加载。在加载类(加载、验证、准备、解析、初始化)的时候,JVM 会先加载 class 文件,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。
而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。
例如,类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的;在 JVM 加载完类之后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。
方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地
元空间大小参数:
jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8以后大小就只受本机总内存的限制(如果不设置参数的话)

JVM参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

Java8 为什么使用元空间替代永久代,这样做有什么好处呢?
官方给出的解释是:
移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

堆是线程共享
堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。此内存区域的唯一目的就是存放对象实例。

堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。
随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage
Collection)。 那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java
类中存在的位置。 Java 的对象可以分为基本数据类型和普通对象。 对于普通对象来说,JVM
会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

在这里插入图片描述

堆的特点:
(1)是Java虚拟机所管理的内存中最大的一块。

(2)堆是jvm所有线程共享的。

堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)

(3)在虚拟机启动的时候创建。

(4)唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。

(5)Java堆是垃圾收集器管理的主要区域。

(6)因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。

(7)java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。

(8)方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。

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

Java堆内存分类:
设置堆空间大小

  • 年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。
  • 老年代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
    在这里插入图片描述

注意: 随着Java语言的发展, 现在已经能看到些许迹象表明日后可能出现值类型的支持, 即使只考虑现在, 由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上分配、 标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

对象分配过程
JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,因此还需要考虑GC执行完内存回收后是否存在空间中间产生内存碎片。

分配过程

1.new的对象先放在伊甸园区。该区域有大小限制

2.当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到伊甸园区

3.然后将伊甸园区中的剩余对象移动到幸存者0区

4.如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区

5.如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区。

6.如果累计次数到达默认的15次,这会进入养老区。可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N

7.养老区内存不足是,会再次出发GC:Major GC 进行养老区的内存清理

8.如果养老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常.

分配对象的流程:
在这里插入图片描述
堆GC
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:一种是部分收集器(Partial GC)另一类是整堆收集器(Fu’ll GC)

部分收集器: 不是完整收集java堆的的收集器,它又分为:

  • 新生代收集(Minor GC / Young GC): 只是新生代的垃圾收集
  • 老年代收集 (Major GC / Old GC): 只是老年代的垃圾收集 (CMS GC 单独回收老年代)
  • 混合收集(Mixed GC):收集整个新生代及老年代的垃圾收集 (G1 GC会混合回收, region区域回收)

整堆收集(Full GC):收集整个java堆和方法区的垃圾收集器
年轻代GC触发条件:

  • 年轻代空间不足,就会触发Minor GC, 这里年轻代指的是Eden代满,Survivor不满不会引发GC
  • Minor GC会引发STW(stop the world) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复.

老年代GC (Major GC)触发机制

  • 老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC
  • 如果Major GC , 内存仍然不足,则报错OOM
  • Major GC的速度比Minor GC慢10倍以上.

FullGC 触发机制:

  • 调用System.gc() , 系统会执行Full GC ,不是立即执行.
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC进入老年代平均大小大于老年代可用内存

元空间

在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中(JVM 的角度看,JVM 内存之外的部分叫作本地内存)。 HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是**元空间(Metaspace)**而已。

它和永久代有什么不同的?

  • 存储位置不同: 永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
  • 存储内容不同: 在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
    在这里插入图片描述
    为什么要废弃永久代,引入元空间?
  • 在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。
  • 移除永久代是为融合HotSpot VM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
  • 永久代会为GC带来不必要的复杂度,并且回收效率偏低。

废除永久代的好处

  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。
  • 将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。
  • 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。

Metaspace相关参数

  • XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。如果没有使用该参数来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。
  • 但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。
  • 如果设置了该参数,当Metaspace剩余空间不足,会抛出:java.lang.OutOfMemoryError: Metaspace space
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

方法区

方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载 的类型信息、常量、 静态变量、 即时编译器编译后的代码缓存等数据。

《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但些简单的实现可能不会选择去进行垃圾收集或者进行压缩”。对HotSpot而言,方法区还有一个别名叫做Non-Heap(非堆),的就是要和堆分开。

元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类的元信息的 。
在这里插入图片描述
创建对象各数据区域的声明:
在这里插入图片描述
创建对象的命令中各部分所存储的区域:

  • Person:类元数据文件存储在方法区中,也就是.class文件。
  • 对象:new Person命令创建出实际的对象,存放在堆中,进行管理
  • 引用:per表示的对象引用存储在栈内存中。在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

方法区的特点:

  • 方法区与堆一样是各个线程共享的内存区域
  • 方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续
  • 方法区的大小跟堆空间一样 可以选择固定大小或者动态变化
  • 方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类 导致方法区溢出虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久带) Java 8之后 是MetaSpace(元空间) )
  • 关闭JVM就会释放这个区域的内存

方法区结构:

在这里插入图片描述
类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。
方法区中存储的内容:

  • 类型信息(域信息、方法信息)
  • 运行时常量池
    在这里插入图片描述

类型信息
对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:

  • 这个类型的完整有效名称(全名 = 包名.类名)
  • 这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)
  • 这个类型的修饰符( public, abstract,final的某个子集)
  • 这个类型直接接口的一个有序列表

域信息

域信息,即为类的属性,成员变量

JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。

域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)

方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  • 方法名称方法的返回类型(或void)
  • 方法参数的数量和类型(按顺序)
  • 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集
  • 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)
  • 异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

方法区设置

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。

JDK8以后

元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定

默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。

与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace

-XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。

如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

jps #查看进程号
jinfo -flag MetaspaceSize 进程号 #查看Metaspace 最大分配内存空间
jinfo -flag MaxMetaspaceSize 进程号 #查看Metaspace最大空间

运行时常量池

常量池vs运行时常量池

  • 字节码文件中,内部包含了常量池
  • 方法区中,内部包含了运行时常量池
  • 常量池:存放编译期间生成的各种字面量与符号引用
  • 运行时常量池:常量池表在运行时的表现形式

编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。

理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时从常量池中取数据,被加载的字节码中的常量池的信息是放到了方法区的运行时常量池中。

总结:它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。 在运行时,由执行引擎将常量池中的数据从字节码文件中加载到内存,存储在方法区的运行时常量池中。
在这里插入图片描述
要弄清楚方法区的运行时常量池,需要理解清楚字节码中的常量池。

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表( Constant pool table),包括各种字面量和对类型、域和方法的符号引用。

常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

对字节码文件反编译之后,查看常量池中的常量池表Constant pool table相关信息:


#常量池表相关信息
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/lagou/concurrent/demo/test/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/lagou/concurrent/demo/test/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/lagou/concurrent/demo/test/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V

在方活中对常量池表的符号引用

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

可以看到方法中对常量池表的符号引用(#2、#3、#4)。比如3: ldc #3 这条对应于常量池表中的#3这条数据内容,表示一个常量字符串Hello World。

为什么需要常量池?

举例来说:

public class Solution {
	public void method() {
		System.out.println("are you ok");
	}
}

这段代码很简单,但是里面却使用了 String、 System、 PrintStream及Object等结构。如果代码多,引用到的结构会更多!这里就需要常暈池,将这些引用转变为符号引用,具体用到时,采取加载。

直接内存
直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分。

在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区 (Buwer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的 DirectByteBuwer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了 在Java堆和Native堆中来回复制数据。

在这里插入图片描述
在这里插入图片描述

NIO的Buwer提供一个可以直接访问系统物理内存的类——DirectBuwer。DirectBuwer类继承自ByteBuwer,但和普通的ByteBuwer不同。普通的ByteBuwer仍在JVM堆上分配内存,其最大内存受到最大堆内存的 限制。而DirectBuwer直接分配在物理内存中,并不占用堆空间。在访问普通的ByteBuwer时,系统总是会使用一个“内核缓冲区”进行操作。而DirectBuwer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuwer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuwer更快。

通过使用堆外内存,可以带来以下好处:

  • 改善堆过大时垃圾回收效率,减少停顿。Full GC时会扫描堆内存,回收效率和堆大小成正比。Native的内存,由OS负责管理和回收。
  • 减少内存在Native堆和JVM堆拷贝过程,避免拷贝损耗,降低内存使用。
  • 可突破JVM内存大小限制。

总结
在这里插入图片描述

  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是二次元穿越来的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值