目录
1.JVM的概念
1.1JVM执行流程
2.JRE/JDK/JVM之间的关系
3.有关JVM的经典问题
3.1 JVM的内存布局
3.1.1内存布局中的异常问题
3.2类加载机制
3.2.1类加载的流程(5个)
3.3类加载机制(双亲委派机制)
3.4 垃圾回收
3.4.1垃圾回收的概念
3.4.2垃圾回收的内存有哪些
3.4.3 如何找到垃圾回收的对象
3.4.4如何回收垃圾
JVM :Java虚拟机( Java Virtual Machine )
虚拟机是:通过软件模拟的具有完整硬件功能的、运行在一个完全隔离环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:
1. VMwave、VirtualBox是通过 软件 模拟物理CPU的指令集,物理系统中有很多寄存器。
2. JVM(解释器)则是通过软件模拟Java字节码的指令集,JVM主要保留了PC寄存器,其他的寄存器都进行了裁剪。
JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键
1.程序执行前,把java代码转换成字节码(class文件),JVM 首先把字节码通过类加载器(ClassLoader)加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部 分的职责与功能
JVM 主要分为以下 4 个部分,来执行 Java 程序:
关系:JDK包含JRE JRE包含JVM
Java运行步骤:java源码—javac编译器—>字节码文件—Java解释器—>机器码文件。
详细可参考文章:深度理解JAVA中的栈、堆、对象、方法区、类和他们之间的关系
JVM 运行时 数据区域也叫内存布局,(分为:堆、栈、程序计数器、方法区)和 Java 内存模型(JMM)完全不同; JVM本质是一个Java进程,JVM启动后就会从操作系统处申请到一块内存
每个线程 都有自己的 栈 和 程序计数器
本地方法栈:JVM内部的方法
虚拟机栈:给上层java代码使用的
(1)如下代码所示:变量t在方法下面,属于局部变量,存在于栈里
(2)如下代码所示:变量t是 成员变量,存在于堆里
(3)如下代码所示:变量t是 静态成员变量(类对象),存在于方法区
(1)堆溢出:
堆用于存储对象实例(成员变量),只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
典型场景:无限递归(堆容量不够)
(2)栈溢出:
Java中的 类加载 是JVM中一个很核心的流程,就是将字节码class文件站换乘JVM中的类对象。
一个类的生命周期如下图(7个):
类加载:.class文件(编译器生成)---->类对象的过程
(1)加载 Loading 阶段,Java虚拟机需完成:
(2) 验证
验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,确保其不会危害虚拟机自身的安全。
(3) 准备
是正式为类中定义的变量(静态变量,static修饰的)分配内存并设置类变量初始值
(4)解析
是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程(也即是:初始化常量)
(5)初始化
Java 虚拟机真正开始执行类中编写的 Java 程序代码,初始化阶段就是执行类构造器方法的过程。(初始化静态变量、static静态代码块的执行,在对象实例化之前完成)
Java 虚拟机的角度,只存在两种不同的类加载器:
JVM是三层架构,类的加载是通过双亲委派模型(类加载器之间的层次关系)来完成 :
这个过程也即是定义三个目录的优先级:标准库>>扩展库>>自定义库
Java_HOME/lib
目录中的类库Java_HOME/lib/ext
目录的类库java.lang.ClassLoader
实现如果一个类加载器接收到类加载的请求(从应用程序类加载器AppClassLoader开始,触发类加载),AppClassLoader把请求委派给父加载器去完成(直至走到Bootstrap,他无法再向上层继续委派,只能在自己的目录下寻找符合的类,若找到就进行加载),若Bootstrap无法挖完成加载请求时,子加载器才会尝试自己去加载。这过程中所有的加载请求最终都会传送到启动类加载器中
双亲委派模型有什么好处:
注意!!! 回收内存(死亡对象的回收),释放内存(因为内存是有限的)
程序在使用内存时,才能申请内库存空间,不使用了则需要释放,确保后续进程有足够的空间
内存泄漏: 程序一直申请内存,但不释放,导致内存越来越少,直至耗尽,此时其他进程若想在申请内存,就申请不到。这种现象成为内存泄漏
C++手动回收内存:申请内存的人负责释放内存
java垃圾回收机制:无论谁申请内存,由一个固定的角色(JVM)统一来释放内存
1)垃圾回收机制的优点:
2)垃圾回收机制的缺点:
(垃圾回收的对象:一般指 堆 )
内存包含四部分:堆、方法区、程序计数器、栈
对于堆上的内存,具体回收的内容是???
不使用的成员变量
整个堆的内存分布:
堆上,存放的是new·出来的成员变量对象,包括三种:
先找出垃圾,再回收
如何找出垃圾??标记垃圾??判定垃圾??
(1)引用计数
给对象增加一个引用计数器,每当有一个地方引用它时,计数器+1;当引用失效时,计数器就-1;任何时刻 计数器为0 的对象 不再被使用,即对象已"死"。利用引用,判断对象是否“已死”
JVM中 不选用 引用计数法 来管理内存,因为引用计数法无法解决对象的循 环引用问题
如下代码所示,两个引用均指向null,则说明该对象没有引用了(被认为是垃圾,引用计数是0,可以回收):
Test a = new Test();
Test b = a;
a = null;
b = null;
(2)可达性分析
从一组初始位置(GCRoot)出发,向下进行深度遍历,把所有能访问到的对象标记成“可达”(可以被访问到),而不可达的对象(没有标记到)就是垃圾。判断对象是否“存活”
JVM中存在 一个/一组 线程 来周期性的遍历 进行 可达性分析(找出不可达的对象进行垃圾回收)
初始位置GCRoot可从如下3种位置出发:
- 栈上的局部变量表中的引用
- 常量池里面的引用指向的对象
- 方法区中,引用类型的静态成员变量
经典的3种垃圾回收方法:
(1)标记-清除
上图引入了额外的问题:内存碎片(空闲内存、正在使用的内存是交替出现的,若申请大块连续内存空间可能会分配失败),内存碎片累积会导致:
(2)复制算法
可解决内存碎片的问题,它将 可用内存 按容量划分为 大小相等的两块(每次只使用一块),当需进行垃圾回收时,将此区域存活着的对象复制到另一块上面,然后再清理掉使用过的内存。
每次都是对整个半区进行内存回收,内存分配时不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。
(3)标记-整理
解决复制算法空间利用率较低的问题,(类似顺序表删除元素,把不需要回收的对象放到需要删除的地方),开销比赋值算法更大
(4)分代回收
通过内存中的区域划分,实现不同区域和不同的垃圾回收策略;
如果对象特别大也会存放于老年代(因为新生代会多次拷贝,会导致开销很大)