一.学习目标
1)JVM内存区域划分
2)JVM的类加载机制
3)JVM的垃圾回收
1.JVM执行流程
程序在执行之前先要把Java代码转换为字节码(.class),JVM首先需要通过一定的方式类加载器把文件加载到运行时数据区,而字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器 执行引擎,将字节码翻译成底层系统指令再交给CPU去执行,而这个过程需要其他语言的接口
本地接口库实现整个程序的功能
口来实现整个程序的功能,这就是四个部分的职责与功能
二.JVM的内存区域划分
(45条消息) JAVA内存模型与JVM内存模型的区别_Tiantian_126的博客-CSDN博客_java内存模型和jvm内存模型的区别
JVM运行时数据区域也叫内存布局,与java内存模型完全不同
1.程序计数器(线程私有)
内存中最小的区域
保存了下一条指令的地址在哪里
指令也就是字节码文件
程序要想运行,JVM需要把字节码文件加载起来放到内存上
程序就会一条一条把指令从内存取出来,放到CPU执行
也就要随时记住 ,当前执行到哪一条了
因为CPU是并发式的执行程序的,
CPU要同时运行所有的进程
而操作系统是以线程为单位进行调度执行,每个线程都得记录自己的执行位置
所以每个线程都有一个程序计数器
2.栈(线程私有)
放进了局部变量和方法调用信息
每个线程都独有一份
举例
B用完返回调用B方法在A调用的位置
每一个方法都有自己的栈帧
错误注意!
3.堆(线程共享)
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。
一个进程用一个堆
多个线程共用一个堆
也是内存中空间最大的区域
4.方法区(线程私有)
方法区中,放入的是"类对象"
.java - >.class(二进制字节码)
.class会被加载到内存中,也就被JVM构成了类对象(加载的过程就称为"类加载")
这里的类对象,就是放到方法区中
类对象就描述了这个类长什么样
类的名字,成员,方法,成员的名字与类型,是public/private.每个方法叫什么名字,是什么类型,public/private.方法里面的指令
还有一个重要动东西(静态成员)
static修饰的成员 是"类属性"
普通的成员 "实例属性"
三.类加载
在设计一个运行环境的时候一个重要核心功能
1.类加载的目标
把.class文件加载到内存中,构建成类对象
分为三个过程
loading linking initializing
1)Loading 环节
先找到对应的.class文件,打开并读取.class文件,同时初步生成一个大概的类对象
Loading最关键的一个环节,就是读取解析class文件
并把读取解析得到的信息初步填写到类对象中
2)Linking 环节
建立多个实体之间的联系
2.1 Verification 校验
主要是验证读到的内容是不是和规范的规定的格式是否完全匹配
如果发现读到的数据格式不符合规范,就会类加载失败,并抛出一个异常
2.2Preparation 准备
准备阶段正式为类中定义的变量(即静态变量,被static修饰的变量)
分配内存并设置变量初始值阶段
就是
①给静态变量分配内存
②并设置到0值
③给他在内存上设置编号为了最后一个阶段做准备
2.3Resolution 解析
Java虚拟机将常量池的符号引用替换为直接引用的过程,也就是初始化常量的过程
.class文件,常量是集中放置的,每个常量都有一个编号
就是准备阶段给的..class文件的结构体里初始情况下只是记录了编号赋值0
resolution阶段需要根据编号找到对应内容填充到类对象中
3) Initializing 初始化
真正对类对象进行初始化,尤其针对静态成员
2.经典面试题
总结
1.类加载阶段会进行静态代码块的执行
要想创建实例,势必要先进行类加载.
2.静态代码块只是在类加载阶段执行一次
3.构造方法和构造代码块,每次实例化都会进行,
构造代码块在构造方法前面
4.父类执行在前,子类执行在后.
四.双亲委派模型
类加载的一个环节-loading阶段
JVM的类加载器 如何根据类的全限定名(java.lang.String)找到.class文件的过程
1.类加载器
JVM提供了专门的对象,叫类加载器,负责进行类加载
找文件的过程也是通过类加载器来负责的
.class文件可能放置的位置有很多,有的要放到jdk目录里,有的要放到项目目录里
还有在其他特定位置
因此,JVM里面提供了多个类加载器,每个类加载器负责了一个片区
默认的类加载器 主要有三个
1) BootStrapClassLoader
负责加载标准库中的类(String,ArrayList.Random,Scanner)
2)ExtensionClassLoader
负载加载JDK扩展的类 很少用到
3) ApplicationClassLoader
负责加载到当前项目目录中的类
程序员还可以自定义类加载器,来加载其他目录的类
例如Tomcat就自定义了类加载器,用来专门加载webapps里面的.class
双亲委派机制就描述了这个找目录的过程中,上述的类加载器如何配合
2.工作机制
1)考虑,加载java.lang.String
a.程序启动,先进入ApplicationClassLoader类加载器
b.ApplicationClassLoader就会先插,它的父类加载器是否已经加载过,如果没有
就调用父类加载器 ExtensionClassLoader
c.ExtensionClassLoader也会检查,它的父加载其是否加载过了.如果没有 就调用
它的父类加载器,BootStrapClassLoader
d.BootStrapClassLoader也会检查,它的父类加载器是否加载过,自己有没有父亲,
于是自己扫描自己负责的目录
e.java.lang.String这个类能在标准库找到,直接由BootStrapClassLoader负责后续的加载过程
2)考虑加载自己写的Test类
a.程序启动,先进入ApplicationClassLoader类加载器
b.ApplicationClassLoader就会先插,它的父类加载器是否已经加载过,如果没有
就调用父类加载器 ExtensionClassLoader
c.ExtensionClassLoader也会检查,它的父加载其是否加载过了.如果没有 就调用
它的父类加载器,BootStrapClassLoader
d.BootStrapClassLoader也会检查,它的父类加载器是否加载过,自己有没有父亲,
于是自己扫描自己负责的目录,没有找到,就回到子加载器ExtensionClassLoader
e.ExtensionClassLoader也扫描自己负责的目录,也没扫描到,
回到子加载器ApplicationClassLoader继续扫描
f.ApplicationClassLoader也扫描自己负责的目录,能找到Test类,于是进行后续加载,查找目录的环节结束
3.原因
一旦程序员自己写的类,与标准库的类,全限定类名重复了,也能够顺利加载到标准库的类
五.JVM的垃圾回收机制
1.概述
写代码的时候,需要经常申请内存
创建变量
new 对象
加载类..
申请内存的时机,一般都有明确,
需要保存某个某些数据,就需要申请内存
释放内存的时期,则不是那么清楚
内存的释放,早了也不行,完了也不行,能够恰到好处
对于c语言,需要程序员自己释放内存
所以会经常出现一个问题->内存泄露
对于c++ 提出了一个智能指针,一定程度的降低内存泄露的可能性
垃圾回收,本质上靠运行时候环境,额外做了很多工作,来完成自动释放内存的操作
2.劣势
1.消耗额外的资源
2.可能会影响到程序的流畅运行----垃圾回收经常会引入STW问题
(Stop T和World)
c++的核心原则是安身立命之本
1.与c语言兼容,也能够和各种硬件各种操作系统做到最大化的兼容
2,追求性能的机制
3.回收的内容
内存有很多种
1.程序计数器--固定大小,不涉及到释放,也就不需要GC
2,栈-----函数执行完毕,对应的栈帧就自动释放,也不需要GC
3.堆-----最需要GC.因为代码最大的内存就在堆上
4,方法区-====是类对象,类加载来的,进行"类卸载"就需要释放内存,
卸载操作其实是一个非常低频的操作
4.堆
垃圾回收的基本单位是"对象:,而不是"字节"
5.垃圾回收的过程
两个大的阶段
1.找垃圾/判定垃圾
2.释放垃圾
1)找垃圾/判定垃圾
核心就是确定这个对象未来是否还会使用,
没有引用指向,就表示不使用了
当下主流的思路,有两种方法
1.基于引用计数(是Python采取的方法)
针对每个对象,都会额外引入一小块内存,保存这个对象有多少个引用指向
这个内存不再使用,就释放了
引用计数为0,就不再使用了
通过引用来决定对象的生死
引用计数简单可靠高效但是有缺点
1.空间利用率低,每个new的对象都得搭配个计数器,(计数器假设四个字节)
假如对象本身很大,多出来4个字节,那没事,但是如果对象很小,空间就浪费了
2.循环引用的问题
1.
2.
t1.t引用指向t2
t2.t引用指向t1
3.
此时此刻,两个对象的引用计数不为0.所以无法释放,但是由于引用长在彼此的身上所以外界代码无法访问
此时这两个对象就被孤立了,既不能使用,也不能释放
这就是内存泄露的问题
2.基于可达性分析(java采取的方案)
通过额外的线程,定期对整个内存空间的对象进行扫描
有些起始位置(GCRoots),会类似于深度优先遍历一样,把可以访问到的对象都标记一遍
GCRoots分为三类
1)栈上的局部变量
2)常量池中的引用指向的对象
3)方法区中的静态成员指向的对象
带有标记的对象就是可达对象,没有标记的对象,就是不可达 也就是垃圾
可达性分析的优点,就是克服了引用计数的两个缺点:1.空间利用率低 2.循环利用
可达性分析的缺点:系统开销大,遍历一次比较慢
2)回收垃圾
三种策略,标记-清除,2.复制算法,3.标记整理
1.标记-清除
标记就是可达性分析的过程
清除就是直接释放内存
如果直接释放,但是被释放的内存是离散的,不是连续的,分散开,带来的问题就是"内存碎片"
2.复制算法
第一步把不是垃圾的拷贝到另一半
第二步,这一半整体释放
此时就可以解决内存碎片的问题
问题:
1.内存空间利用率低
2.如果要保留的对象多,要释放的对象少,此时复制开销就很大
3.标记-整理
把要的往不要的搬运
这个方法空间利用率高但是没有解绝复制高
6.分代回收
上述方法,虽然都能解决问题,但是都有缺陷,实际上JVM的实现,
会把多组方案结合使用
针对对象进行分类(根据对象的年龄分类)
年龄----一个对象熬过一轮GC的扫描 就长了一岁
1.刚创建出来的对象,就放在伊甸区
2.如果伊甸区的对象熬过一轮GC扫描,就会被拷贝到幸存区(复制算法)
3.在后续的几轮GC中,幸存区的对象就会在两个幸存区之间来回拷贝(复制算法)
每一轮都会淘汰一波幸存者
4,在持续若干轮后,进入老年代
老年代的清除是(标记-整理)
5.特殊情况
大对象,占有内存多的对象直接进入老年代
因为大对象拷贝开销大,不适合使用复制算法
7.垃圾回收器
1) 历史遗留
2)目前主流
CMS收集器
G1收集器