运行时数据区概述以及线程

运行时数据区内部结构

image-20200616171552089

内存是非常重要的系统资源 是硬盘和cpu的中间仓库和桥梁, JVM内存布局 规定了java在运行过程中内存申请 分配 管理的策略,保证了JVM的高效稳定运行. 不同的JVM对于内存划分和管理机制存在部分差异 结合JVM虚拟机规范,探讨经典的JVM内存布局

image-20200616171859301

image-20200616172108162

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁.另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁.

  • 每个线程: 独立包括程序计数器 栈 本地栈

  • 线程间共享: 堆 堆外内存(永久代或元空间, 代码缓存)

    image-20210306131024281

线程

  • 线程是一个应用程序里的运行单元 JVM允许一个应用有多个线程并行执行
  • 在HotSpot里 每个线程都与操作系统的本地线程直接映射
    • 当一个java线程准备好执行以后 此时一个操作系统的本地线程也同时创建
    • java线程执行终止后 本地线程也会回收
  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上 一旦本地线程初始化成功 他就会调用java线程中的run()方法
  • 守护线程
  • 普通线程

守护线程普通线程的唯一区别是:当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用System.exit则必定会退出)

HotSpot JVM里主要是以下几个:

虚拟机线程 周期任务线程 GC线程 编译线程 信号调度线程

程序计数器(PC寄存器)

image-20200616174300996

image-20200616174425061

  • 它是一块 很小的内存空间 几乎可以忽略不计 也是运行速度最快的存储区域

  • 在JVM规范中 每个线程都有他自己的程序计数器 是线程私有的 生命周期与线程的生命周期 保持一致

  • 任何时间一个线程都只有一个方法在执行 也就是所谓的当前方法 程序计数器 会存储当前线程正在执行的java方法 的jvm指令地址 或者 如果是在执行native方法 则是未指定值(undefined)

  • 它是程序控制流的指示器 分支 循环 跳转 异常处理 线程恢复等基础功能 都需要依赖这个计数器来完成

  • 字节码解释器工作时就是通过改变这个计数器的值来获取 下一条需要执行的字节码指令

  • 它是唯一一个在java虚拟机 规范中没有 规定任何 OutOfMemoryError情况的区域

GC OOM 栈 寄存器没有垃圾回收 但是栈有可能溢出

(类似于游标 或者集合的迭代器)

image-20200616175753892

蓝色列:指令地址(偏移地址)

右边为:操作指令

image-20200616180132942

两个常见面试问题

image-20200616180320222

PC寄存器为什么会被设定为线程私有?

多线程在特定的一个时间段内只会执行其中的某一个线程的方法,CPU会不停做任务切换,这样必然导致经常终端或恢复

为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样依赖各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况.

由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一个指令

这样必然导致经常中断或者恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响

CPU时间片

CPU时间片 即CPU分配给各个程序的时间 每个线程被分配一个 时间段 称作他的时间片

在宏观上:我们可以同时打开多个应用程序 每个线程并行不悖,同时运行

但在微观上:由于只有一个CPU 一次只能处理程序要求的一部分 如何处理公平 一种方法就是引入时间片 每个程序轮流执行

虚拟机栈

虚拟机栈概述

虚拟机栈出现的背景

由于跨平台性的设计,Java指令都是根据栈来设计.不同平台CPU架构不同,所以不能设计为基于寄存器

优点是:跨平台 指令集小 编译器容易实现 缺点是性能下降 实现同样功能需要更多的指令

内存中栈与堆

运行时的单位 存储的单位

虚拟机栈是什么

java虚拟机栈(Java Virtual Machine Stack) 早期也叫java栈

每个线程在创建时都会创建一个虚拟机栈 其内部保存一个个的**栈帧(**Stack Frame) ,对应着一次次的Java方法调用

每个方法执行时,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息.每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

生命周期

生命周期和线程一致

作用

主管java程序的运行 他保存方法的 局部变量(8种基本数据类型 对象的引用地址) 部分结果 并参与方法的调用和返回

  • 局部变量 vs 成员变量(属性)
  • 基本数据类型变量 vs 引用类型变量(类 数组 接口)

栈的特点

  • 是一种快速有效的分配存储方式 访问速度仅次于程序计数器
  • JVM直接对java栈的操作只有两个:
    • 每个方法执行入栈 压栈 进栈
    • 执行结束 后的出栈操作
  • 对于栈来说不存在 垃圾回收问题

GC; OOM

栈中可能出现的异常

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的Java虚拟机栈 那每一个线程的java虚拟机栈容量可以在线程创建时候独立选定 如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量 java虚拟机将会抛出一个StackOverflowError异常
  • 如果java虚拟机栈可以动态扩展 并且在尝试扩展的时候无法申请到足够的内存 或者在创建新的线程的时候没有足够的内存去创建对应的虚拟机栈 那java虚拟机将会抛出一个OutOfMemoryError异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 演示栈中的异常:StackOverflowError
* @author shkstart
* @create 2020 下午 9:08
*
* 默认情况下:count : 11420
* 设置栈的大小: -Xss256k : count : 2465
*/
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}

}

设置栈内存大小

我们可以使用参数-Xss选项来设置线程的最大栈空间 栈的大小直接决定了函数 调用的最大可达深度

栈的存储单位

栈中存储着什么

  • 每个线程都有自己的栈 栈中的数据都是以栈帧(Stack Frame)的格式 存在
  • 在线程上正在执行的每个方法都对应着一个 栈帧
  • 栈帧是一个内存区块 是一个数据集 维系着方法执行过程中的各种数据信息

OOP的基本概念 类 对象

基本结构 field (属性 字段 域) method

栈运行原理

  • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循”先进后出”的原则

  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧.即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method), 定义这个方法的类就是当前类(Current Class).

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作

  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前帧

  • 不同线程所包含的栈帧不允许存在相互引用的 即不可能在一个栈帧中引用另外 一个线程的栈帧

  • 如果当前方法调用了其他方法 方法返回之际 当前栈帧会传回此方法的执行结果给前一个栈帧 虚拟机丢弃当前栈帧 使前一个栈帧重新成为当前栈

  • java方法有两种返回函数的方式 正常的函数返回 使用return 另外一种是抛出异常不管哪种都会导致栈帧被弹出

栈帧的内部结构

image-20200617085856579

  • 局部变量表也成为 局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,方法引用,已经返回地址类型
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中.在方法运行期间是不会改变局部变量表的大小的
  • 方法嵌套调用的次数由栈的大小决定.一般来说,栈越大,方法嵌套调用次数越多.对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息 增大的需求,进而函数调用就会占用更多的占空间,导致其嵌套调用次数就会减少
  • 局部变量表中的变量只在当前方法调用中有效. 在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程.当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁

字节码中方法内部结构的剖析

变量槽Slot的理解

数据类型在局部变量表中的存储空间以局部变量槽来表示 局部变量表所需的内存空间在编译器完成分配

局部变量表所需的内存空间在编译时期完成分配 也就是说在进入一个方法时 这个方法需要在栈帧中分配多大的局部变量空间是完全确定**(编译期)**的

在方法运行期间 不会改变局部变量表的大小

这里指的大小 是变量槽的数量

参数值的存放总是在局部变量数组的index()开始 到数组长度-1结束

最基本的存储单元是Slot

在局部变量表里32位以内的类型占用一个slot

64位的占用两个slot (long double)

  • byte short char 在存储前被转换为int, boolean 也被转换成int 0表示false 非0表示true
  • long 和double占用两个slot

image-20200617093920027

关于slot的使用理解

image-20200617094553753

1
2
3
4
5
6
7
8
9
10
/*
变量的分类:按照数据类型分:
① 基本数据类型
② 引用数据类型
按照在类中声明的位置分:
① 成员变量:在使用前,都经历过默认初始化赋值
类变量: linking的prepare阶段:给类变量默认赋值 ---> initial阶段:给类变量显式赋值即静态代码块赋值
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
*/

补充说明

  • 在栈帧中 与性能调优关系最为密切的就是 局部变量表 在方法执行时 虚拟机是由局部变量表完成方法的传递
  • 局部变量表中的变量也是重要的垃圾回收根节点只要被局部变量表中直接或间接引用的对象都不会被回收

操作数栈

image-20210329225110748

  • 每一个独立的栈帧中除了包含局部变量表以外.还包含一个后进先出的操作数栈,也可以称之为表达式栈

  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈

    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈
    • 比如执行复制交换,求和等操作
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间

  • 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的

  • 每一个操作数栈都有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为Max_stack的值

  • 栈中的任意一个元素都是可以任意的Java数据类型

    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈单位深度
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问

  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令

  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证

  • Java虚拟机的解释引擎是基于栈的执行引擎,其中栈指定就是操作数栈

代码追踪

image-20200617100816629

image-20200617100952437

image-20200617101026575

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
程序员面试过程中, 常见的i++和++i 的区别,放到字节码篇章时再介绍。

*/
public void add(){
//第1类问题:
int i1 = 10;
i1++;

int i2 = 10;
++i2;

//第2类问题:
int i3 = 10;
int i4 = i3++;

int i5 = 10;
int i6 = ++i5;

//第3类问题:
int i7 = 10;
i7 = i7++;

int i8 = 10;
i8 = ++i8;

//第4类问题:
int i9 = 10;
int i10 = i9++ + ++i9;
}

栈顶缓存技术

image-20200617101904920

动态链接(指向运行时常量池的方法引用)

  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里.比如:描述一个方法调用了另外的方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

字节码文件中 有一个常量池 运行后存放到方法区 成为运行时常量池

为什么需要常量池?

提供符号和常量 便于指令识别

方法的调用

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

  • 静态链接

    当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时.这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接

  • 动态链接

    如果被调用的方法在编译期无法被确定,也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称为动态链接

对应方法的绑定机制为: 早期绑定(Early Binding)晚期绑定(Late Binding).绑定是一个字段,方法或类在符号引用被替换为直接引用的过程,这仅仅发生一次

  • 早期绑定:

    早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用

  • 晚期绑定:

    如果被调用的方法在编译期无法被确定下来,只能在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 说明早期绑定和晚期绑定的例子
* @author shkstart
* @create 2020 上午 11:59
*/
class Animal{

public void eat(){
System.out.println("动物进食");
}
}
interface Huntable{
void hunt();
}
class Dog extends Animal implements Huntable{
@Override
public void eat() {
System.out.println("狗吃骨头");
}

@Override
public void hunt() {
System.out.println("捕食耗子,多管闲事");
}
}

class Cat extends Animal implements Huntable{

public Cat(){
super();//表现为:早期绑定
}

public Cat(String name){
this();//表现为:早期绑定
}

@Override
public void eat() {
super.eat();//表现为:早期绑定
System.out.println("猫吃鱼");
}

@Override
public void hunt() {
System.out.println("捕食耗子,天经地义");
}
}
public class AnimalTest {
public void showAnimal(Animal animal){
animal.eat();//表现为:晚期绑定
}
public void showHunt(Huntable h){
h.hunt();//表现为:晚期绑定
}
}

image-20200617110158792

虚方法和非虚方法

非虚方法:

  • 如果方法在编译期就确定了具体的调用版本 这个版本在运行时是不可变的 这样的方法称为非虚方法
  • 静态方法 私有方法 final方法 实例构造器 父类方法都是非虚方法
  • 其他方法称为虚方法

多态使用前提 :

  1. 类的继承关系
  2. 方法的重写

image-20200617110804255

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.atguigu.java2;

/**
* 解析调用中非虚方法、虚方法的测试
*
* invokestatic指令和invokespecial指令调用的方法称为非虚方法
* @author shkstart
* @create 2020 下午 12:07
*/
class Father {
public Father() {
System.out.println("father的构造器");
}

public static void showStatic(String str) {
System.out.println("father " + str);
}

public final void showFinal() {
System.out.println("father show final");
}

public void showCommon() {
System.out.println("father 普通方法");
}
}

public class Son extends Father {
public Son() {
//invokespecial
super();
}
public Son(int age) {
//invokespecial
this();
}
//不是重写的父类的静态方法,因为静态方法不能被重写!
public static void showStatic(String str) {
System.out.println("son " + str);
}
private void showPrivate(String str) {
System.out.println("son private" + str);
}

public void show() {
//invokestatic
showStatic("atguigu.com");
//invokestatic
super.showStatic("good!");
//invokespecial
showPrivate("hello!");
//invokespecial
super.showCommon();

//invokevirtual
showFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
//虚方法如下:
//invokevirtual
showCommon();
info();

MethodInterface in = null;
//invokeinterface
in.methodA();
}

public void info(){

}

public void display(Father f){
f.showCommon();
}

public static void main(String[] args) {
Son so = new Son();
so.show();
}
}

interface MethodInterface{
void methodA();
}

invokedynamic

image-20200617111752835

动态类型语言和静态类型语言

动态类型语言静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言.反之就是动态类型语言.

说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这时动态语言的一个重要特征

Java属于静态语言 js和python动态类型语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 体会invokedynamic指令
* @author shkstart
* @create 2020 下午 3:09
*/
@FunctionalInterface
interface Func {
public boolean func(String str);
}

public class Lambda {
public void lambda(Func func) {
return;
}

public static void main(String[] args) {
Lambda lambda = new Lambda();

Func func = s -> {
return true;
};

lambda.lambda(func);

lambda.lambda(s -> {
return true;
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 体会invokedynamic指令
* @author shkstart
* @create 2020 下午 3:09
*/
@FunctionalInterface
interface Func {
public boolean func(String str);
}

public class Lambda {
public void lambda(Func func) {
return;
}

public static void main(String[] args) {
Lambda lambda = new Lambda();

Func func = s -> {
return true;
};

lambda.lambda(func);

lambda.lambda(s -> {
return true;
});
}
}

方法重写的本质

image-20200617141830676

  • 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都有重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率.因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现,使用索引表来代替查找

  • 每个类中都有一个虚方法表,表中存放着各个方法的实际入口

  • 那么虚方法什么时候被创建?

    虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕

在链接 阶段的解析阶段 被创建

方法返回地址

  • 存放调用该方法的PC寄存器的值
  • 一个方法的结束有两种方式
    • 正常执行结束
    • 出现未处理的异常 非正常退出
  • 无论通过哪种方式退出 在方法退出后都返回到该方法被调用的位置 方法正常退出时 调用者的pc计数器的值作为返回地址 即调用该方法的指令的下一条指令的地址 而通过异常退出的 需要通过异常表来确定 栈帧中一般不保存这部分信息

本质上,方法的退出就是当前栈帧出栈的过程.此时,需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去

正常完成出口和异常完成出口的区别在于:通过异常完成出口的退出不会给他的上一层调用者产生任何的返回值

image-20200617144204802

image-20200617144512636

面试题

image-20200617145251415

为什么不是分配的栈内存越大越好?

不是 内存是有限的 挤压其他栈空间 会使得线程数变少

垃圾回收主要在 方法区和堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.atguigu.java3;

/**
* 面试题:
* 方法中定义的局部变量是否线程安全?具体情况具体分析
*
* 何为线程安全?
* 如果只有一个线程才可以操作此数据,则必是线程安全的。
* 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
* @author shkstart
* @create 2020 下午 7:48
*/
public class StringBuilderTest {

int num = 10;

//s1的声明方式是线程安全的
public static void method1(){
//StringBuilder:线程不安全 但是在这个方法里面 s1是安全的
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
//...
}
//sBuilder的操作过程:是线程不安全的
public static void method2(StringBuilder sBuilder){
sBuilder.append("a");
sBuilder.append("b");
//...
}
//s1的操作:是线程不安全的
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1的操作:是线程安全的
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}

public static void main(String[] args) {
StringBuilder s = new StringBuilder();


new Thread(() -> {
s.append("a");
s.append("b");
}).start();

method2(s);

}

}

本地方法栈

本地方法接口

什么是本地方法?

简单的讲,一个Native Method就是一个Java调用非Java代码的接口.一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C.这个特征并非Java所特有,很多其它编程的语言都有这一机制,比如在C++中,你可以用extern”C”告知C++编译器去调用一个C的函数

本地接口的作用是融合不同编程语言为Java所有,它的初衷是融合C/C++程序

1
2
3
4
5
6
7
8
9
10
public class IHaveNatives {
public native void Native1(int x);

public native static long Native2();

private native synchronized float Native3(Object o);

native void Native4(int[] ary) throws Exception;

}

image-20200617151007608

image-20200617151059925

现状

目前该方法使用的越来越少类,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见. 因为现在异构领域间的通信发达,比如可以使用socket通信,也可以使用Web Service等等

本地方法栈

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用

  • 本地方法栈 也是线程私有

  • 允许被实现成固定或者可动态扩展的内存大小 (在内存溢出方面是相同的)

    • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 StackOverflowError异常
    • 如果本地方法栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的内存的时候没有足够的内存区创建对应 的本地方法栈,那么Java虚拟机将会抛出一个OutofMemoryError异常
  • 本地方法是使用C语言实现的

  • 它的具体做法是NativeMethodStack中登记native方法 在Execution Engine执行时加载本地方法库

  • 当某个线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界 他和虚拟机拥有同样的权限

    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 它甚至可以直接使用 本地处理器中的寄存器
    • 直接从本地内存的堆中分配任意数量的内存
  • 并不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式, 数据结构等

    如果JVM产品不打算支持native方法,也可以无需实现本地方法栈

  • Hotspot JVM中直接将本地方法栈和虚拟机栈合二为一

堆的核心概述

  • 一个JVM实例 只存在一个堆内存 堆也是Java内存管理的 核心区域
  • Java堆区 在JVM启动的时候就被创建 其空间大小也就确定了 是JVM管理的最大一块内存空间
    • 堆内存大小是可以 调节的
  • java 虚拟机 规范 规定 堆可以处于物理上不连续的内存空间中 但在逻辑上它应该被视为连续的
  • 所有的线程共享 java堆 在这里还可以划分线程私有的缓冲区

共享就会存在线程安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* -Xms10m -Xmx10m
*
* @author shkstart shkstart@126.com
* @create 2020 16:41
*/
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("end...");
}

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.atguigu.java;

/**
* -Xms20m -Xmx20m
* @author shkstart shkstart@126.com
* @create 2020 16:42
*/
public class HeapDemo1 {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("end...");
}
}

image-20210309223041066

image-20210309223058192

堆空间关于对象创建和Gc的概述

  • Java虚拟机规范 中对Java堆的描述是: 所有的对象实例以及数组都应当在运行时分配在堆上

    (还有可能在栈上分配)

  • 数组和对象可能永远不会在存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置

  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会移除

  • 堆,是GC执行垃圾回收的重点区域

堆的细分内存结构

image-20200617161625770

image-20200617161829115

image-20200617161947360

设置堆内存大小与oom

  • Java堆区用于存储Java对象实例,那么堆的大小在JVM启动的时候就已经设定好了,大家可以通过选项”-Xmx”(堆区的最大内存)和”-Xms”(起始内存)来设置

  • 一旦堆中的内存大小超过”-Xmx”所指定的最大内存时,将会抛出OutOfMemoryErrory异常

  • 通常会将-Xms和-Xmx两个参数配置同样的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能.

    在服务器 不断扩容 释放 造成系统压力 避免GC之后造成性能额外压力

  • 默认情况下,初始内存大小:物理电脑内存大小/64

    最大内存大小:物理电脑内存大小/4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.atguigu.java;

/**
* 1. 设置堆空间大小的参数
* -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
* -X 是jvm的运行参数
* ms 是memory start
* -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
*
* 2. 默认堆空间的大小
* 初始内存大小:物理电脑内存大小 / 64
* 最大内存大小:物理电脑内存大小 / 4
* 3. 手动设置:-Xms600m -Xmx600m
* 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
*
* 4. 查看设置的参数:方式一: jps / jstat -gc 进程id
* 方式二:-XX:+PrintGCDetails
* @author shkstart shkstart@126.com
* @create 2020 20:15
*/
public class HeapSpaceInitial {
public static void main(String[] args) {

//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");

// System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
// System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");

try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

OOM举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* -Xms600m -Xmx600m
* @author shkstart shkstart@126.com
* @create 2020 21:12
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}

class Picture{
private byte[] pixels;

public Picture(int length) {
this.pixels = new byte[length];
}
}

新生代和老年代

image-20200618082817163

image-20200618083215960

1
2
3
4
* -XX:NewRatio : 设置新生代与老年代的比例。默认值是2.
* -XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8
* -XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略 (暂时用不到)
* -Xmn:设置新生代的空间的大小。 (一般不设置)
  • 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
  • 当然开发人员可以通过”-XX:SurvivorRatio”调整这个空间比例
  • 几乎所有的对象都是在Eden区被New出来
  • 绝大部分的Java对象的销毁都在新生代进行,新生代中80%的对象都是”朝生夕死”的
  • 可以使用选项”-Xmn”设置新生代最大内存大小

图解对象分配过程

image-20200618084701871

总结:

  • 针对幸存者s0,s1区的总结: 复制之后有交换,谁空谁是to
  • 关于垃圾回收: 频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集

image-20200618090459971

特殊情况

常用 调优工具

image-20200618092448669

MinorGC MajorGC FullGC

调优: 主要减少GC时间 重点关注 MajorGC和FullGC

image-20200618093219449

image-20200618093738276

image-20200618094452939

image-20200618094650191

堆空间分代思想

为什么需要把Java分代?不分代就不能正常工作吗?

  • 经研究,不同对象的生命周期不同.70%-99%的对象都是临时对象
    • 新生代:有Eden,两块大小相同的Survivor(又称为from/to,s0/s1)构成
    • 老年代:存放新生代中经历多次GC仍然存活的对象

image-20200618095658040

内存分配策略

如果对象在Eden出生并且经过第一次的MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor区中每熬过一次MinorGC,年龄就增加一岁,当它的年龄增加到一定程度(15岁,其实每个Jvm每个GC都有不同.)就会被晋升到老年代

针对不同年龄段的对象分配原则如下所示:

  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
  • 空间分配担保
    • -XX:HandlePromtionFailure

TLAB

image-20200618100839356

image-20200618100944672

什么是TLAB?

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间

  • 多个线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题

    同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略

  • 据知所有OpenJDK衍生出来的JVM都提供了TLAB的设计

TLAB的再说明

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
  • 在程序中,开发人员可以通过选项”-XX:UseTLAB“设置是否开启TLAB空间
  • 默认情况下,TLAB空间的内存非常小,仅占整个Eden空间的1%,当然我们可以通过选项”-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存

image-20200618101307566

参数设置总结

  • -XX: +PrintFLagsInitial: 查看所有参数的默认初始值
  • -XX:+PrintFlagsFinal: 查看所有参数的最终值(可能会存在修改,不再是初始值)
  • -Xms: 初始堆空间内存(默认为物理内存的1/64)
  • -Xmx: 最大堆空间内存(默认为物理内存的1/4)
  • -Xmn: 设置新生代的大小.(初始值及最大值)
  • -XX: NewRatio: 配置新生代与老年代在堆结构的占比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 测试堆空间常用的jvm参数:
* -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
* -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
* 具体查看某个参数的指令: jps:查看当前运行中的进程
* jinfo -flag SurvivorRatio 进程id
*
* -Xms:初始堆空间内存 (默认为物理内存的1/64)
* -Xmx:最大堆空间内存(默认为物理内存的1/4)
* -Xmn:设置新生代的大小。(初始值及最大值)
* -XX:NewRatio:配置新生代与老年代在堆结构的占比
* -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
* -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
* -XX:+PrintGCDetails:输出详细的GC处理日志
* 打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
* -XX:HandlePromotionFailure:是否设置空间分配担保
**/

在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间

  • 如果大于,则此次MinorGC是安全的

  • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败

    • 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小

      如果大于,尝试进行一次MinorGC 但这次依旧是有风险的

      如果小于,则改为进行一次Full GC

    • 如果HandlePromotionFailure=false,则改为进行一次Full GC.

在JDK6 update24之后,HandlePromotionFailure参数不会再影响对虚拟机的空间分配担保策略.观察OpenJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数.但是在代码中已经不会再使用它.JDK6 update

24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC 否则将进行Full GC

堆是分配对象存储的唯一选择吗?

在<深入理解Java虚拟机>中关于Java堆内存有这样一段描述:

随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象分配到堆上也渐渐变得不那么绝对了

如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么可能就被优化成栈上分配.这样就无需在堆上分配内存,无需进行垃圾回收

基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH技术实现off-heap将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到GC的回收频率和提升GC的回收效率的目的

逃逸分析

  • 如何将堆上的对象分配到栈,需要使用逃逸分析手段
  • 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
  • 通过逃逸分析.Java HotSpot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
  • 逃逸分析的基本行为就是分析对象动态作用域
    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
    • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸.例如作为调用参数传递到其他地方

image-20200618120032116

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.atguigu.java2;

/**
* 逃逸分析
*
* 如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用。
* @author shkstart
* @create 2020 下午 4:00
*/
public class EscapeAnalysis {

public EscapeAnalysis obj;

/*
方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
//思考:如果当前的obj引用声明为static的?仍然会发生逃逸。

/*
对象的作用域仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
//getInstance().xxx()同样会发生逃逸
}
}

image-20200618121011811

结论:能使用局部变量的 就不要在方法外定义

代码优化

使用逃逸分析,编程器可以对代码做如下优化:

  1. 栈上分配.将堆分配转化为栈分配.如果一个对象在子程序中被分配,要使指向对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
  2. 同步省略.如果一个对象被发现只能从一个线程中被访问到,那么对于这个对象的操作可以不考虑同步
  3. 分离对象或标量替换.有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器

栈上分配

  • JIT编译器在编译期间根据逃逸分析的结果,如果发现一个对象并没有逃逸出方法的话,就可能被优化成栈上分配.分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收.这样就无需进行垃圾回收了
  • 常见的栈上分配的场景
    • 在逃逸分析中,已经说明了.分别是给成员变量赋值,方法返回值,实例引用传递

同步省略

  • 线程同步的代价是相当高的,同步的后果是降低并发性和性能

  • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程

    如果没有 那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步.这样就能大大提高并发性和性能.这个取消同步的过程就叫同步省略,也叫锁消除

image-20200618122236441

image-20200618122842342

image-20200618122550788

image-20200619105557653

image-20200619105645796

image-20200619105949024

方法区

方法区概述 栈堆交互关系

image-20200619110355148

image-20200619110633507

方法区的基本理解

image-20200619111638240

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误
  • 关闭JVM就会释放这个区域的内存

HotSpot中方法区的演进

  • 在JDK7及以前,习惯上把方法区,称为永久代. JDK8开始,使用元空间取代了永久代
  • 本质上,方法区和永久代并不等价.仅仅是对hotspot而言.Java虚拟机规范对如何实现方法区不做统一要求.
    • 现在看来 当年使用永久代,不是好的idea.导致Java程序更容易OOM(超过-XX: MaxPermSize上限)

image-20200619112803981

image-20200619112853469

设置方法区大小和OOM

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

JDK7及以前:

  • 通过-XX:PermSize来设置永久代初始分配空间.默认值是20.75M
  • -XX:MaxPermSize来设定永久代最大可分配空间.32位默认是64M,64位机器模式是82M
  • 当JVM加载的类信息容量超过了这个值,会报异常OOM: PermGenspace

JDK8及之后

  • 元数据区大小可以通过参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定

    替代上述原有的两个参数

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

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

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

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

image-20200619114303568

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* jdk6/7中:
* -XX:PermSize=10m -XX:MaxPermSize=10m
*
* jdk8中:
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* @author shkstart shkstart@126.com
* @create 2020 22:24
*/
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 10000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}

image-20200619114815785

方法区内部结构

image-20200619123231454

image-20200619123325138

image-20200619123453000

image-20200619123616991

image-20200619123700588

image-20200619124732213

image-20200619124856057

常量池

image-20200619125309486

image-20200619125741154

image-20200619125818874

小结:

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

运行时常量池

image-20200619130811352

方法区的演进

image-20200619195205923

image-20200620085159200

image-20200620090655155

image-20200620090839096

image-20200620090905259

StringTable为什么要调整位置

JDK7 将Stringtable放到了堆空间中 因为永久代的回收效率很低 在full gc的时候才会触发 而 full gc 是老年代的空间不足 永久代不足才会触发 这就导致 StringTable回收效率不高 而我们开发中会有大量的字符串被创建 回收效率低 导致永久带内存不足 放到堆里能即时回收内存

静态变量放在哪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 结论:
* 静态引用对应的对象实体始终都存在堆空间
*
* jdk7:
* -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
* jdk 8:
* -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
* @author shkstart shkstart@126.com
* @create 2020 21:20
*/
public class StaticFieldTest {
private static byte[] arr = new byte[1024 * 1024 * 100];//100MB

public static void main(String[] args) {
System.out.println(StaticFieldTest.arr);

// try {
// Thread.sleep(1000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 《深入理解Java虚拟机》中的案例:
* staticObj、instanceObj、localObj存放在哪里?
* @author shkstart shkstart@126.com
* @create 2020 11:39
*/
public class StaticObjTest {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();

void foo() {
ObjectHolder localObj = new ObjectHolder();
System.out.println("done");
}
}

private static class ObjectHolder {
}

public static void main(String[] args) {
Test test = new StaticObjTest.Test();
test.foo();
}
}

方法区的垃圾回收

image-20200620093255523

常量池中废弃的常量不再使用的类型

image-20200620093933766

image-20200620093958148

运行时数据区的总结 常见大厂面试题

image-20200620094726780

image-20200620094758266