内存模型

线程独占:

虚拟机栈:先进后出的 又称方法栈,线程执行方法都是创建一个栈帧,用来存储局部变量表 (存放局部变量的), 操作数栈 (后进先出的操作数栈。负责写入数据和提取数据),动态链接 (执行常量池中的方法引用), 方法出口等信息,JVM 对栈的操作有 2 种,出栈和入栈。方法调用就是入栈。方法返回就是出栈

本地方法栈:区别是虚拟机栈为执行 Java 方法服务,而本地方法栈则为 Native 方法服务 什么是 native 方法呢 就是非 Java 方法。与 Java 环境外交互。因为 JVM 一些底层是 C 写的。比如 Thread 类中的 setPrioruty 方法

HotspotJVM 中 将本地栈和虚拟机栈合二为一的 栈是运行时单位 堆是存储的单位

程序计数器:一块较小的内存空间,是当前线程所执行的字节码的行号指令器,每条线程都要有一个独立的程序计数器,这类内存也被称为线程私有的内存。用来记录程序执行到哪一个地方,下次可以在这个地方继续执行

线程共享:

堆:java 虚拟机内存最大的一块,被所有线程共享,几乎所有线程实例都在这里分配内存。

1.7 包含新生代、老年代、永久代

1.8 只有新生代和老年代 (永久代被删除,新增元空间并且直接存放在内存上)

方法区:存储已被虚拟机加载的类信息,常量,静态变量

堆内存详解

以 Java8 及之后为例,堆是内存模型内占用最大空间的一块地方。这里主要存放对象实例,同时堆又被分为 2 个区域:

新生代:存放新对象或者没达到一定年龄的对象。新生代内部又划分为 3 个区域,Eden 区、以及 2 个幸存区(S0 和 S1),这三个区域默认比例为 8:1:1。当 Eden 区满的时候会发生 Minor GC 此时将 Eden 区和 S0 区的对象放入 S1,然后清空无用对象,此时 S0 区是空的。下次 Minor GC 时 将 Eden 区和 S1 区存活的对象放入 S0,这样做是为了优化垃圾收集的频率,因为我们创建的对象大部分都是无用的。每发生一次 MinorGC,对象的年龄会 + 1,如果一个对象在新生代达到了一定次数(默认 15 次)则会被放入老年代。可以通过调整 MaxTenuringThreshold 参数来决定进入老年代的年龄

老年代:存放常用的对象和大对象(大对象直接放入老年代是为了避免在新生代内产生大量的内存拷贝)。老年代的 GC 为 Major GC,和新生代比起来,需要的时间更长

JVM 的组成部分

JVM 包含两个子系统和两个组件。

类加载子系统:根据类的全限定名来装载 class 文件

执行引擎子系统:执行 class 中的指令

本地接口:与编程语言交互的接口 (c 函数)

运行时数据区: JVM 的内存模型

方法区、永久代、元空间的关系

方法区实际上是一种规范,我们可以理解成一个接口。不同的虚拟机厂商针对这个规范做了不同的实现。以 HotSpot 虚拟机举例。永久代就是最初的一个实现。慢慢的,这个接口一直变更。最后放弃实现类。使用元空间进行替代。

深入理解 Java 虚拟机中。是这么说的。方法区和堆一样,是各个线程共享的内存区域。用于存储被虚拟机加载的类信息,常量,静态变量和一些即时编译后的代码数据

新生代和老年代默认比值是 1:2

到了 Java8,永久代被移除,元空间接替。

元空间不与堆存在一起,而是直接存在本地内存上。所以理论上,机器的内存有多大,元空间就有多大

为什么使用元空间替代永久代

永久代的版本下,字符串常量池存在永久代中。大量使用字符串的情况下,很容易发生 OOM 异常,此外,JVM 加载的 class 总数,方法总大小有时也不稳定。所以永久代的大小很难确定。使用元空间可以解决这个问题。

对象内存布局

对象内存布局分为三个部分,对象头,实例部分,对齐填充

对象头 (Header) 包含三部分

  1. Mark Word 存放对象自身的运行时数据,如 hashcode,gc 分代年龄,锁状态标识,线程持有的锁,偏向线程 ID

  2. Class Pointer 对象指向类型元数据的指针 虚拟机通过这个指针来确定这个对象是哪个类的实例。

  3. 如果这个对象是数组,还有数组长度的信息。因为虚拟机可以通过 Java 对象的元数据确定 Java 对象的大小。但是从数组的元数据无法确定数组的大小。

实例部分 (Instance Data): 对象真正存储的有效信息,也是程序代码定义的类型字段内容。

对齐填充 (Padding): 没什么特别的含义,可以理解为占位符。因为 VM 的内存管理对象起始地址必须是 8 字节的整数倍,也就是对象的大小必须是 8 字节的整数倍,对象头部正好是 8 字节的整数倍,所以如果我们对象实例数据没有对齐时,就要通过对齐填充来补齐。

为什么 CG 分代年龄最大是 15?

因为对象头 header 的 mark word 里面存的信息有对象年龄。使用的是 4bit 4bit 所能表达的最大值就是 15

判断对象是否存活的 2 种算法

引用计数算法: 给对象种添加一个引用计数器,每当一个地方引用它时,计数器 + 1 当引用失效时,计数器 - 1 引用数量为 0 时,说明对象没有被任何引用,可以被认为时垃圾对象,效率高,但无法解决循环引用的问题 Python 采用此算法

可达性算法(根搜索算法): 为了解决引用计数法的循环引用问题,可达性分析算法的基本思路就是一系列名为 GC Roots 的对象作为起点,从这些节点向下搜索,搜索走过的路径成为引用链 当一个对象到 GC Rootes 没有任何引用链相连时,证明此对象不可用 (不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程,两次标记后仍然是可回收对象,将要面临回收)

可做为 GCRoots 的对象:虚拟机栈中引用对象 本地方法引用对象 方法区常量引用对象 方法区静态对象

CMS 垃圾回收器和 G1 垃圾回收器

CMS 垃圾回收器:老年代的回收器,以最短回收停顿时间为目标的收集器。标记清除算法运作过程:初始标记,并发标记,重新标记,并发清除。完成后会产生大量空间碎片。

G1 垃圾回收器: JDK7 诞生,JDK8 走向成熟,JDK9 成为默认的垃圾回收器。主要作用于老年代和新生代,基于标记整理算法实现。回收的范围是整个 Java 堆,分为初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确控制停顿。

JVM 调优参数

- Xmx:堆的最大值

- Xms:堆的最小值

- Xmn:新生代大小

- Xss:堆栈大小

- XX:NewRatio:新生代与老年代的比例,默认为 2 也就是 1:2

- XX:MaxTenuringThreshold:对象进入老年代的年龄 默认 15

- -XX:SurvivorRatio:Eden 区与幸存区的比例大小 默认为 8 也就是幸存区和 Eden 区的比例为 2:8

一般 Xms 和 Xmx 我们会设置为一样,这样可以避免动态调整带来的损耗,让程序启动就分配整个堆内存

类的生命周期

加载:通过类的权限定名获取对应的二进制字节流并转换为方法区运行时需要的数据结构,在堆中生成一个类对象,作为一个入口

验证:主要是确保加载的类符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证以及符号引用验证等

准备:为类的静态变量分配内存并设置初始值(这里的初始值指的是 0,null,false 等,并非程序代码写的初始值!)

解析:将类中的符号引用转为直接引用,即直接指向目标的指针

初始化:为类的静态变量赋予正确的初始值

双亲委派模型

双亲委派模型是 JVM 类加载的一种。其他的加载方式还有

全盘负责:当一个类加载器去负责加载某个类。那么这个类所引用的其他类也要有这个类去加载

父类委托:先让父类加载器加载这个类。只有父类加载器无法加载此类自己才加载。

缓存机制:缓存机制会保证所有加载过的 class 都会被缓存。当程序加载某个类的时候,会先去缓存区寻找,如果不存在,才会去加载对应的类。会造成修改 Class 后,必须重启

双亲委派加载过程:如果一个类加载器收到了类加载的请求。首先不会自己去加载这个类。而是把这个请求委派给父类加载器去完成。不断地向上抛,直到最顶层的启动类加载器。当父类无法完成加载才会让子类去加载。

加载过程:自定义类加载器 -》 应用程序加载器 -》 扩展类加载器 -》 启动类加载器

应用程序加载器:负责加载用户类路径上的类

扩展类加载器:加载 lib 里的类 比如 java.* 开头的

启动类加载器:加载 javahome/lib 库中的类

为什么要使用双亲委派模型?

如果不去最顶层这样一层层加载。假如用户自己定义一个 Object 类。或者是 String 又或者是 Integer。编译的时候,出现了多份同样的字节码,直接破坏了一致性,程序的安全收到极大影响。

打破双亲委派的方法

继承 classLoader 类,重写 loadClass 和 findClass 方法

线程安全性问题

我们以累加数字为例,有一个类负责累加我们传递过去的值,最后把值返回给我们,如果返回的值不是我们输入的值,那么就表明出现了线程不安全。这里我们以多个环境举例,具体如下:

  1. 累加的变量放在方法外,也就是全局变量,这个时候我们开启两个线程,分别传入 25 和 50,这 2 个线程返回的累加值是多少?
  2. 累加的变量放在方法内,也就是局部变量,这个时候我们开启两个线程,分别传入 25 和 50,这 2 个线程返回的累加值是多少?
  3. 使用 ThreadLoca 来存储的变量,放在方法外,这个时候我们开启两个线程,分别传入 25 和 50,这 2 个线程返回的累加值是多少?

第 1 个场景返回 75 和 75,其余 2 个场景返回 25 和 50。因为在第 1 个场景中,变量是全局变量,所有线程都可以访问到,所以将 2 个线程传入的值累加了起来,而第 2 个场景,因为局部变量是私有的,所以线程之间是隔离的。第 3 个场景我们使用了 ThreadLocal 作为变量,ThreadLocal 只能读取到本线程的值,读不到别的线程,也做到了线程隔离。