JVM—内存模型

1.什么是JVM

是一个通过在实际的计算机上模拟各种计算机功能的虚拟计算机,也是JAVA语言“Write Once ,Run Anywhere~”的核心技术。

1.1 JDK、JRE和JVM 三者是什么关系?

官方图

image-20210618090450641

很显然,JDK是JRE的超集,除了包含JRE以外,还包含一些编译调试程序和应用的工具。(包括java、javac、JAVA API)

JRE是JVM的超集,包含JVM,并且还包含一些JAVA核心类库、运行程序和应用的其他组件。(JAVA SE API子集 + JVM)

JVM主要的工作是解释字节码指令,并映射到本地的CPU指令集和OS的系统调用,使之与系统无关,实现跨平台。(跨平台的核心)

1.2 JVM架构图

1.3 JVM 内存模型

在这里插入图片描述

程序计数器

线程私有

是一块较小的内存空间。

当前线程所执行的字节码的行号指数器(取指,执行)。

它指定下一条待执行的指令是哪个。

Java虚拟机栈

线程私有(生命周期与栈同步,线程结束,栈内存也就释放了,所以不存在垃圾回收的问题

用来描述Java方法执行的线程内存模型,也就是通常所讲的

每个方法被执行,就创建一个栈帧(Stack Frame,是栈里的元素),用来存储:

  • 局部变量表
    • 基本数据类型(boolean、byte、char、short、int、long、float、double)
    • 对象引用(只是引用,类似于指针,但不是对象本身)
  • 操作数栈
  • 动态连接
  • 方法出口(returnAddress,方法会压栈出栈,肯定要记录出口)
  • 等。。。

image-20210704120212644

Java 虚拟机栈会出现两种错误:StackOverFlowErrorOutOfMemoryError

  • StackOverFlowError 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

本地方法栈

线程私有

类似于上面的Java虚拟机栈,但是是为了虚拟机使用到的本地(Native)方法服务

(虚拟机栈为虚拟机执行Java方法服务,也就是字节码)

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。

native关键字

说明Java 的作用范围达不到了,回去调用底层C语言的库了。

进入本地方法栈,调用本地方法接口 JNI。

JNI作用:扩展Java 的使用范围,融合不同的编程语言为Java所用。

在内存中开辟一块空间,即本地方法栈,在这里登记native方法,在最终执行的时候,通过JNI加载本地方法库中的方法

Java堆

也称GC堆,虚拟机所管理的最大的一块线程共享的内存区域

目的就一个:存放实例对象

堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  1. OutOfMemoryError: GC Overhead Limit Exceeded : 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space 错误。(和本机物理内存无关,和你配置的内存大小有关!)

方法区

线程共享

用于存储已被虚拟机加载的:

  • 类型信息(构造方法、接口定义) Class
  • 静态变量 static
  • 常量 final
  • 运行时常量池
  • 即时编译器编译后的代码缓存等数据

别名:非堆(Non-Heap)

里面存的很少被GC回收,之前常被人称为永久代(JDK8后成为元空间)(并不等同)。

方法区是《Java虚拟机规范》里规定的东西,是接口,并没有规定具体怎么实现

而永久代是HotSpot的具体实现,是实现类

元空间

在JDK8之后,元空间取代的是永久代,而不是方法区

使用的是本地内存,不是JVM的内存(脱缰的野马

**好处:**不再受JVM的内存的限制,受本机可用内存的限制,可以能溢出,但几率小多了。

**注意:**在JDK1.8中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will always be your father",变动的只是方法区中内容的物理存放位置。

正如上面所说,类型信息(元数据信息)等其他信息被移动到了元空间中;

但是运行时常量池和字符串常量池被移动到了堆中。

但是不论它们物理上如何存放,逻辑上还是属于方法区的。

  1. JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代
  3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
  1. JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代
  3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)

2. Java对象揭秘

如何创建对象

new,是没错,细节呢?

  1. 类加载检查

    检查类是否被加载、解析、初始化过,没有就加载类

  2. 分配内存

    两种分配方式,根据Java堆是否规整决定(整整齐齐,还是乱七八糟)

    • 指针碰撞

      整整齐齐,挪动空闲与非空闲内存中间的指针就行了。

    • 空闲列表

      乱七八糟,那只能维护一个列表,来标明哪些空闲,哪些被占用。

  3. 初始化内存空间(所有字段置为0)

  4. 设置对象头

    • 是哪个类的实例?如何找到类的元数据信息?对象的哈希码?对象的GC分代年龄等信息(对象头部信息)
  5. 执行构造函数

    new指令后,会接着执行<init>()方法

对象的内存布局

包含三部分:

  • 对象头

    • 对象自身运行时的数据

      哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

    • 类型指针

      确定该对象 是哪个类的实例

    • 数组长度(如果是个数组对象)

  • 实例数据

    对象存储的真正有效信息,父类变量优先于子类

  • 对齐填充

    对象起始地址默认是8个字节的整数倍,所以对象本身的大小也要是8字节的整数倍。

    对象头部是8字节的整数倍,若实例数据没有对其,就通过对其对齐来补齐它。

对象的定位访问

通过上的reference来操作上的具体对象。

主流的方式:

  • 使用句柄(间接访问实例数据)

    好处:稳定(reference始终不变)

    reference只存稳定的句柄地址,在对象移动时,只需要修改实例数据指针,reference不需要被修改

  • 直接访问(直接访问实例数据)

    好处:速度快(定位|查找)

    省去了一次指针定位的时间开销。

1624499346578

3. 内存泄漏

3.1 什么是内存泄漏

不再会被使用的对象的内存却因为某种原因,不能被回收,就是内存泄露。

  1. 首先,这些对象是可达的,即在可达性分析的过程中,有通路的
  2. 其次,这些对象是无用的,程序以后再也不会使用这些对象了

所以就是,没用,还没法被回收。

3.2 例子

(1) 新生代的引用挂在老年代上

比如说单例模式。

新生代本来可能很快就要被回收的,但是依靠着老年代,一直存活了下来,但是它本身就没啥用,就被搁置了。

public class Simple {
    Object object;
    public void method1(){
        object = new Object();
        //...其他代码
    }
}
如何解决:
  • 缩小作用域
  • 养成对失效对象的引用赋值为null 的好习惯。
(2) 容器使用时的内存泄漏

一个对象被放在了一个容器里,这的对象本身以后可能不会再被使用到了,但是容器在一直被使用,就可能导致内存泄漏。

void method(){
    Vector vector = new Vector();
    for (int i = 1; i<100; i++){
        Object object = new Object();
        vector.add(object);
        object = null;
    }
    //...对vector的操作
    //...与vector无关的其他操作
}
如何解决:

如果不用容器了,就把容器的引用赋值为null

void method(){
    Vector vector = new Vector();
    for (int i = 1; i<100; i++){
        Object object = new Object();
        vector.add(object);
        object = null;
    }
    //...对v的操作
    vector = null;
    //...与v无关的其他操作
}
(3) 各种提供close()方法的对象

我们用完之后要显示调用close()方法,才能释放资源。

但是万一操作的过程中,发生了异常,没有正常的close()释放资源,就会导致内存的泄漏。

读写流,Hibernate使用时创建的session

Session session=sessionFactory.openSession();
session.close();
如何解决

用try catch finally包起来

try{
    session=sessionFactory.openSession();
    //...其他操作
}finally{
    session.close();
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值