Android自我进阶——JAVA之JVM

为什么要了解jvm的运行原理?

1.针对系统进行内存和垃圾回收监控

2.解决因内存溢出和泄露造成的问题

3.对系统进行优化

4.提升jvm和系统性能

JVM运行原理主要有三方面:

1.内存管理

2.执行流程

3.垃圾回收
一、jvm内存管理

在这里插入图片描述在这里插入图片描述

(1)程序计数器(线程私有):当前线程执行字节码的行号指示器,字节码解释器工作时通过改变计数器的值来选择下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程回复等基础都需要计数器完成。由于jvm多线程是通过线程轮流切换分配处理器执行时间的方式来实现的.所以,任何一个特定时刻,一个处理器(单核,其实相当于多核cpu的一个内核)只会执行一个线程中的指令;多核cpu情况下多个线程会在多个内核上调度

备注!

单核cpu:实现多进程依靠于操作系统的进程调度算法,比如时间片轮转算法,比如有3个正在运行的程序(即三个进程),操作系统会让单核cpu轮流来运行这些进程,然后一个进程只运行2ms,这样看起来就像多个进程同时在运行,从而实现多进程.

多线程其实是最大限度的利用cpu资源.一个拥有两个线程的进程的执行时间可能比一个线程的进程执行两遍的时间还长一点,因为线程的切换也需要时间.即采用多线程可能不会提高程序的运行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间.

多核cpu:什么是多核cpu?多核cpu是一枚处理器中集成多个完整的计算引擎(内核).

多核cpu和单核cpu对于进程来说都是并发,并不是并行.

但是多核cpu每一个核心都可以独立执行一个线程,所以多核cpu可以真正实现多线程的并行.比如四核可以把线程1234分配给核心1234,如果还有线程567就要等待cpu的调度.线程1234属于并行;如果一会核心1停止执行线程1改为执行线程5,那线程15属于并发.

(2)java虚拟机栈(线程私有):生命周期和线程相同,描述java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧,栈帧中存有局部变量表,操作数,动态链接和方法出口等信息。每个方法从调用到执行完毕,对应着一个栈帧的在虚拟机中的入栈和出栈过程。

(3)本地方法栈(线程私有):与虚拟机栈相似。区别是:虚拟机栈为虚拟机执行java方法(字节码)服务;本地放发展则为虚拟机是用到的Native方法服务

(4)java堆(线程共享):所有对象实例及数组都要在堆上分布(但并不是绝对的,有兴趣可以搜搜栈上分配和,TLAB分配)java堆是GC管理机制的主要区域。

java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。

堆大小=新生代+老年代。堆的大小可以指定,扩展。当无法扩展又存不下的时候就会抛出OOM异常

新生代:主要用来存储新生的对象。一般占堆的三分之一空间。由于频繁创建对象,所以新生代会频繁的触发MinorGC进行垃圾回收

新生代又分为Eden区,SurvivorFrom、SurvivorTo三个区,三个区的默认比例是8:1:1。注意:From区和To区并不是固定,哪一个Survivor区为空,哪一个Survivor区就为To区。

Eden区:java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)

老年代:主要用来存放jvm认为生命周期较长的内存对象(经历了几次MinorGC仍然存活)。老年代Gc相对没有那么频繁

(5)永久代、方法区、元空间

永久代:主要存放类定义,字节码和常量等很少会变更的信息。

《java虚拟机规范》中只是规定了方法区这个概念和它的作用,并没有规定如何去实现它。在不同的jvm上方法区的实现肯定是不同的。大多数用的jvm都是Sun公司的HotSpot,在HotSpot上使用永久代来实现方法区。永久代是HotSpot的概念,方法区是java虚拟机规范中的定义,是一种规范,而永久代是一种实现。一个是标准,一个是实现。其他虚拟机实现并没有永久代这种说法。在jdk1.7之前,HotSpot使用永久代实现方法区,使用GC分代来实现方法区的内存回收。

元空间:java8(jdk1.8)中,移除永久代,用元数据区(元空间)区域代替。永久代与元空间类似的方面是都是jvm规范中方法的实现。但最大的区别在于:元空间不在虚拟机中,而是使用本地内存。元空间储存类的元信息,静态变量和常量池并入堆中,相当于永久代的数据被分到了堆和元空间。

二、JVM执行流程。Java代码通过java源码编译器,编译生成class文件,然后交给jvm执行

(1)类加载器加载字节码文件

为什么要使用类加载器?

java 语言里,类加载都是在程序运行期间完成的。这样做缺点是:类加载的时候稍微增加一些性能开销。

优点是:给Java应用程序提供更搞的灵活性。例如:

(1)编写一个面向接口的应用程序,可能等到程序运行的时候才指定其实现的子类

(2)用户可以自定义一个类加载器,让程序运行的时候从网络或者其他地方加载一个二进制流作为程序代码的一部分(这是Android插件化,动态更新安装apk的基础)

类加载机制:
在这里插入图片描述

加载:类加载器将class字节加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据访问的入口。

链接:将java类的二进制代码合并到jvm运行状态之中的过程

验证:确保加载的类信息符合jvm规范,没有安全方面的问题

准备:正式为类变量分配内存并设置初始值

解析:虚拟机常量池的符号引用替换字节引用的过程

初始化:

类的主动引用(一定会发生类的初始化)

(1)new一个类的对象

(2)调用类的静态成员(除了final常量)和静态方法

(3)使用java.lang.reflect包的方法对类进行反射调用

(4)当虚拟机启动,java Demo01,则一定会初始化Demo01类,说白了就是先启动main方法所在的类

(5)当初始化一个类,如果其父类没有被初始化,则先初始化它父类

类的被动引用(不会发生类的初始化)

(1)当访问一个静态域时,只有真正声名这个域的类才会被初始化

(2)通过子类引用父类的静态变量,不会导致子类初始化

(3)通过数组定义类的引用,不会触发此类初始化

(4)引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)

三、Java垃圾回收

垃圾回收机制是由垃圾收集器Garbage Collection来实现的,GC是后台一个低优先级的的守护进程。在内存低到一定限度时,才自动运行,因此垃圾回收的时间是不确定的。GC也要消耗cpu等资源,如果GC执行过于频繁会对Java程序执行产生较大影响。GC只能回收通过new关键字申请的内存(在堆上),但是堆上内存并不全是通过new申请分配的。还有一些本地方法,这些内存如果不手动释放,会导致内存泄露。需要手动释放后,再被GC回收。

垃圾分类

(1)改变对象的引用,如置为null或者指向其他对象

    Object obj1 = new Object();

    Object obj2 = new Object();

    obj1 = obj2; //obj1成为垃圾

    obj1 = obj2 = null ; //obj2成为垃圾

(2)引用类型

强引用:是最难被GC回收的,你坑虚拟机抛出异常,中断程序,也不回收强引用指向的实例对象。强引用(指向实例对象,存在堆中)出现内存不够用OOM也不会回收: Object obj=new Object();

软引用:在内存不足时,GC会回收软引用指向的对象。软引用可以用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用的引用对象被垃圾回收,java虚拟机就会把这个软引用加入到与之关联的引用队列中。

String str=“hello world”;

SoftReference soft=new SoftReference(str);//将强引用转为弱引用

System.out.print(soft.get());

弱引用(WeakReference),不管内存足不足,只要我GC,都可能回收弱引用指向的对象。

WeakReference wReference=new WeakReference(str);

System.out.println(wReference.get());

(3)循环每执行一次,生成的Object对象都会成为可回收的对象。

for(int i=0;i<20;i++) {

Object obj = new Object();

System.out.println(obj.getClass());

}

虚引用(PhantomReference ),虚引用必须和引用队列(ReferenceQueue)联合使用。

当垃圾回收器发现一个对象有虚引用时,首先执行所引用对象的finalize()方法,在回收内存之前,把这个虚引用对象加入到引用队列中,

你可以通过判断引用队列中是否有该虚引用对象,来了解这个对象是否将要被垃圾回收。

然后就可以利用虚引用机制完成对象回收前的一些工作。(注意:当JVM将虚引用插入到引用队列的时候,虚引用执行的对象内存还是存在的。但是PhantomReference并没有暴露API返回对象。

所以如果我想做清理工作,需要继承PhantomReference类,以便访问它指向的对象。)

ReferenceQueue queue=new ReferenceQueue<>();

PhantomReference phantomReference=new PhantomReference(str,queue);

System.out.println(phantomReference.get());

(4)类嵌套

class A{

A a;

}

A x=new A();//分配了一个空间

x.a=new A(); //分配了一个空间

x=null;//产生了两个垃圾

(5)线程中的垃圾

calss A implements Runnable{

void run(){

//…}

}

A x = new A();

x.start();

x=null; //线程执行完成后x对象才被认定为垃圾

垃圾回收判断算法

(1) 引用计数法

引用计数法是最经典的一种垃圾回收算法。其实现很简单,对于一个A对象,只要有任何一个对象引用了A,则A的引用计算器就加1,当引用失效时,引用计数器减1.只要A的引用计数器值为0,则对象A就不可能再被使用。

缺点:无法处理循环引用的问题,因此在Java的垃圾回收器中,没有使用该算法

优点:引用计数法实现比较简单

引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响

(2)可达性分析法

可达性分析法也被称之为根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:

对象是属于根集中的对象

对象被一个可达的对象引用

垃圾回收算法

(1)标记-清除算法

标记-清除(Tracing Collector)算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。

优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。

缺点:标记和清除过程的效率都不高,这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片,虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。

(2)标记-整理算法

标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。

优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。

缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

(3)复制算法

复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。

优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。

缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。

(4)分代收集算法

分代收集(Generational Collector)算法的将堆内存划分为新生代、老年代和永久代。新生代又被进一步划分为 Eden 和 Survivor 区,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。

新生代GC(MinorGC)复制算法

在这里插入图片描述在后续GC这里插入图片描述备注:图片里忘记说了,后续GC,依此类推

Eden区:Java新对象的出生地(如果新创建的对象占用内存很大则直接分配给老年代)。当Eden区内存不够的时候就会触发一次MinorGc,对新生代区进行一次垃圾回收。

ServivorTo:保留了一次MinorGc过程中的幸存者。

ServivorFrom: 上一次GC的幸存者,作为这一次GC的被扫描者。

当JVM无法为新建对象分配内存空间的时候(Eden区满的时候),JVM触发MinorGc。因此新生代空间占用越低,MinorGc越频繁。

MinorGC采用复制算法。

老年代GC(MajorGC) MajorGC采用标记—清除算法(或者标记—整理算法)

MajorGC的耗时比较长,因为要先整体扫描再回收,MajorGC会产生内存碎片。为了减少内存损耗,一般需要合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OOM。

永久代GC(jdk1.8中已经移除)

指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。

Class在被加载的时候元数据信息会放入永久区域,但是GC不会在主程序运行的时候清除永久代的信息。所以这也导致永久代的信息会随着类加载的增多而膨胀,最终导致OOM。

总结:

minorGC 清理新生代。

Major GC 是清理老年代。

Full GC 是清理整个堆空间—包括年轻代和老年代。

(1)MinorGC 触发机制

Eden区满的时候,JVM会触发MinorGC。
(2)MajorGC 触发机制

1 在进行MajorGC之前,一般都先进行了一次MinorGC,使得有新生代的对象进入老年代,当老年代空间不足时就会触发MajorGC。

2 当无法找到足够大的连续空间分配给新创建的较大对象时(如大数组),也会触发MajorGC进行垃圾回收腾出空间。

(3)Full GC触发机制

1 调用System.gc时,系统建议执行Full GC,但是不必然执行

2 老年代空间不足

3 方法区空间不足

4 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

5 由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,

4 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值