-
学习内容
- 对Jvm的整体认识
- 栈区学习:方法的整体调用过程中的内存变化
- 堆区学习:对象的内存分配与创建,对象在内存中的表现
- gc算法(验证了分代思路,验证eden跟survival关系,old存在的价值,复验出内存地址的核心概念)
-
学习后需要理解的三个方面
- 方法怎么执行
- 对象怎么分配
- 对象怎么回收
一.Jvm是一种规范
- (Jvm最核心的定义)JVM就是一种规范,对于汇编的语言规范和处理,而上层的高级语言实际上我们就理解为是一堆的字符串;
1.Java程序的执行过程
-
一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到方法区,执行
引擎将会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译
存在,输入字节码,调用操作系统函数。
过程如下:Java 文件->编译器>字节码->JVM->机器码。
JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀
的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作
2.字节码文件与JVM
-
我们平时说的JAVA字节码,指的是JAVA语言编译(通过javac编译.java后缀文件)成的字节码,准
确的说任何能在JVM平台上执行的字节码格式都是一样的,所以应该统称为JVM字节码
-
不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的JVM上运行
-
JAVA虚拟机与JAVA语言并没有直接联系,他只是特定的二进制文件格式.class文件有所关联, CLASS文件中包含JVM虚拟机指令集(bytecodes)和符号表,还有一些其他辅助信息。
3.Hotspot虚拟机
- 隶属:sun
- HotSpot历史发展版本:
- 1.最初由Longview Technologies设计开发
- 2.97年被Sun公司收购,09年Oracle收购sun
- 3.JDK1.3开发Hotspot成为默认虚拟机
- 4.现阶段占据JAVA语言虚拟机市场的绝对地位
- 5.一般面试所有提到的JVM虚拟机都默认指代的是Hotspot虚拟机
4.Dalvik虚拟机&ART虚拟机
- Dalivk VM:
- 隶属:Google
- 发展历史:
- 应用于Android系统,并且在Android2.2中提供了JIT(程序执行过程中,将热点代码进行编译缓存执行),发展迅猛
- Dalvik是一款不是JVM的JVM虚拟机。本质上他没有遵循与JVM规范
- 不能直接运行java Class文件
- 他的结构基于寄存器结构,而不是JVM栈架构
- 执行的是编译后的Dex文件,执行效率较高
- 与Android5.0后被ART替换
5.JVM组成部分
- 类加载器
- 将编译好的class文件加载到JVM进程中
- 运行时数据区
- 存放系统执行过程中产生的数据
- 执行引擎
- 用来执行汇编及当前进程内所要完成的一些具体内容
6.运行时数据区结构
7.堆栈在内存中的职责
- 栈是运行时的处理单位,堆是运行时的存储单位;
- 即:
- 栈是用来解决程序运行问题,如程序如何运行、如何处理数据、方法是怎么执行的
- 堆用来解决数据存储问题,数据放哪儿,怎么放
二.运行时数据区解析,方法调用全过程
1.虚拟机栈基本信息
-
虚拟机栈
-
是什么
- 承载方法调用的过程中产生的数据容器,随线程开辟,为线程私有;
-
作用:
-
主管java方法运行过程中所产生的值变量、运算结果、方法的调用与返回等信息管理
主核心:局部变量、计算结果
-
-
结构作用:
-
栈结构的应用能产生一种快速有效的分配方案,访问速度仅次于程序计数器
JAVA直接堆栈操作只有两个:出栈、入栈(此种应用不需要有GC设定)
-
-
2.程序计数器/PC寄存器
- 作用:多线程环境下对需要执行的代码进行定位
- 唯一一块不会产生内存泄漏
3.栈区存储结构与运行原理
- 栈帧;
- 单线程,默认不改的情况下,栈空间是1M;
3.1.栈帧内部结构划分
-
栈帧的本质:无数个内存空间的内存集合(可以通过装逼工具-HSDB查看)
- 所有的堆区和栈区就是在内存上开辟一连串的内存空间
-
局部变量表(详细讲解)
- 使用的是数组的形式进行存储
- 在最初学习Java的时候,说到栈是存放基本数据类型和引用类型的地址,这两者实际上是存放在局部变量表中的;
-
操作数栈(详细讲解)
-
栈帧数据(除局部变量表和操作数栈以外的数据的统称)
3.1.1.局部变量表
-
局部变量表也被称之为局部变量数组或者本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义方法体内的局部变量。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题;
- 局部变量表需要的容量大小是在编译器确定下来的,并保存在方法的code属性的maximumlocal variables数据项中,运行期间局部变量表大小不变;
- 方法嵌套调用的次数由栈的大小决定,局部变量表决定着栈帧的大小,这里是在编译期就会确定下来;
-
关于局部变量表slot(变量槽)
- slot是局部变量表的基础单位
- 在表中,32位类型数据占用一个slot,64位数据占用2位
-
slot重复利用问题
-
局部变量表中slot是可以重用的,如果一个局部变量过了其他作用域,那么其作用域之后声明的新
的局部变量有可能会复用这个slot,以便于节省资源
-
-
局部变量表:
- 1.默认在局部变量表第一位会置入一个this指针
- 2.参数也会置入
- 3.内部所有申明的变量
- 4.注意槽的概念: 32位1slot 引用类型32位
- 5.大小在编译时固定
- 6.局部变量表的slot存在复用
3.1.2.操作数栈
-
每一个独立的栈帧中除了包含局部变量表之外还包含一个后进先出的操作数栈;
-
作用(单纯的通过文字去理解是比较抽象的,配合实际场景去理解就很简单的):在方法执行过程中根据字节码指令,往栈中写入数据或者提取数据,某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入;
其中,比如:复制、交换、求和、求余等操作;
4.工具使用
- sa-jdi.jar工具应用(查看栈区在物理层面的过程)
- window
- jps命令
5.总结
- 对栈区有一个全面的了解了;
- 栈区在方法运行过程中,了解了字节码的运作过程以及局部变量表和操作数栈;
三.对象分配过程
1.堆区结构详情
1.1.堆概述
- 一个JVM进程存在一个堆内存(随Jvm进程而开辟的),堆是JVM内存管理的核心区域
- java 堆区在JVM启动是被创建,其空间大小也被确定,是JVM管理的最大一块内存(堆内存大小可以调整)
- 本质上堆是一组在物理上不连续的内存空间,但是逻辑上是连续的空间
- 所有线程共享堆,但是堆内对于线程处理还是做了一个线程私有的部分(TLAB)
1.2.堆的对象管理
- 在《JAVA虚拟机规范》中对Java堆的描述是:所有的对象示例以及数组都应当在运行时分配在堆上
- 但是从实际使用角度来看,不是绝对,存在某些特殊情况下的对象产生是不在堆上分配
- 这里请注意,规范上是绝对、实际上是存在相对
- 方法结束后,堆中的对象不会马上移除,需要通过GC执行垃圾回收后才会回收
1.3.堆区的结构细分
- 记住是由新生代和老年代组成即可
- 年轻代:临时变量,生命周期短的对象
- 老年代:生命周期长的对象
- 新生代和老年代,以及堆区的大小都可以进行相关的设定(关于设定Android开发人员是不需要去设定的,art虚拟机是已经调过的);
- 何时进入老年代,会以年龄(可以理解成一次GC不死长一岁)的阈值作为参考;
- 进入老年代的阈值是15,但是对于art虚拟机而言是6;
- Eden区:用来接收所有对象的生产(一般对象产生后出现的位置都会在这里)
1.4.体会堆空间的分代思想
- 为什么需要分代?有什么好处?
- 经研究表明,不同对象的生命周期不一致,但是在具体使用过程中70%-90的对象是临时对象
- 分代唯一的理由是优化GC性能。如果没有分代,那么所有对象在一块空 间,GC想要回收扫描他就必须扫描所有的对象,分代之后,长期持有的对象可以挑出,短期持有的对象可以固定在一个位置进行回收,省掉很大一部分空间利用
1.5.堆的默认大小
- 默认空间大小:
- 初始大小:物理电脑内存大小 / 64
- 最大内存大小:物理电脑内存大小 / 4
1.6.工具:jstat的使用
- 结尾C 代表总量,结尾U代表已使用量
- S0 S1代表 survivor区的From 与 To
- E代表的是 Eden区,OC代表老年总量,OU代表老年使用量
1.7.对象分配过程图示
- 1.当对象产生后,首先是在Eden区,当Eden区满了,会触发一下MinorGc,没有被回收的对象会被推到Survivor区,同时,年龄同步增长;
- 2.当又有新对象产生了,Eden区满了,此时,会检测Eden区和Survivor的from区,将Eden区未被回收的对象和Survivor中from区的对象推到to区,同时同步增长年龄;
- 3.一旦年龄增长到6岁,对象就会被推到老年代;
- 4.当老年代满了之后,会触发一次full gc;
- 结合MinorGc、MajorGC(老年代的部分)、FullGC(活动范围是新生代和老年代)
- MajorGC的速度是MinorGc的10倍
1.8.对象分配的特殊情况
- 对于超大型的对象:如果分配的对象Eden区放不下,就区看old区是否放得下,还是放不下,就触发MajorGC,再看old区是否放的下,还是放不下,就OOM;
1.9.JVisualVm工具
- jdk的bin目录下(jdk11版本无)的
- 同样可以监控安卓的项目
- 下方的Visual GC需要下插件(在工具选项中下载)
1.10.MinorGc、MajorGC、FullGC的区别
-
JVM在进行GC时,并非每次都对上面三个内存区域一起回收,大部分的只会针对于Eden区进行
-
在JVM标准中,他里面的GC按照回收区域划分为两种:
- 一种是部分采集(Partial GC ):
- 新生代采集(Minor GC / YongGC): 只采集新生代数据
- 老年代采集(Major GC / Old GC):只采集老年代数据,目前只有CMS会单独采集老年代
- 混合采集(Mixed GC): 采集新生代与老年代部分数据,目前只有G1使用
- 一种是整堆采集(Full GC): 收集整个堆与方法区的所有垃圾
- 补充:Minor和Major gc性能是Full gc的十倍
- 一种是部分采集(Partial GC ):
1.11.GC触发策略
-
年轻代触发机制
- 当年青代空间不足时(这里年轻代满指的是Eden区中满了 ),就会触发MinorGc;
- 因为Java大部分对象都是具备朝生熄灭的特性,所以MinorGC非常频繁,一般回收速度也快;
- MinorGc会出发STW行为,暂停其他用户的线程;
-
老年代GC触发机制
- 出现MajorGC经常会伴随至少一次MinorGC(非绝对,老年代空间不足时会尝试触发MinorGC如果空间还是不足则会出发MajorGC) ;
- MajorGC比MinorGC速度慢10倍,如果MajorGC后内存还是不足则会出现OOM;
-
FullGC触发
-
调用System.gc()时
-
老年代空间不足时
-
方法区空间不足时
-
通过MinorGC进入老年代的平均大小大于老年代的可用内存
-
在Eden使用Survivor进行复制时,对象大小大于Survivor的可用内存,则该对象转入老年代,且老年代的可用内存小于该对象
-
注意:Full GC 是开发或者调优中尽量要避开的
-
1.12.GC日志查看
- 添加配置信息
- gc的调用过程日志
1.13.TLAB(Thread Local Allocation Buffer)
- 什么是TLAB?
- 堆区是线程共享区,任何线程都可以访问堆中共享数据
- 由于对象示例的创建很频繁,在并发环境下对重划分内存空间是线程不安全的,如果需要避免多个线程对于同一地址操作,需要加锁,而加锁则会印象分配速度
- 所以JVM默认在堆区中开辟了一块空间,专门服务于每一个线程。他为每个线程分配了一个私有缓存区域,包含在Eden中,这就是TLAB
- 作用
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题
- 其它
- TLAB会作为内存分配的首选,TLAB总空间只会占用EDEN空间的1%
- 一旦对象在TLAB空间分配失败,JVM会尝试使用加锁来保证数据操作的原子性,从而直接在Eden中分配
2.对象逃逸与代码优化
- 堆是分配对象存储的唯一选择吗?
- 是,(在《深入理解JAVA虚拟机》一书中,有一段这样的描述: )但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象分配到堆上也渐渐地变得不那么“绝对”了。
2.1.标量替换
- 标量(Scalar):指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量
- 聚合量(Aggregate):Java中的聚合量指的是类,封装的行为就是聚合
- 标量替换:指的是,在未发生逃逸的情况下,函数内部生成的聚合量在经过JIT优化后会将其拆解成标量。
2.2.逃逸分析
-
一个对象的作用域仅限于方法区域内部在使用的情况下,此种状况叫做非逃逸
-
一个对象如果被外部其他类调用,或者是作用于属性中,则此种现象被称之为对象逃逸
-
此种行为发生在字节码被编译后JIT对于代码的进一步优化
-
逃逸分析案例
-
使用逃逸分析,编译器可以堆代码做如下优化:
-
1.栈上分配:JIT编译器在编译期间根据逃逸分析计算结果,如果发现当前对 象没有发生逃逸现象,那么当前对象就可能被优化成栈上分配,会将对象直接分配在栈中
-
2.标量替换:有的对象可能不需要作为一个连续的内存结构存在也能被访问到,那么对象部分可以不存储在内存,而是存储在CPU寄存器中
-
逃逸分析弊端
- 逃逸分析技术在99年已经发布,到JDK1.6版本后退出,但是这个技术至今还未完全成熟,原因是无法保证逃逸分析的性能消耗一定高于它的实际消耗,虽然经过逃逸分析可以做到标量替换,栈上分配,锁消除等操作。但是逃逸分析自身也需要进行一系列的复杂分析算法的运算,这也是相对耗时过程;
3.对象的生产与对象内存分布
- 问题:
- 对象在JVM中的内存结构是怎么样的
- 对象头当中有什么信息
3.1.对象创建的几种实例化方案
- 字节码看对象的创建过程(先记录一下)
3.2.对象创建步骤
3.3.对象内存布局
- JVM普通对象头32位Mark word解析
四.GC与调优
1.GC核心概述
1.Java自动化内存管理
- 好处:无需开发人员手动参与内存分配与回收,降低内存泄漏与溢出风险;
- 缺点:弱化了开发人员在程序出现内存溢出时定位问题和解决问题的能力;
- 对于我们现在的意义:我们必须对这些自动化技术的原理进行了解,学会如何去监控和调节;
2.垃圾回收经典三问
- 1.哪些内存需要回收?
- 2.什么时候回收?
- 3.如何回收?
3.什么是垃圾?
- 垃圾:指在程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾;
- 国外定义原文:An object is considered garbage when it can no longer be reached from any pointer in the running program.
4.为什么需要GC?
- 对于系统而言,内存迟早都会被消耗完,因为不断的分配内存空间而不进行回溯,就好像不停的产生生活垃圾;
- 但是除了释放垃圾对象,也需要对于内存空间进行碎片管理,没有GC就不能保证应用程序的正常化进行;
5.清除的概念
- 在这里的清除不是去干掉具体内存中的数据,而是本身分配的是一组连续的内存编码给我们使用,清除就是在回收这些空闲地址,将它们保存在空闲地址表当中,下次有新的对象需要空间时去判断是否够用;
2.GC相关算法
- 分为垃圾确认(标记阶段)算法和清除垃圾(清除阶段)算法;
- 标记阶段算法
- 引用计数算法
- GCRoot可达性分析算法
- 清除阶段算法
- 标记-清除算法
- 复制算法
- 标记-压缩算法
1.引用计数算法
- 原理:对每一个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。
- 例:一个对象A,只要有任何一个对象引用了A则A的引用计数器就+1,当引用失效时,引用计数器就-1.只要对象A的引用计数器的值为0,即标识对象A不可能再被使用,可进行回收;
- 优点:实现简单,垃圾对象便于识别,判断效率高;
- 缺点:
- 需要单独的字段存储计数器,这样的做法增加的存储空间的开销;
- 每次赋值需要额外的加减法计算,增加了时间开销;
- 引用计数算法最大的问题是无法处理循环引用的情况,这是一个比较致命的缺陷;
2.可达性分析算法
- 相对于引用计数算法,他有效的解决了在引用计数算法中的循环引用问题,防止内存泄漏发生;这种类型的垃圾收集也叫作追踪性垃圾收集;
- 概念:
- 可达性分析算法以根对象集合为起点,按照从上至下的方式搜索被根对象集合所链接的对象目标是否可达
- 使用可达性分析算法后,内存中的存货对象会被根对象集合直接或者间接连接着,搜索所走过的路径称之为引用链
- 如果目标对象没有任何引用链项链,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象
- 在可达性分析算法中只有能够被根对象集合直接或间接连接的对象才是存活对象
- 可以作为gcRoots对象的有
- 虚拟机栈汇总的引用对象
- 例:各个线程被调用的方法中使用的参数、局部变量等
- 本地方法栈内JNI引用的对象
- 方法区中类静态属性引用对象
- 例:JAVA类的引用类型静态变量
- 方法区中常量引用的对象
- 例:字符串常量池里面的引用
- 所有被同步所synchronize持有的对象
- java虚拟机内部引用的对象
- 例:基本数据类型对应的Class对象,一些常驻的异常对象(NullPointerException等)
- 系统类加载器
- 总结:一个指针,其保存了堆里面的对象,但自己又不在堆当中,那么它就是一个Root;
- 虚拟机栈汇总的引用对象
3.标记-清除(Mark-Sweep)算法
- 背景:标记清除算法是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并应用于Lisp语言;
- 执行过程:当堆空间中有效内存空间被耗尽时,就会停止这个程序(Stop the world),然后进行两项工作,标记,清除这两部分;
- 标记:从引用根节点上开始遍历(可达性分析算法)标记所有被引用的对象。一般是在对象Header中记录为可达对象;
- 清除:对堆内存从头到尾进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收;
- 缺点:效率不高;在进行GC的时候需要停止整个应用程序,导致用户体验差;且会产生的大量的内存碎片;
4.复制(Copying)算法
- 背景:为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.LMinsky与1963年发表了著名论文,”使用双存储区的Lisp语言垃圾收集器“,该论文中被描述的算法被人们称之为复制算法。
- 执行过程:将内存空间分为两块,每次只使用其中一块,在垃圾回收的时候,将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块,交换两个内存角色。
- 缺点:1.需要两倍空间;2.GC需要维护对象的引用关系,时间开销加大;
- 使用场景:此种方案使用与垃圾对象较少,量级不大的情况;
5.标记-压缩/整理(Mark-Compact)算法
- 背景:复制算法的高效是建立在存活对象少、垃圾对象多的前提下。这种情况在新生代中经常发生,但是在老年代,更常见的情况是大部分对象都是存活的。如果依然使用复制算法,由于存活对象多,复制成本也会非常高。因此基于老年代使用复制算法并不适用;
- 执行过程:第一阶段与标记清除算法一致;第二阶段将所有的存货对象压缩到内存的一端,按照顺序排放,之后清理边界外所有空间;
- 优劣:效率一般般
- 标记压缩算法的最终效果等同于标记-清除算法执行完成后在进行一次内存碎片的整理,因此,也可以把他称之为标记-清除-压缩(Mark-Sweep-Compact)算法;
- 二者本质差异在于标记清除算法是一直非移动式的回收算法,标记压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策;
- 可以看到,标记的存活被整理后,按照内存地址一次排列,而未被标记的内存会被清理掉。如此一来,我们需要给新对象分配内存是,JVM只需要持有一个内存的起始地址即可,这个比维护一个空闲列表显然少了很多开销;
3.清除阶段的三种算法对比
- 性能对比
- 效率上来说,复制算法最快,但是内存浪费最多;
- 而为了尽量兼顾上面三个指标,标记整理算法相对平滑一些,但是效率上不尽人意,它比复制算法多了一个标记阶段,比清除多了一个整理内存阶段;
4.分代收集算法
- 为了满足垃圾回收的效率最优性,所以分代收集算法应运而生;
- 分代手机算法基于一个事实:不同的对象生命周期是不一样的,因此,不同生命周期的对象可以采取不同的收集方式,以便于提高回收效率。一般是把JAVA堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同回收算法,相对提高效率;
- 在系统运行过程中,会产生大量对象,其中有些对象是业务信息相关,如HTTP请求的Session、线程、Socket连接等对象,这类对象跟业务挂钩,因此生命周期长,还有一部分是运行过程中生成的临时变量,这些对象生命周期短,比如:String,这些对象甚至只使用一次即可回收;
- 目前所有GC都采用分代收集算法进行执行;
- 对象的状态经过大量的调研研究划分为年青代与老年代两个类别
- 年青代:区域相对小,对象生命周期短、存活率低,且产生对象频繁,应用复制算法,其回收整理速度是最快的。复制算法效率只与当前存活对象大小有关,因此很实用于年青代的回收,而空间问题,因为存活率问题,所以单独开辟S0,S1两块空间;
- 老年代:区域较大,生命周期长、存活率高,回收不及年青代频繁;这种情况存在大量存过对象下,复制算法不适用,所以一般是用标记-清除与标记-整理算法混合实现;Mark阶段的开销与存活对象的数量成正比;Sweep阶段的开销与所管理的大小成正比;Compact阶段的开销与存活对象的数据成正比;
5.增量收集算法
- 上述所有算法,在垃圾回收过程中,软件都会处于一种Stop The World的状态。在STW状态下,应用程序所有线程都会挂起,暂停一切正常工作,等待垃圾回收完成,这种情况将严重影响用户体验或系统稳定。为了解决这个问题,催生出了一套增量收集算法;
- 基本概念:如果一次性将所有垃圾进行处理,需要造成系统长时间停顿,那么久可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。一次反复,直到垃圾收集完成;
- 总结:实际上就是通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理、复制等工作;
- 缺点:使用这种方式,由于在垃圾回收过程中,间断性的还执行了应用程序代码,所以能减少停顿时间。但是因为线程切换和上下文转换的消耗,会是的垃圾回收的总体成本上升,系统吞吐量下降;
6.分区算法
- 相同条件下,堆空间越大,一次GC时间越长,停顿时间也越长,为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干小区间,而不是整个堆空间,从而减少一次GC所产生的停顿;
- 实际上GC过程要复杂的多,需要考虑的不单单是这些,还有并行与并发的兼顾,而且通常都会应用复合算法去使用;
7.常用7中GC垃圾收集器
7,1.并行与串行
-
站在垃圾回收线程的角度
-
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态;
- 串行(Serial):单线程执行,如果内存不够则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完成再启动新线程;
7.2.并行&并发
-
并发:指的是多个事情在同一时间段内同时发生,并发的多个任务之间是相互抢占资源;
-
并行:指的是多个事情在同一时间点上同时发生了,并行的多个线程之间不相互抢占资源;
-
只有在多个CPU或者一个CPU多核的情况中,才会发生并行。
7.3.常用GC垃圾回收器对比
- ART虚拟机使用的垃圾回收期是CMS
7.4.CMS
-
CMS是jdk1.5发布的,真正意义上的并发处理垃圾回收器;
-
特性:用户线程停顿短暂(低延迟);
-
CMS的四个重要阶段(即将标记—>整理—>清除 进行了整理)
- 初始标记:这个阶段会出现STW(这里时间短)现象,主要工作内容是标记处GCRoot能关联到的对象。(注意,这里只有GCROOT的对象,不会涉及引用链);
- 都标记成灰色,如果没有引用就标记成白色;
- 并发标记:遍历GCROOT整个引用链,这个工作耗时非常长(故开并发),采取了与垃圾收集器线程一起运行的方法;
- 会与用户线程混合使用,这里不尝试STW现象,但是会出现数据有变动的情况;
- 遍历完标记黑色;
- 重新标记:因为在上面步骤有用户线程行为,所以此处再次STW,进行重新标记,但是这部分只管重新运行后的那部分对象数据的变动;
- 标记有变动的部分,再次进行STW;
- 除开灰黑对象之外,无状态对象再拿出来标记;
- 并发清理:清理所有标记的死亡对象,释放,这一步与用户线程同步进行;
- 不停用户线程;
- 清除白色;
- 初始标记:这个阶段会出现STW(这里时间短)现象,主要工作内容是标记处GCRoot能关联到的对象。(注意,这里只有GCROOT的对象,不会涉及引用链);
-
内存整理的时机在哪儿?
- 定时处理,Old区的整理没有Eden区频繁,依赖于三色标记法(暂时了解即可,目前不是很理解)来实现;
四.Dalvik虚拟机&ART虚拟机
1.历史
- Dalivk VM:隶属:Google
-
发展历史:应用于Android系统,并且在Android2.2中提供了JIT,发展迅猛 。Dalvik是一款不是JVM的JVM虚拟机。本质上他没有遵循与JVM规范 ,不能直接运行java Class文件 。它的结构基于寄存器结构,而不是JVM栈架构。执行的是编译后的Dex文件,执行效率较高,于Android5.0后被ART替换;
-
从3个方面来总结Java虚拟机跟Android虚拟机的区别
- 栈区:比JVM标准少了操作数栈的概念,而是采用寄存器的多地址方案;
- 堆区:
- Android对于内存的使用场景对比于标准JVM有区别
- Android有四块区域
- 应用数据在allocationSpace中,大数据和数组在Lager Object Space中,超过3页,原子数组;
- 对象分配
- Android采用我们的最大限度保证不进行OOM,充分利用每一份空间
- 看空间够不够,不够,浮游GC一次;
- 不够,局部GC一次;
- 不够,full GC一次;
- 还不够,扩容;
- 还不够,软引用回收;
- OOM;
- Android采用我们的最大限度保证不进行OOM,充分利用每一份空间
- Android对于内存的使用场景对比于标准JVM有区别
- 类加载模式
- JVM:一个个类文件加载,加载频率高,需要了再加载;
- Dalvik:所有代码一次性一起先编译,对于这个javac编译后的指令进行优化,优化成dex文件;
2.Android 内存管理模型
- 这里讨论的主要是Java层
3.Dalvik虚拟机&ART虚拟机与Hotspot区别
-
Dalvik虚拟机不需要跨平台,其目标设备非常明确;
-
Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实
例。Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件;
-
Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的
指令集是基本寄存器的,而后者的指令集是基于堆栈的;
4.Dalvik虚拟机&Hotspot区别
-
基于堆栈的Java指令(1个字节)和基于寄存器的Dalvik指令(2、4或者6个字节)各有优劣;
-
一般而言,执行同样的功能, Java虚拟机需要更多的指令(主要是load和store指令),而Dalvik虚拟机需
要更多的指令空间;
-
需要更多指令意味着要多占用CPU时间,而需要更多指令空间意味着指令缓冲(i-cache)更易失效;
-
Dalvik虚拟机使用dex(Dalvik Executable)格式的类文件,而Java虚拟机使用class格式的类文件;
-
一个dex文件可以包含若干个类,而一个class文件只包括一个类;
-
由于一个dex文件可以包含若干个类,因此它可以将各个类中重复的字符串只保存一次,从而节省了空间,
适合在内存有限的移动设备使用;
- 一般来说,包含有相同类的未压缩dex文件稍小于一个已经压缩的jar文件;
5.ART与Dalvik
-
Dalvik虚拟机执行的是dex字节码,解释执行(程序在运行过程中,逐行进行代码编译)。从Android 2.2版本开始,支持在程序运行的过程中进行选择热点代码(经常执行的代码)进行编译或者优化。
-
而ART(Android Runtime) 是在 Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默
认 Android 运行时。ART虚拟机执行的是本地机器码。Android的运行时从Dalvik虚拟机替换成ART虚拟机,
并不要求开发者将自己的应用直接编译成目标机器码,APK仍然是一个包含dex字节码的文件。
-
ART虚拟机执行的本地机器码是从哪里来?
-
Dalvik下应用在安装的过程,会执行一次优化,将dex字节码进行优化生成odex文件。而Art下将应用的dex字节码翻译成本地机器码的最恰当AOT(运行之前,将所有代码打包编译成机器码)时机也就发生在应用安装的时候。ART 引入了预先编译机制(Ahead Of Time) ,在安装时,ART 使用设备自带的 dex2oat 工具来编译应用,dex中的字节码将被编译成本地机器码;
-
AndroidN的运作方式
-
ART使用预先(AOT)编译,并且从AndroidN混合使用AOT编译,解释和JIT;
- 1、最初安装应用时不进行任何AOT编译(安装又快了),运行过程中解释执行,对经常执行的方法进行JIT,经过JIT编译的方法将会记录到Profile配置文件中;
- 2、当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行AOT编译。待下次运行时直接使用;
-
6.ART堆区的划分
-
ImageSpace:连续地址空间,不进行垃圾回收,存放系统预加载类,而这些对象是存放system@framework@boot.art@classes.oat这个OAT文件中的该文件存于data/dalvikccache目录下,每次开机启动只需把系统类映射到ImageSpace;
-
ZygoteSpace:连续地址空间,匿名共享内存,进行垃圾回收,管理Zygote进程在启动过程中预加载和创建的各种对象、资源;
-
注:ImageSpace和ZygoteSpace在Zygote进程和应用程序进程之间进行共享,而AllocationSpace就每个进程都独立地拥有一份;
虽然ImageSpace和ZygoteSpace都是在Zygote进程和应用程序进程之间进行共享,但是前者的对象只创建一次,而后者的对象需要在系统每次启动时根据运行情况都重新创建一遍;
-
-
AllocationSpace与ZygoteSpace性质一致,在Zygote进程fork第一个子进程之前,就会把ZygoteSpace一分为二,原来的已经被使用的那部分堆还叫ZygoteSpace,而未使用的那部分堆就叫AllocationSpace。以后的对象都在AllocationSpace上分配;
-
LargeObjectSpace离散地址空间,进行垃圾回收,用来分配一些大于12K的大对象。当满足以下三个条件时,在largeobject heap上分配,否则在zygote或者allocationspace上分配:
- 1.请求分配的内存大于等于Heap类的成员变量large_object_threshold_指定的值;
- 2.这个值等于3*kPageSize,即3个页面的大小;
- 3.已经从ZygoteSpace划分出AllocationSpace,即Heap类的成员变量have_zygote_space_的值等于true;
- 4.被分配的对象是一个原子类型数组,即byte数组、int数组和boolean数组等;
7.工具使用
- MemoryProfiler(最核心的是看内存抖动)
- 垃圾桶:是具体的一次gc
- 内存状态概览
- Others:大数据区
- Code:静态代码占用的内存
- Native:c代码产生出来的
- 案例分析
- 在OnDraw中使用Color.parseColor(“#000000”)
- MAT(查看GCRoot引用链)
- 最容易发生内存泄漏的地方:内部类持有外部类的引用
- 一般卡顿都伴随有泄漏
五.Jvm与内存优化
-
优化内存的良好编码习惯
-
1.数据类型:不要使用比需求更占空间的基本数据类型;
-
2.循环尽量用foreach,少用iterator, 自动装箱尽量少用;
-
3.数据结构与算法的角度处理
- 数组,链表,栈,树,图
- 数据量千级以内可以使用Sparse数组(key为整数),ArrayMap(key为对象)性能不如HashMap但节约内存;
-
4.枚举优化
- 缺点
- 每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存;
- 较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的IO开销,使我们的应用需要更多的空间;
- 特别是分dex多的大型APP,枚举的初始化很容易导致ANR;
- 枚举可以进行改进
public enum SHAPE{ RECTANGLE, TRIANGLE, SQUARE, CIRCLE } public class SHAPE{ public static final int RECTANGLE=0; public static final int TRIANGLE=1; public static final int SQUARE=2; public static final int CIRCLE=3; }
- 缺点
-
5.static、final的问题
- static会由编译器调用clinit方法进行初始化,static final不需要进行初始化工作,打包在dex文件中可以直接调用,并不会在类初始化申请内存。所以基本数据类型的成员,可以全写成static final;
-
6.字符串的连接尽量少用加号(+);
-
7.重复申请内存的问题
- 同一个方法多次调用,如递归函数;
- 回调函数中new对象,读流;
- 直接在循环中new对象等;
- 不要在onMeause() onLayout() onDraw() 中去刷新UI(requestLayout);
-
8.避免GC回收将来要重用的对象
- 内存设计模式对象沲+LRU算法;
-
9.Activity组件泄漏
- 非业务需要不要把activity的上下文做参数传递,可以传递application的上下文;
- 和Activity有关联的对象写成static
- 如private static Button btn;
- private static Drawable drawable;
- 非静态内部类和匿名内部内会持有activity引用,建议单独写个文件;
- 单例模式持有activity引用;
- handler.postDelayed()问题
- 如果开启的线程需要传入参数,用弱引接收可解决问题;
- handler记得清除removeCallbacksAndMessages(null);
-
10.尽量使用IntentService,而不是Service;
六.类的生命周期
- 在JAVA中数据类型分为引用数据类型与基本数据类型,基本数据类型由虚拟机预先定义,引用数据类型则需要进行类加载;
- 按照JAVA虚拟机规范,从class文件到加载到内存当中的类,到类写在出内存位置,他的整个生命周期包含下述七个阶段:
- 先了解具体流程,具体细节后续再研究;
七.类加载器
八.总结
- 以上内容只是虚拟机内容的冰山一角,虚拟机知识的学习是持续性的,后续会继续投入时间在虚拟机的学习上并整理笔记;
评论区