ErCargo's Coffee Time

以大多数人的努力程度之低,根本轮不到拼天赋

Action Conquers Fear, Impetuous, Indolence and so on. (行动能够克服一切恐惧,浮躁,懒惰)


Welcome to star and fork my github

Java 内存模型

  • 内存简介
  • JVM 架构图
  • Java 内存模型

内存

计算机程序都是在内存中运行的(包括虚拟内存)

在程序运行过程中, 需要不断的将内存的「逻辑地址」和「物理地址」进行映射,找到相关的【指令】以及【数据】去执行

逻辑地址 -> 分段管理机制 -> 线性地址 -> 分页管理机制 -> 物理地址

Java 程序实际上是一个操作系统进程, 与其他进程一样面临内存限制,即受限于操作系统架构提供的「可寻址地址空间」 (可寻址地址空间由处理器的位数决定(32位/64位))

地址空间划分为:

  • 内核空间(操作系统程序, c 运行时的空间 , 连接计算机硬件,调度程序,提供联网和虚拟内存等服务的逻辑和基于 C 的进程)
  • 用户空间(java 进程实际运行时使用的内存空间)

JVM 架构图

ref: 《Java虚拟机学习笔记》

其中 Runtime Data Area 就是 Java 的内存模型

Java 内存模型

Java 程序运行在虚拟机之上,运行时需要内存空间,虚拟机在执行 Java 程序的过程中会把「他管理的内存」划分不同的数据区域.

「C 编译器」会将内存区域划分为:数据段(包括「堆」「栈」「静态数据区」)和代码段

Q: JDK 8 是如何划分内存空间的?

从线程的角度讨论

线程私有的区域

  1. 程序计数器
  2. 虚拟机栈(Stack)
  3. 本地方法栈(带有 Native 关键字的方法使用的内存空间)
Java 方法执行的内存模型, 每个方法被执行时都会创建一个栈帧(栈帧是方法运行期间的基础数据结构)存放在虚拟机栈中;
「栈帧」用于存储局部变量表,操作数栈, 动态连接,返回地址等
- 当方法被调用时,jvm 会开辟一块空间(会创建一个栈帧);
- 当方法调用结束时,栈帧就会被销毁;(这也是为什么栈的内存不需要通过 GC 回收, 而是自动释放)

栈帧

  • 局部变量表:包括了方法执行过程中的所有变量,this 引用,所有方法参数,其他局部变量

  • 操作数栈: 在执行字节码指令时被用到,类似于原生 CPU 寄存器,大部分 JVM 字节码把时间花费在 操作数栈上,包括入栈,出栈,复制,交换,产生消费变量等

Q: 递归为什么会引起 java.lang.StackOverflow 异常?

package com.ercargo.restart;

/**
 * @author ercargo  on 2020/4/5
 * @DESCRIBE
 * 0,1,1,2,3,5,8,13...
 * F(0) = 0
 * F(1) = 1
 * F(n) = F(n-1) + F(n-2)
 * e.g:
 * F(2) = F(1) + F(0) = 1
 * F(3) = F(2) + F(1) = 2
 */
public class Fibonacci {

    public static int fib(int n) {
        if (n <= 1) {
            return n;
        } else {
            return fib(n - 1) + fib(n - 2);
        }
    }

    public static void main(String[] args) {
        System.out.println(fib(0));
        System.out.println(fib(1));
        System.out.println(fib(2));
        System.out.println(fib(3));
        System.out.println(fib(9));

        System.out.println(fib(10000000));
    }
}

每【执行】一次方法时, 就会创建一个栈帧, 并将栈帧压入虚拟机栈中,方法执行完成时, 会将栈帧出栈 递归过深,方法调用次数过多, 创建的栈帧数就越多, 当栈帧数会超出整个虚拟机栈的深度时, 就会报错:

Exception in thread "main" java.lang.StackOverflowError

解决方法: 限制递归深度/循环替代

另外:虚拟机栈过多也会引发 java.lang.OutOfMemoryError 异常

线程共享的区域

  1. 元空间 MetaSpace(类加载信息)
在 jdk8 以后开始把类的元数据放在本地堆内存中,称为 MetaSpace(在 jdk7 以前是属于永久代的),元空间和永久代都是用来存储 Class 信息,包括 Class 对象的 method ,field 等
元空间和永久代都是方法区(Method Area)的实现,方法区只是一种 JVM 的规范;
- 在 Java 7 之后原先位于【方法区(Method Area)】里的「字符串常量池」被移动到 「Java 堆(Heap)」中, 
- 并且使用元空间替代了永久代(java.lang.OutOfMemoryError: PermGen space 不复存在)
上面这部分内容还是要参照 JVM 架构图来理解,主要是 Runtime Data Area 包含的部分 
  1. Java 堆(数组和类对象)常量池(字面量和符号引用)
对象实例的分配区域, Java 堆可以处于物理上不连续的内存空间中, 只要逻辑上是连续的就可以, 可扩展的
-Xmx  
-Xms
-Xss

GC 管理的主要区域(分代回收算法)
    新生代                              老年代
Eden 「from survivor」「to Survivor」  tenured

从存储的角度讨论

  1. JVM 3 大性能调优参数
  • Xss 规定了每个线程虚拟机栈的大小 (256k)该配置影响进程中并发线程数的大小;
  • Xms 堆的初始值 (该进程刚创建出来时 java 堆的大小,一旦对象容量超过了 java 堆的初始容量, java 堆将会自动扩容, 扩容至 -Xmx 设置的大小)
  • Xmx 堆能扩展到的最大值 (一般 -Xms 和 -Xmx 的值相等, 堆的扩容会发生内存抖动,影响程序运行时的稳定性)
  1. Java 内存模型中「堆」和「栈」的区别:

    程序运行时的内存分配策略:

    • 静态存储: 在编译时确定每个数据目标在运行时的存储空间需求;(不允许可变数据存储,递归等)
    • 栈式存储: 数据去需求在编译时未知,运行时模块入口前确定;
    • 堆式存储: 编译/运行时都不能确定, 动态分配

区别 1: 栈存放对象的地址 堆存放对象实例 区别 2: 栈自动释放内存 堆需要 GC 区别 3: 栈比堆小

内存

  1. 元空间,堆,线程独占部分的联系
  2. intern() 方法
最近的文章

聊聊 GC

– Java 垃圾回收机制– 常见的垃圾收集器– Java 中的各种引用Java 垃圾回收机制对象什么情况下被看作是垃圾?对象不被任何其他对象引用时,就可以被认为是垃圾对象,就可以被内存回收如何判断对象是否为垃圾? 引用计数算法 判断对象的引用数量来决定是否可以被回收,引用计数器为 0 时就可以被回收; 但是不可避免循环引用的问题; 可达性分析算法(主流的 JVM 使用的算法) 判断对象的引用链是否可达, 来决定队形是否...…

继续阅读
更早的文章

Java 类加载之反射机制

Java 反射机制能够动态获取信息、动态调用对象方法,即:在运行过程中: 对任意一个「类」都能知道它的属性和方法; 对任意一个「对象」,都能调用它的任意方法和属性;e.g:public class RobotTest { private String name; public void sayHi(String helloSentence){ System.out.println(helloSentence + "" + name); } priv...…

继续阅读