likes
comments
collection
share

Android面向面试复习——JVM篇

作者站长头像
站长
· 阅读数 38

内存泄漏(Memory Leak)

  1. 静态集合类,如Map,List等。容器中的对象在程序结束之前不能被释放,容易造成内存泄漏

Android面向面试复习——JVM篇

  1. 单例模式。单例静态特性,如果持有外部对象引用,外部对象无法被回收造成内存泄漏

  2. 内部类持有外部类。如果一个外部类对象返回一个内部类对象,内部类对象被长期引用,外部类对象无法被回收,造成内存泄露

  3. 数据库连接,IO资源等没有close(),GC无法回收,导致内存泄漏

  4. 变量不合理作用域。变量作用域定义过大,无法跟随方法的执行结束而被回收

  5. 改变哈希值。当一个对象被存储进HashSet后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则会找不到对象,导致内存泄漏

  6. 缓存泄漏。建议改为弱引用,替换强引用,就可以被回收了

  7. 监听器和回调。客户端在API中注册回调,却没有显示取消,就会聚集,同样建议改为弱引用方便回收


内存抖动

短时间内有大量的对象被创建或被回收。如循环中的字符串拼接操作。


内存溢出(OOM)

OutOfMemoryError.Java heap space:java堆内存溢出

OutOfMemoryError.PermGen space:永久代溢出

StackOverflowError:栈溢出

延申:如何解决OOM?

通过内存映像分析堆存储快照,确定到底是内存泄漏还是内存溢出。如果是内存泄漏可进一步通过工具查看泄漏对象到GC Roots的引用链,定位泄漏代码位置。如果不是内存泄漏,就修改虚拟机堆参数,检查是否存在某些对象生命周期过长,持有状态时间过长,减少程序运行期内存消耗


Java类加载器

  1. (引导类加载器)改为启动类加载器。c++实现,加载核心类,不可获取
  2. (拓展类加载器)改为平台类加载器。java实现,加载扩展核心类,可获取
  3. 应用程序加载器,也叫系统类加载器,加载用户指定路径上的类库,可获取

自定义类加载器的作用:隔离加载类,修改类的加载方式,扩展加载源,防止源码泄漏

延申:Android类加载器

  1. BootClassLoader:用于加载常用类
  2. DexClassLoader:加载dex文件以及包含dex的压缩文件
  3. PathClassLoader:加载系统类和应用程序类

JVM内存区域

  1. 方法区:类型信息,常量,静态变量,即时编译(JIT)后的代码缓存,运行时常量池。JDK8之后把方法区从虚拟机内存移动到了本地内存 1.1 JDK1.6之前,永久代在方法区,静态变量存放在永久代

1.2 JDK1.7,有永久代,字符串常量池,静态变量移除,保存在堆

1.3 JDK1.8,无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆。

方法区的垃圾回收主要回收常量池中废弃的常量和不再使用的类型

  1. PC计数器 记录各个线程当前正在执行的字节码指令地址

延申:使用PC计数器记录当前字节码指令地址有什么用?为什么使用PC计数器记录当前线程的执行地址?

CPU是需要不停切换的,从别处切换回来之后需要知道上次执行到哪了,JVM字节码解释器需要通过改变PC计数器的值来明确下一条应该执行什么样的字节码指令

  1. 堆 3.1 JDK7之前内存分为新生代+养老代+永久代

3.2 JDK8之后内存分为新生代+养老代+元空间

3.3 年轻代分为Eden+from+to,占比为8:1:1,只有Eden触发Minor GC

3.4 养老代与年轻代占比为2:1,触发Major GC

  1. 虚拟机栈 4.1 每个方法执行时都会创建一个栈帧,栈帧里存放如下

局部变量表

操作数栈:用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

动态链接:每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,动态链接将这些符号引用转换位调用方法的直接引用

方法返回地址:存放调用该方法的pc寄存器的值一些附加信息

4.2 保存方法的局部变量,部分结果,参与方法的调用与返回

  1. 本地方法栈 提供native方法,c语言底层

延申:局部变量表、Slot

局部变量表(LocalVariableTable):定义为一个数字数组,用于存储方法参数和定义在方法体内的局部变量,是线程私有的,所需要的容量大小在编译器间确定

Android面向面试复习——JVM篇

Slot:

  1. 局部变量表的最基本存储单元。
  2. 由上图看出index从0开始。32位以内的类型(byte、short、char、boolean被转换为int)只占1个Slot,64位的类型(long、double)占用2个Slot。
  3. 此外,Slot槽位是可以循环利用的,如果一个局部变量过了其作用域那么在其作用域之后申明的新的局部变量就会复用过期的局部变量槽位。

延申:栈溢出情况?方法中定义局部变量是否安全?

局部数组过大,递归调用层次过多,指针数组越界等都会造成栈溢出

方法中定义局部变量是否安全要具体分析:

Android面向面试复习——JVM篇

总结:

  1. 程序计数器线程私有,不存在Error和GC
  2. 本地方法栈和虚拟机栈线程私有,存在Error,不存在GC
  3. 堆空间线程共有,存在Error和GC
  4. 方法区线程共有,存在Error和GC

延申:堆和栈的区别?

栈内存:栈内存存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短

堆内存:堆内存存储的是数组和对象,凡是new建立的都是在堆中,堆中存放的都是实体,实体用于封装数据,而且是封装多个属性,如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取

所以堆与栈的区别很明显:

1.栈内存存储的是局部变量而堆内存存储的是实体,且栈由指令分配,内存控制无需手动。堆由C/C++运算函数分配,内存控制需要手动操作

2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短,且栈内存远小于堆内存

3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收


比较Minor GC,Major GC,Full GC

  1. Minor GC,年轻代空间不足时触发,只有Eden会触发,幸存者区不会主动触发,Minor GC非常频繁,回收速度块,会引发STW
  2. Major GC,老年代空间不足时触发,速度很慢
  3. Full GC,开发时尽量避免,内存非常不足时触发全局GC,有时与Major GC混用

TLAB

堆中的空间不是百分百共享的,JVM为每个线程分配了一个私有缓存区域TLAB,大约占堆内存的1%,包含在Eden区中。

多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时提升内存分配的吞吐量,也因此称之为快速分配策略

一旦对象在TLAB空间分配内存失败时,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间分配内存

Android面向面试复习——JVM篇


垃圾回收器

Serial回收器:连续回收器,串行回收,简单高效,单核cpu效率较高,新生代复制,老年代标记整理

ParNew回收器:平行回收器,并行回收,只能处理新生代,是Serial的多线程版本

Parallel Scavenger回收器:平行清道夫,吞吐量优先,并行回收,只能处理新生代,适合后台运算

ParallelOld回收器:平行清道夫的老年版,多线程,标记整理算法

SerialOld回收器:串行老年代回收,单线程,标记整理算法

CMS回收器:并发,标记清除

G1回收器:垃圾优先,并行并发,分代收集,空间整合,整体标记整理,局部复制

延申:G1回收器具体流程

年轻代GC,老年代并发标记,混合回收MixedGC,有可能进行FullGC,发生了要及时进行调优


7款垃圾回收器总结

Android面向面试复习——JVM篇

延申:垃圾回收器与分代的关系,以及合作关系

Android面向面试复习——JVM篇

Android面向面试复习——JVM篇

Android面向面试复习——JVM篇


永久代改成元空间的原因

  1. 尽量少出现Full GC,效率高
  2. 对永久代调优很困难

JVM内存模型

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,简要访问过程如下

Android面向面试复习——JVM篇


判断对象存活的两种方式:

可达性分析:从GC Roots开始遍历向下,不可达元素被认定为不可用

引用计数算法:为对象增加一个计数器,使用他的地方计数器就+1,每当一个引用失效就-1,计数器为0就表示不可用

延申:那么哪些元素可以被作为可达性分析中的GC Roots:

虚拟机栈中引用的对象,本地方法栈内引用对象,方法区中类静态属性引用对象,方法区中常量引用对象,所有被同步锁持有的对象,java虚拟机内部的引用


常见垃圾收集算法:

标记——清除算法:标记从根节点开始遍历所有的被引用对象为可达对象,清除对堆内存从头到尾进行线性遍历未被标记的对象,效率不高,需要STW,清理出的内存不连续

复制算法:将内存空间分为两块,每次使用其中一块,垃圾回收时将正在使用的内存中的存活对象复制到另一个区域,之后清除正在使用的内存块中的所有对象,交换两个内存角色。也是幸存者区的from和to会交换名字的原因。效率高,清理的内存连续,但是需要两倍的内存,且适用于存活对象不多的情况,适用于年轻代。

标记——压缩算法:标记从根节点开始遍历所有的被引用对象为可达对象,将存活对象压缩到内存一端,按顺序摆放后清理边界外所有空间。效率略高,也需要STW,一般用于老年代

分代收集算法:根据不同年代实行不同算法。新生代对象朝生夕死,使用复制算法,老年代使用标记压缩算法

增量收集算法:将垃圾收集线程和应用程序线程交替执行,解决STW问题,但仍是传统的标记——清除,和复制算法,增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段方式完成标记清理或复制工作。但是由于频繁切换会使得垃圾回收总体成本上升

分区收集算法:将一块大内存区域划分为多个小部分,根据目标的停顿时间,每次合理回收若干小区间,而不是整个堆空间,减少一次GC产生的停顿

Android面向面试复习——JVM篇


System.gc()

默认情况下通过System.gc()或Runtime.gc()会显示触发GC,然而System.gc()会附带一个免责声明,即无法保证对垃圾收集器的调用

延申:STW

所有垃圾回收器都有STW,即使是当前优秀的G1回收器都有STW,开发中尽量避免调用System.gc(),否则会出现STW


安全点与安全区域

安全点(SafePoint):特定位置开始GC,因为程序并非所有地方都可以停顿下来开始GC,只有特定位置才可以

安全区域(SafeRegion):一段代码片中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的,可以看作扩展了的SafePoint


四种引用:

强:不回收,容易造成内存泄露

软:内存不足才回收,内存不足将要溢出时使用

弱:发现即回收,内存充足时这些缓存数据也可以加速系统

虚:对象被回收时收到通知,常用于引用队列Ac,资源释放操作


类加载机制:

  1. 加载:获取二进制数据流,解析数据结构,创建实例作为入口

  2. 链接: 2.1 验证:格式检查,语义检查,字节码验证,符号引用验证。保证字节码合法

2.2 准备:为类的静态变量分配内存,初始化为默认值

2.3 解析:将符号引用转化为直接引用

  1. 初始化:为类的静态变量赋于正确的初始值,执行clinit方法,由父及子,静态先行

延申:由Person p = new Person()来看类的加载过程

具体步骤:

  1. 虚拟机中找到Person的文件路径,由对应的类加载器来加载Person,如果有直接父类时会先加载父类

  2. 栈内执行main函数,开辟一个空间给p

  3. 验证合法后,new的时候在堆内开辟一块内存给p,准备阶段将对象实例默认初始化

  4. 调用构造方法,执行第一句时会进入父类构造方法。执行完父类构造方法后会开始正式对当前对象进行初始化,对象p被赋予对应的值,即阶段

  5. 初始化完毕,将堆内存赋给栈内的对象p,并让p添加一个指向堆内存的引用


ClassLoader源码解析

常用方法有三:

  1. loadClass():此方法里实现java双亲委派,JVM不鼓励重写此方法,父加载器加载失败后调用findClass()方法。
  2. findClass():检查完父类加载器后被loadClass()方法调用,JVM鼓励重写此方法而非loadClass()。
  3. defineClass():通常和findClass一起使用,将class文件实例化成对象。

想打破双亲委派就要重写loadClass(),不想打破只需要重写findClass()


双亲委派机制原理的优势与缺点,何时打破

原理:一个类加载器接收到需要加载的请求时不会先自己去加载而是委托给父类的加载器去加载,一步步向上委托,能加载就给父类加载。不能加载则层层往下,留给子加载器去自己加载

优势:防止类重复加载,保护核心API

缺点:顶层ClassLoader无法访问底层ClassLoader所加载的类

打破:三次

  1. JDK1.2之前无双亲委派概念,打破到处存在
  2. 自身模型缺陷必须打破
  3. 程序热部署需求

对象的实例化

创建对象的方式:

  1. new
  2. Class的newInstance()
  3. Constructor的newInstance()
  4. 使用clone()
  5. 使用反序列化
  6. 第三方库

字节码角度看对象的实例化:

Android面向面试复习——JVM篇

创建对象的步骤和对象的赋值操作:

Android面向面试复习——JVM篇


String

  1. 声明为final,是不可继承的
  2. JDK8以前内部定义了final char[] value用于存储,JDK9时将char改为byte数组,这里也看出String由final声明,是不可变的
  3. String的常量池是一个HashTable,固定大小,不存放相同内容是因为内容多了会导致哈希冲突
  4. JDK6及以前,字符串常量池存放在永久代,JDK7时字符串常量池被放在了堆内存,具体原因是因为permSize比较小,而且永久代垃圾回收不频繁

字符串拼接

  1. 常量与常量拼接结果就在常量池,拼接原理是编译器优化
  2. 拼接中只要有一个变量结果就在堆中,此时拼接原理是StringBuilder
  3. append()方法比拼接要高很多,因为直接拼接会创建新的对象,append()方法自始至终只有一个对象

intern()方法

如果不是用双引号声明的String对象,可以使用String提供的intern()方法,方法会从字符串常量池中查询当前字符串是否存在,若不存在就会把当前字符串放入常量池中。

也就是说在任何字符串上调用intern()返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此下列表达式的值必定是true: ("a" + "b" + "c").intern() = "abc"

延申:new String("ab")会创建几个对象?

Android面向面试复习——JVM篇

Android面向面试复习——JVM篇


类的主动使用被动使用

Android面向面试复习——JVM篇


对象的finalize()机制

finalize()方法是可以重写的,当垃圾回收器发现没有引用指向一个对象,回收此对象之前都会先调用这个对象的finalize()方法

由于此方法存在,虚拟机中的对象一般处于三种可能状态

  1. 可触及的:从根节点开始,可达
  2. 可复活的:对象所有的引用被释放,但是对象有可能在finalize()
  3. 不可触及的:对象的finalize()被调用,并且没有复活,就会进入不可触及状态,不可触及的对象不可能被复活,因为finalize()只会被调用一次

判断一个对象是否可以回收:

Android面向面试复习——JVM篇


方法调用指令

Android面向面试复习——JVM篇


CMS工作流程

  1. initial-mark:初始标记,标记从GC Roots直接可达的老年对象
  2. concurrent-mark:通过遍历第一个阶段标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象
  3. concurrent-preclean:并发预清理,标记老年代存活对象,让重新标记阶段时间尽可能短
  4. concurrent-abortable-preclean:同样为了减轻重新标记阶段工作量,在进入重新标记阶段前等到一个Minor GC
  5. concurrent-remark:重新标记阶段,多线程工作
  6. concurrent-sweep:并发清除,用户线程被激活,将未被标记为存活的对象标记为不可达
  7. concurrent-reset:并发重置,重置回收器状态,准备进入下一个并发回收周期

G1工作流程:

  1. pause:利用STW标记所有存活对象
  2. concurrent-root-region-scan-start:根分区扫描开始,扫描survivor分区
  3. concurrent-root-region-scan-end:根分区扫描结束
  4. concurrent-mark-start:并发标记阶段开始
  5. concurrent-mark-end:并发标记阶段结束
  6. finalize marking:处理finalizer对象
  7. ref-proc:引用处理
  8. unloading:类卸载
  9. clean-up:清理阶段,也会STW,计算最后存活的对象,为下一并发标记阶段做准备,老年代分区和巨型对象会被释放和清理,处理没有任何存活对象的分区的RSet
  10. concurrent-cleanup-start:并发清理阶段启动
  11. concurrent-cleanup-end:并发清理阶段结束

欢迎指正。

转载自:https://juejin.cn/post/7011716717718011912
评论
请登录