Java虚拟机

601 阅读11分钟

一、Java虚拟机发展史

  1. Sun Classic/Exact VM
  2. Sun Hotspot VM
  3. 移动端虚拟机KVM/Squawk VM/JavaInJava
  4. BEA JRockit/IBM J9VM
  5. 其他:Apache Harmony/Google Android Dalvik/Microsoft VM Sun Classic VM:世界第一款商用Java虚拟机,纯解释方式来执行Java代码,如果要使用JIT编译器,必须外挂。技术比较原始,使命已经终结。JDK1.0

Sun Exact VM:已经具备现代高性能虚拟机的雏形,编译器和解释器混合工作模式。准确式GC,虚拟机可以知道内存中的某个位置的数据具体是什么类型。JDK1.2

Sun Hotspot VM:目前使用最广泛的JVM。热点代码探测技术:通过执行计数器找出最具编译价值的代码,然后通知JIT编译器以方法为单位进行编译。JDK1.3成为默认虚拟机

KVM,CDC/CLDC Hotspot Implenmentation,Squawk VM,JavaInJava,Maxine VM。

JRockit:号称世界上最快的Java 虚拟机。2008、2009年Oracle分别收购BEA和Sun,整合两款VM,在HotSpot的基础上,移植JRockit的优秀特征,比如JRockit的垃圾回收器和MissonControl服务套件

Apache Harmony/Google Android Dalvik/Microsoft VM平台专有

二、Java虚拟机运行时数据区

图片1.png 程序计数器:表示的是当前线程所执行的字节码的行号指示器,解释器通过改变这个计数器来选取下一条需要执行的字节码指令。程序计数器是在Java虚拟机规范中唯一没有规定任何OutOfMemoryError异常情况的区域。

虚拟机栈:描述的是Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、方法出口等信息。平时说的堆、栈中的栈。如果线程请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈:与虚拟机栈发挥的作用是非常相似的,虚拟机栈为Java方法服务,本地方法栈为Native方法服务。

Java堆:被所有线程共享,是Java虚拟机所管理的内存中最大的一块。Java堆唯一的目的是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,又被称为GC堆,可细分为新生代和老年代,Eden空间:From Survivor空间:To Survivor空间。当堆无法满足内存分配需求时,将抛出OutOfMemoryError异常。

方法区:用于存储已被虚拟机加载的类信息、常量、静态变量等,也称“永久代”。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。运行时常量池:存放编译器生成的各种字面量和符号引用。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

三、垃圾收集

垃圾收集,主要就是思考GC需要完成的3件事情:

1. Which:哪些资源要回收

对象已死吗?如何判定一个对象是否可以回收?主要有两种方法:1.引用计数算法2.可达性分析算法 引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器减1;其中计数器为0的对象是不可能再被使用的已死对象。当两个对象相互引用时,这两个对象就不会被回收。引用计数算法不被主流虚拟机采用,主要原因是它很难解决对象之间相互循环引用的问题。

图片2.png 可达性分析算法:通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(对象不可达)时,这个对象就是不可用的。

2.When:什么时候回收?

图片3.png 要真正宣告一个对象死亡,至少要经历两次标记过程: 若对象在进行可达性分析后发现没有与GC Roots相连接的引用链,会被 第一次标记 并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法(如当对象没有重写finalize()方法或者finalize()方法已经被虚拟机调用过则认为没有必要执行)。 如果有必要执行则将该对象放置在F-Queue队列中,并在稍后由一个由虚拟机自己建立的、低优先级的Finalizer线程去执行它;稍后GC将对F-Queue中的对象进行第二次标记,如果对象还是没有被引用,则会被回收。

3. How:如何回收?

主要的垃圾收集算法: 1)标记-清除算法 (Mark-Sweep)

图片4.png 标记:首先标记所有需要回收的对象

清除:在标记完成后统一回收所有被标记的对象

缺点 效率问题,标记和清除两个过程的效率都不高(回收后空间碎片过多,再次回收(即可达性分析时)有时需要遍历整个内存区域)。

空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。

2)复制算法(Copying)

图片5.png 思路:将可用内存按容量分为两个块,每次只用其中之一。当这一块内存用完之后,将还存活的对象复制到另一边去,然后清除所有已经使用过的部分。

优点 每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点 代价是将内存缩小为了原来的一半,未免太高了一点。

解决方法 新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 在HotSpot里,考虑到大部分对象存活时间很短将内存分为Eden和两块Survivor,默认比例为8:1:1。代价是存在部分内存空间浪费,适合在新生代使用。

3)标记-整理算法(Mark-Compact)

图片6.png 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4)分代收集算法 当前商用虚拟机都采用了这种算法,根据对象的存活周期将内存划分为几块,一般是把Java堆分为新生代和老生代,根据各个年代采用适当的收集算法。

新生代一般采用复制算法(Copying)。老生代一搬采用 标记-清理(Mark-Sweep) 或者标记-整理(Mark-Compact) 进行回收。

四、Android常见的内存泄漏

1、Handler 引起的内存泄漏

图片7.png 在Android开发中,我们经常会使用Handler来控制主线程UI程序的界面变化,使用非常简单方便,但是稍不注意,很容易引发内存泄漏。

我们知道,Handler、Message、MessageQueue是相互关联在一起的,Handler通过发送消息Message与主线程进行交互,如果Handler发送的消息Message尚未被处理,该Message及发送它的Handler对象将被MessageQueue一直持有,这样就可能会导致Handler无法被回收。SecondActivity代码中有一个延迟1秒执行的消息Message,当界面从SecondActivity跳转到ThirdActivity时,SecondActivity自动进入后台,此时如果系统资源紧张(或者打开设置里面的“不保留活动”选项),SecondActivity将会被finish。但问题来了,由于SecondActivity的Handler对象mHandler为非静态匿名内部类对象,它会自动持有外部类SecondActivity的引用,从而导致SecondActivity无法被回收,造成内存泄漏

图片8.png

图片9.png

解决办法:将Handler声明为静态内部类,就不会持有外部类SecondActivity的引用,其生命周期就和外部类无关,如果Handler里面需要context的话,可以通过弱引用方式引用外部类。

通过上面的方法,创建一个静态Handler内部类,其持有的对象context使用弱引用,可以避免SecondActivity内存泄漏,但是Looper线程的消息队列中可能还有待处理的消息,所以在Activity的onDestroy方法中,还要记住移除消息队列中待处理的消息

2、单例模式引起的内存泄漏

由于单例的生命周期是和app的生命周期一致的,如果使用不当很容易引发内存泄漏。

图片10.png 这是一个单例模式的标准写法,表面上看没有任何问题,但是细心的同学会发现,构建该单例的一个实例时需要传入一个Context,此时传入的Context就非常关键,如果此时传入的是Activity,由于Context会被创建的实例一直持有,当Activity进入后台或者开启设置里面的不保留活动时,Activity会被销毁,但是单例持有它的Context引用,Activity又没法销毁,导致了内存泄漏 图片11.png

上述代码中,SecondActivity2包含一个内部类InnerClass,并且在onCreate代码中创建了InnerClass的静态实例mInner,该实例和app的生命周期是一致的。

在某些场景,如Activity需要频繁切换,需要不断加载大量图片的场合,是会出现上述代码的,每次Activity启动之后都会使用该单例,避免重复一些有压力的操作。但是这样会引起内存泄漏,因为非静态的内部类InnerClass会自动持有外部类SecondActivity2的引用,创建的静态实例mInner就会一直持有SecondActivity2的引用,导致SecondActivity2需要销毁的时候没法正常销毁。

上述代码的正确做法是把内部类InnerClass修改为静态的就可以避免内存泄漏了,因为静态内部类InnerClass不在持有外部类SecondActivity2的引用了。

当然,也可以把InnerClass单独抽出来作为一个类,写成单例模式,完成同样的功能,同时也可以避免内存泄漏。

3、非静态内部类创建静态实例引起的内存泄漏

图片12.png 上述代码中,SecondActivity2包含一个内部类InnerClass,并且在onCreate代码中创建了InnerClass的静态实例mInner,该实例和app的生命周期是一致的。

在某些场景,如Activity需要频繁切换,需要不断加载大量图片的场合,是会出现上述代码的,每次Activity启动之后都会使用该单例,避免重复一些有压力的操作。但是这样会引起内存泄漏,因为非静态的内部类InnerClass会自动持有外部类SecondActivity2的引用,创建的静态实例mInner就会一直持有SecondActivity2的引用,导致SecondActivity2需要销毁的时候没法正常销毁。

上述代码的正确做法是把内部类InnerClass修改为静态的就可以避免内存泄漏了,因为静态内部类InnerClass不在持有外部类SecondActivity2的引用了。

当然,也可以把InnerClass单独抽出来作为一个类,写成单例模式,完成同样的功能,同时也可以避免内存泄漏。

4、监听器(各种需要注册的Listener,Watcher等)

5、资源对象没关闭造成内存泄漏

6、属性动画

监听器、资源对象、以及动画,要及时的关闭,避免造成内存泄漏

作者:自如大前端研发中心-超级ZO研发组-李竹鑫

招聘信息

自如大前端研发中心招募新同学!

FE/iOS/Android工程师

公司福利有:

  • 全额五险一金,并额外购买商业保险
  • 免费健身房+年度体检
  • 公司附近租房9折优惠
  • 每年2次晋升机会

欢迎对技术有执着热爱的你加入我们!简历请投递 zhangxl122@ziroom.com, 或加微信 v-nice-v 详聊!