学懂Java的内存模型才有底气去面试

1.程序计数器

1.1定义

Program Counter Register 程序计数器(寄存器)

程序计数器是用于存放下一条指令所在单元的地址的地方。

当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。

特点
(1) 是线程私有的
(2) 不会存在内存溢出
(3)如果正在执行的是Native 方法,则这个计数器值为空
(4)程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,他的生命周期随着线程的创建而创建随着线程的结束而死亡。

1.2作用

在这里插入图片描述

多线程的Java应用程序:为了让每个线程正常工作就提出了程序计数器(Programe Counter Register),每个线程都有自己的程序计数器这样当线程执行切换的时候就可以在上次执行的基础上继续执行,仅仅从一条线程线性执行的角度而言,代码是一条一条的往下执行的,这个时候就是程序计数器;

JVM就是通过读取程序计数器的值来决定下一条需要执行的字节码指令,进而进行选择语句、循环、异常处理等;

2.虚拟机栈

在这里插入图片描述

2.1定义

Java Virtual Machine Stacks (Java 虚拟机栈)

虚拟机栈也称为Java栈,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)

特点

(1)Java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
(2)虚拟机栈说明了线程运行时的瞬时状态
(3)每次方法调用,都会产生对应的栈帧
(4)每个方法被调用至执行完毕的过程,就对应这个栈帧在虚拟机栈中从入栈到出栈的完整过程
(5)栈的深度是有限制的,所以会存在栈溢出的问题
(6)Java虚拟机栈会出现两种异常,StackOverFlowError 和 OutOfMemoryError
若 Java的虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度大于虚拟机栈的最大深度就会出现StackOverFlowError
若虚拟机栈的内存大小允许动态扩展,当线程请求栈的内存用完是,再无法动态的进行扩展时,OutOfMemoryError。

Java虚拟机栈的结构
在这里插入图片描述扩展:那么方法和函数如何调用?
Java的栈可以类比数据结构中的栈,Java中主要保存的内容是栈帧,每一次方法的调用都会对应有一个栈帧入栈,调用结束,都会有一个栈帧出栈,
java的两种返回方式:
1.return语句
2.抛出异常
问题辨析

  1. 垃圾回收是否涉及栈内存?
    垃圾回收不涉及栈内存
  2. 栈内存分配越大越好吗?
    不是,因为每一个线程都对应一个栈,栈分配的空间越大,相对应多线程的执行就会受到影响.
  3. 方法内的局部变量是否线程安全?
    如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

2.2栈内存溢出

package cn.itcast.jvm.t1.stack;
/**
 * 演示栈内存溢出 java.lang.StackOverflowError
 * -Xss256k
 */
public class Demo1_2 {
    private static int count;
    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }
    private static void method1() {
        count++;
        method1();
    }
}

在这里插入图片描述栈溢出一般存在两种情况

(1) 栈帧过多导致栈内存溢出
(2)栈帧过大导致栈内存溢出

2.3线程运行诊断

cup占用时间过长

public class Demo1_16 {
    public static void main(String[] args) {
        new Thread(null, () -> {
            System.out.println("1...");
            while(true) {

            }
        }, "thread1").start();


        new Thread(null, () -> {
            System.out.println("2...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread2").start();

        new Thread(null, () -> {
            System.out.println("3...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread3").start();
    }
}

(1)在linux环境下运行代码 然后使用top命令查看进程的CPU使用情况
(2)ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
(3) jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

线程死锁

package com.lf.jvm.stack;
//线程死锁问题
class A{};
class B{};
public class Stack02 {
    static A a = new A();
    static B b = new B();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
    }
}

使用jconsole调试工具
在这里插入图片描述

3本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务(Native方法主要底层使用的是C++或者C语言实现的)。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
在这里插入图片描述

4堆

4.1定义

Java1.6 和Java1.8 中堆不一样,主要体现在在Java1.8中使用了元空间替换了永久区
在这里插入图片描述

堆内存用来存放new创建的对象和数组。 堆内存中所有的实体都有内存地址值。 堆内存中的实体是用来封装数据的,这些数据都有默认初始化值。
堆内存中的实体不再被指向时,JVM启动垃圾回收机制,自动清除,这也是JAVA优于C++的表现之一

4.2堆内存溢出

在这里插入图片描述

java.lang.OutOfMemoryError: Java heap space
设置堆的参数 :-Xmx8m

package cn.itcast.jvm.t1.heap;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Demo1_5 {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

以下错误就是堆内存溢出
在这里插入图片描述

4.3堆内存诊断

  1. jps 工具
    查看当前系统中有哪些 java 进程
    在这里插入图片描述

  2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id

  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测

5元空间(方法区)

5.1定义

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
在这里插入图片描述

二、方法区特点
1.方法区是线程共享的,多个线程都用到一个类的时候,若这个类还未被加载,应该只有一个线程去加载类,其他线程等待;

2.方法区的大小可以是非固定的,jvm可以根据应用需要动态调整,jvm也支持用户和程序指定方法区的初始大小;

3.方法区有垃圾回收机制,一些类不再被使用则变为垃圾,需要进行垃圾清理。

4.方法区(Method Area) 与Java堆一样,是各个线程共享的内存区域。

5.方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

6.方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:
java.lang.OutOfMemoryError:PermGen space(JDK7及之前)或者java.lang.OutOfMemoryError: Metaspace(JDK8及之后)

三、方法区存放的内容
在这里插入图片描述

《深入理解JVM》书中描述如下:
它存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
在这里插入图片描述类型信息
对每个加载的类型( 类class、接口interface、枚举enum、注annotation),JVM必须在方法区中存储以下类型信息:
①这个类型的完整有效名称(全名=包名.类名)
②这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
③这个类型的修饰符(public, abstract, final的某个子集)
④这个类型直接接口的一个有序列表

域(Field)信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public, private,protected, static, final, volatile, transient的某个子集)

方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型(或void)
方法参数的数量和类型(按顺序)
方法的修饰符(public, private, protected, static, final,synchronized,
native, abstract的一个子集)
方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
异常表( abstract和native方法除外)

方法区常用的参数:
-XX:MetaspaceSize=N //设置Me他space的初始
-XX:MaxMetaspaceSize = N // 设置Metaspace的最大大小

JDK1.8,方法区被彻底移除,取而代之的是元空间,元空间使用的是直接内存

为什么要将永久代替换为元空间?
在这里插入图片描述如图所示,永久代的方法区和堆使用的物理内存是连续的。

永久代的大小配置

-XX:PermSize:设置永久代的初始大小。
-XX:MaxPermSize:设置永久代的最大值,默认是64M。

对于永久代,如果动态生成很多class的时候,很有可能出现java.lang.OutOfMemoryError:PermGen space错误,这是因为永久代空间配置的大小有限。在典型的单一应用中,需要编写和加载很多的jsp页面,就会出现java.lang.OutOfMemoryError。在JDK1.8版本之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上,机器内存有多大,元空间就有多大。

综上所述,表面上是为了避免OOM异常,因为通常使用PermSize和MaxPermSize设置了永久代的大小上限,但是不是总能设置到刚刚合适的大小,而使用默认值是很容易遇到OOM错误。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制,而是由系统的实际可用空间来控制。

常量池
Java中的常量池分为三种类型:

类文件中常量池(The Constant Pool)
运行时常量池(The Run-Time Constant Pool)
String常量池

类文件中常量池 ---- 存在于Class文件中

所处区域:堆

诞生时间:编译时

内容概要:符号引用和字面量

class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。

常量池中存放的是符号信息,java虚拟机在执行指令的时候会依赖这些信息。

运行时常量池 ---- 存在于内存的元空间中

诞生时间:JVM运行时

内容概要:class文件元信息描述,编译后的代码数据,引用类型数据,类文件常量池。

所谓的运行时常量池其实就是将编译后的类信息放入运行时的一个区域中,用来动态获取类信息。

运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

字符串常量池 ---- 存在于堆中

从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。

字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

6直接内存

6.1 定义

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值