总览

面试相关难题

  1. Java的内存分成哪几部分 详细介绍一下
  2. Java内存中哪些部分会溢出?
  3. JDK7和8在内存结构上的区别是什么?

内存调优:

程序计数器

程序计数器(Program Conter Register) 也叫PC寄存器 每个线程会通过程序计数器记录当前要执行的字节码指令的地址

  1. 在加载阶段 虚拟机将字节码文件中的指令读取到内存之后 会将原文件中的偏移量转换成内存地址 每一条字节码指令都会拥有一个内存地址
  2. 在代码执行过程中 程序计数器会记录下一行字节码指令的地址 执行完当前指令之后 虚拟机的执行引擎根据程序计数器执行下一行指令
  3. 程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑
  4. 在多线程执行情况下 Java虚拟机需要通过程序计数器记录CPU切换前解释执行到哪一句指令并继续解释运行

问题: 程序计数器在运行中会出现内存溢出吗?
内存溢出:指的是程序在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限
不会
因为每个线程只存储一个固定长度的内存地址 程序计数器是不会发生内存溢出的(程序员无需对程序计数器做任何处理 程序计数器完全由java虚拟机控制)

Java虚拟机栈 本地方法栈

Java虚拟机栈

  1. Java虚拟机栈 采用栈的数据结构来管理方法调用中的基本数据 每一个方法的调用使用一个栈帧来保存
  2. Java虚拟机栈随着线程的创建而创建 而回收则会在线程的销毁时进行 由于方法可能会在不同线程中执行 每个线程都会包含一个自己的虚拟机栈

栈帧的组成

局部变量表

作用是在运行过程中存放所有的局部变量 -> 编译成字节码文件时就可以确定局部变量表的内容

  1. 栈帧中的局部变量表是一个数组 数组每一个位置称之为槽(slot) long和double类型占用两个槽 其他类型占一个槽
  2. 实例方法中的序号为0的位置存放的是this(即当前实例对象的引用) 指的是当前调用方法的对象 运行时会在内存中存放实例对象的地址
  3. 方法参数也会保存在局部变量表中 其顺序与方法中参数定义的顺序一致
  4. 局部变量表保存的内容由:实例方法的this对象 方法的参数 方法体中声明的局部变量

问题:

(答案并不是9 (9 的算法: this 1个 6个int 1个long))

为了节省空间 局部变量表中的槽是可以复用的 一旦某个局部变量不再生效 当前槽就可以再次被使用

操作数栈

是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域 他是一种栈式的数据结构 如果一条指令将一个值压入操作数栈 则后面的指令可以弹出并使用改值
编译器就可以确定操作数栈的最大深度 从而执行时正确的分配内存大小

帧数据

帧数据主要包含动态链接、方法出口、异常表的引用

  1. 动态链接
    当前类的字节码指令引用了其他类的属性或者方法时 需要将符号引用(编号) 转换成对应的运行时常量池中的内存地址 动态链接就保存了编号到运行时常量池的内存地址的映射关系

  2. 方法出口
    方法出口指的是方法在正确或异常结束时 当前栈帧会被弹出 同时程序计数器应该指向上一个栈帧中的下一条指令的地址 所以在当前栈中 需要存储此方法出口的地址

  3. 异常表
    异常表存放的是异常的处理信息 包含了try代码块和catch代码块执行后跳转到的字节码指令位置

栈-内存溢出

Java虚拟机栈如果栈帧过多 占用内存超过栈内存可以分配的最大大小就会出现内存溢出
Java虚拟机栈内存溢出时会出现StackOverflowError的错误

Java虚拟机栈 注意事项

本地方法栈

Java虚拟机栈存储了Java方法调用时的栈帧 而本地方法存储的是native本地方法(用C++来写)的栈帧
在Hotspot虚拟机中 Java虚拟机栈和本地方法栈实现上使用了同一个栈空间 本地方法栈会在栈内存上生成一个栈帧 临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来

堆内存

堆内存存储对象

堆内存也会发生内存溢出 -> OutofMemory错误

堆内存的三个值 used total max
max默认为系统的1/4 total默认为系统的1/64

随着堆中的对象增多 当total可以使用的内存即将不足时 java虚拟机会继续分配内存给堆
total值会变大 total最多只能与max相等

问题:

方法区

方法区是存放基础信息的位置 线程共享 主要包含:

  1. 方法区是用来存储每个类的基本信息(元信息) 一般称之为InstanceKlass对象 在类的加载阶段完成

方法区属于<<JAVA虚拟机规范>>中设计的虚拟概念
JDK7中使用永久代来实现
JDK8中使用元空间来实现

方法区溢出

为元空间分配最大内存大小 防止内存占用率过高

字符串常量池

字符串中常量池存储在代码中定义的常量字符串内容 比如”123” 这个”123”就会被放入字符串常量池

s1 通过new 关键字创建对象 最终会放在堆内存中 (s1是栈中的局部变量 用于表示字符串”123”在堆内存中的地址)
s2 并没有通过new关键字创建对象 s2存放的是字符串常量池中”abc”的地址

即JDK8以后 运行时常量池和字符串常量池就已经不是一个东西了

问题1:

分析:

如上图所示显然是不相等 a b c 中存放的字符串常量池中的地址
但是根据底层代码(字节码指令)所示 d = a + b 在底层实际是使用了new方法进行创建
变量 连接使用的是StringBuilder
因此d中存放的堆内存中的地址 c与d显然是不相等

问题2:

d为常量 编译阶段直接连接

直接内存

总结