1. - 本文以 hotspot 虚拟机
  2. - jvm是一个内存中的虚拟机,也就代表着它的存储就是内存,我们所写的类、常量、方法都在内存中

1. 引言

1.1 java是如何实现平台无关性

虚拟机(JVM) - 图1

  1. 程序员编写一个hello.java文件
  2. 使用javac hello.java命令开启java编译器进行编译,生成一个hello.class文件
  3. 使用java hello启动虚拟机运行程序
  4. 虚拟机首先会将字节码加载到内存中,这个过程称为类加载,是由类加载器(class loader)完成
  5. 然后虚拟机针对加载到内存中的字节码进行解析,比如实例化对象……

    2. Jvm的构造

    虚拟机(JVM) - 图2

    2.1 类加载器(class loader)

    2.1.1 作用

    主要是从系统外部获取特定格式的字节码文件,将其转换为二进制数据流,加载进入系统,交给虚拟机进行连接,初始化等操作,因为虚拟机就在内存中所以,类加载器也可以说将字节码文件加载到内存中

    2.1.2 种类

    对用户不可见,想看的话去看jvm的源码

  6. BootStrapClassLoader: C++编写,加载核心类库java.*

  7. ExtensionClassLoader: java编写,加载扩展库javax.*,自定义jar
  8. AppClassLoader: java编写,加载程序所在目录,加载classPath,即环境变量
  9. CustomClassLoader: java编写,自定义class load定制化加载

    2.1.3 双亲委派机制

    image.png

  10. 双亲委派机制(上图↑)

    1. 自底向上的检查类是否已经加载,若发现没有加载
    2. 自顶向下的尝试加载类
  11. 该加载机制的优势
    1. 避免多份同样的字节码文件被夹在,浪费内存
  12. 源码解析

    1. // 注意: 一些不重要的代码已经删除,想看直接来看源码
    2. protected Class<?> loadClass(String name, boolean resolve) throws
    3. ClassNotFoundException {
    4. // 需要用同步锁,防止在高并发下多个线程同时加载
    5. synchronized (getClassLoadingLock(name)) {
    6. // 首先检查类是否已经加载,如果已经加载,因为resolve默认是false直接返回加载的类
    7. Class<?> c = findLoadedClass(name);
    8. // 如果类没有加载,就开始下面的骚操作
    9. if (c == null) {
    10. // 这里就是一层一层的向上询问,父类加载器是否加载过该类
    11. // 最后因为BootstrapClassloader是c++写的需要调用native的方法
    12. if (parent != null) {
    13. c = parent.loadClass(name, false);
    14. } else {
    15. c = findBootstrapClassOrNull(name);
    16. }
    17. // 检查完4层的类加载器,发现类还是没有被加载过,则尝试从上到下不断尝试加载该类
    18. if (c == null) {
    19. c = findClass(name);
    20. }
    21. }
    22. return c;
    23. }
    24. }

    2.2 运行时区域(runtime data area)

    运行时区域这个就是**Jvm**的内存结构模型

2.2.1 线程问题

image.png

  1. 线程私有: 程序计数器、虚拟机栈、本地方法栈
  2. 线程共享: 元空间也就是方法区、堆

    2.2.2 程序计数器

  3. 记录当前线程所执行的字节码文件的行号指示器,改变计数器的值选取下一行所需要执行的指令

  4. 如果是执行java方法则是计数能力,如果是native的方法此时的值是Undefined
  5. 和线程是一对一的关系,所以“线程私有”
  6. 由于是逻辑计数,不会发生内存泄漏

    2.2.3 虚拟机栈

  7. java方法执行的内存模型

  8. 包含多个栈帧,每个方法被执行的时候都会创建一个栈帧,即方法运行期间的基础数据结构,当方法调用结束的时候帧才会被销毁
  9. 栈帧存储的信息:局部变量表、操作栈、动态连接、返回地址
  10. 局部变量表:包含方法执行过程中的所有变量
  11. 操作栈:入栈、出栈、复制、交换、产生消费变量

    问题一:递归为什么会引起**java.lang.stackOverFlowError**,栈溢出?

    1. 每创建一个方法就会产生一个栈帧,系统会在该方法执行完成之后销毁栈帧
    2. 每个线程的虚拟机栈深度是有限的
    3. 递归调用会不断的产生栈帧,并且不会销毁,所以造成栈溢出

    问题二:虚拟机栈过多,会引起**java.lang.OutOfMemoryError**,内存溢出?

    1. 虚拟机栈会进行动态扩展内存,如果未能申请到扩展内存,便会报出上面一错误

2.2.4 本地方法栈

与虚拟机栈类似,主要作用于标注了native的方法

2.2.5 方法区

  1. 方法区只是JVM的规范,它的实现是元空间(mate space)jdk7以前叫做永久代(PermGen)
  2. 两者都是用来存储class的相关信息,包含class对象的method
  3. jdk7之前字符串常量池位方法区jdk8之后移动到了堆内存

    问题三:**Mate Space**相较于**PermGen**的优势?(核心是**mate space**使用本地内存产生的好处)

    1. **Mate Space**使用本地内存;**PermGen**使用的**jvm**的内存
    2. 字符串常量池存在**PermGen**中容易出现性能问题和内存溢出
    3. 类和方法的信息大小不确定,给永久代大小指定带来困难,太小容易永久代溢出,太大容易导致老年代溢出
    4. **PermGen**会为**GC**带来不必要的复杂性,并且回收效率偏低,是指永久代不足的时候会触发**Full GC**

2.2.6 堆

  1. 对象实例的分配区域,并且堆内存是可扩展的,物理地址可以是不连续的,逻辑是连续的即可,如果堆内存申请扩展没有收到相应的内存,会抛出java.lang.OutOfMemoryError
  2. GC管理的区域,有时候也称为GC heap

    2.3 执行引擎(execution engine)

    负责对class load加载到内存中的命令进行解析,解析完成提交到操作系统执行

    2.4 本地接口(native interface)

    融合不同语言的原生开发库为java所用,比如“Class.forName()”,锁底层实现CAS

    一些问题

    类装载的过程

  3. 加载: 通过classLoader加载class字节码文件,生成class对象

  4. 链接
    a. 校验: 检查加载的class的正确性和安全性
    b. 准备: 为类变量分配存储空间,并设置类变量的初始值
    c. 解析: jvm将常量池内的符号引用转换为直接引用
  5. 执行: 执行类变量赋值和静态代码块

    JVM三大内存调优参数的意义-Xms -Xmx -Xss

    1. 项目启动的时候指定内存大小:java -Xms128m -Xmx128m -Xss256k -jar xxx.jar
    2. Xms: 规定堆的初始大小,堆如果不够会自动扩容知道Xmx规定的大小
    3. Xmx: 规定heap能达到的最大值
    4. Xss: 规定了每个线程虚拟机栈(堆栈)的大小,会直接影响并发线程数的大小

      内存分配策略

  6. 静态存储: 编译时确定每个数据目标在运行的的存储空间需求

  7. 栈式存储: 数据需求在编译时未知,运行时模块入口前确定
  8. 堆式存储: 编译时或者运行时模块入口都无法确认,动态分配

    内存模型中堆和栈的区别

  9. 联系:引用对象、数组时,栈里面定义变量保存堆中目标的首地址;当他们没有引用对象指向的时候,在一个不确定的时间被GC回收

  10. 管理方式:栈自动释放,堆需要GC
  11. 空间大小:栈比堆小
  12. 碎片相关:栈产生的碎片远小于堆
  13. 分配方式:栈能支持静态分配和动态分配,堆只支持动态分配
  14. 效率:栈的效率比堆高很多

image.png

元空间、堆、线程独占部分之间的联系-内存角度

  1. 翔仔面试视频6-119:36
  2. 执行main方法之内存中的变化,看图即可

image.png
image.png