目录
1,存储区功能划分
寄存器:最快的存储区,在处理器内部,编译器分配,程序中无法控制。
栈区(stack)
(1)在RAM中,由编译器自动分配释放,存放函数的参数和局部变量的值,即存放基本数据类型(int、short、double、long、float、boolean、char、byte)和对象的引用。如方法体内声明参数int a,只要栈的剩余空间大于需要的内存空间,则系统自动在栈区为b分配空间。否则会报异常提示栈溢出。(2)栈区是向低地址扩展的,连续的内存区域,栈顶地址和栈的容量是设定好的,1M或2M或其他。(3)申请效率上,由系统分配,速度很快,仅次于寄存器。(4)存储顺序一般是先函数参数,再是局部变量。(5)生命周期是确认的,没有引用指向数据时,数据会消失。(6)数据共享,如int a=9;b=9;ab都指向c
堆区(heap)
(1)程序员手动分配释放,存放new出来的对象。操作系统有一个记录空闲内存地址的链表,当需要申请堆区空间时,会遍历该链表寻找第一个大于此空间的堆结点,然后将该结点从空闲地址链表删除,将该结点分配给程序,并在这块内存区间首地址记录内存大小,方便以后内存释放,并将剩下的空间重新放到空闲内存地址链表中。(2)堆是向高地址扩展的,不连续的内存区域,因为它是由链表存储的内存区域,所以是不连续的,链表的遍历就是由低地址向高地址的,堆的大小受限于计算机中有效的虚拟内存空间,所以堆获得的空间比较灵活,也比较大。(3)申请效率上,一般是new,速度较慢,容易产生内存碎片。(4)存储顺序显示内存块大小,其他由程序员决定。(5)内存分为新生代和老年代,永久代(5)由垃圾回收器进行回收。
全局区(静态区)
存放全局变量和静态变量,初始化的在初始化区,未初始化的在相邻的未初始化区,由系统释放。
常量池
存放常量,对于相等的字符串,常量池中只有一份,由系统释放
程序代码区
存放函数体的二进制代码非arm(随机访问存储器)存储如硬盘等永久存储空间
2、GC机制
(1)如何确定哪些对象可以回收?(可达性分析算法)
引用计数法:判断对象的引用数量。实现方法:给对象添加引用计数器,当有引用时就+1,引用完成并失效时就-1,当计数器的值为0时,则被视为失效的垃圾对象,则可以被回收。但是这个方法不能解决循环引用问题,如对象a,b互相引用,则gc机制无法识别,所以没有采用这个机制。
可达性分析算法:通过判断对象的引用链是否可达来决定对象是否可以被回收。实现方法:把所有的引用关系看作一张图,通过一系列的名为GCRoot的根节点作为起始点开始往下搜索,走过的路就叫引用链,当一个对象没有任何引用链连接GcRoot时,则认为不可达,则可以回收
(2)jvm会在什么时候回收?
CPU空闲时回收;堆内存满了时回收;主动调用System.gc()时回收。
当新生代内存不够用时为Minor GC,当JVM内存不够用时为Full GC。
(3)gc怎么回收?使用最多的是分代收集算法
标记-清除算法:最基础的算法,先标记,回收时将带有标记的内存都回收。虽然简单,但标记和清除的效率都很低,并且清除内存后会造成内存碎片化,当需要保存较大对象时无法找到足够的连续内存来保存,造成空间浪费。
复制算法:为了解决内存碎片化问题,将内存分为大小相等的两块AB,每次只使用一块内存,如这次使用A块,当垃圾回收时,就将A上还活跃的对象复制到B内存中,然后一次性清理掉A中的内存空间。这样不会有内存碎片化问题,但总是复制,效率变低,关键是内存还少了一半儿。所以将该算法进行了改进,内存区域不再划分为大小相等的两块,而是以8:1:1的比例分为Eden区,和两个Survivor区,每次都会优先使用Eden区,当Eden区满了,就将活跃对象复制到survivor区,若此时存活的对象太多导致survivor区不够时,就会通过分配担保机制将对象复制到老年代中。
标记-整理算法:和标记-清除算法类似,内存回收后,将存活的对象向左移动,整理到一起,避免产生碎片化。
分代收集算法:jvm使用最多的算法,根据对象的生命周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以采用复制算法。老年代里对象存活率高,所以采用标记-整理算法或者标记-清除算法。
3、内存分配
3.1,jvm内存有3个区域:
新生代:一般都是新生成的对象,目标是尽可能快速的回收那些生命周期短的对象,对象从这个区域被回收的过程为Minor GC,因为每次回收都有大量对象被回收,少量存货,所以使用的是复制算法。空间分配为一个伊甸园空间Eden,两个幸存者空间SurvivorA,B,每次垃圾回收时,Eden里存活的对象复制到幸存者A中,然后清空Eden,当A满了,就将A中存活的复制到B,清空A,然后AB互换身份继续循环下去。当循环n次(一般是15次)后,依然存活的被转到老年代。
老年代:一般是生命周期较长的对象。空间比新生代大,GC次数比新生代少,老年代满了时会回收,回收时间长,回收过程为Major GC或Full GC,因为对象存活率高,所以使用的是标记-整理算法
永久代:存放静态文件,如Java类常量,字符串常量等,回收过程为Major GC,只有所有实例被回收,ClassLoader被回收,Class对象无法通过任何途径访问包括反射时,才会发生回收机制。
3.2,内存分配
== :对基本数据类型比较的是值,对引用数据类型比较的是地址。
equals:只能比较引用数据类型,基本数据类型没有此方法,如果没有重写Object的equals方法,则该方法返回的结果就是==的结果,重写后的equals方法比较的是对象中的属性,如String类就重写了equals方法,比较的是对象的值。
String s1 = new String("helloworld");//创建了两个对象,“”引起来的在常量池中,new出的对象在堆中。
c,c1,c2的地址值相同,都在常量池中,所以返回true。d相当于d=new String(a+b),是在堆内存中,地址不同所以返回false。
4、内存泄漏及解决方法
指程序中动态分配内存给一些临时对象,对象可达无法被GC回收,但对象已无用。
4.1场景:
长生命周期的对象持有短生命周期对象的引用,如全局静态map中缓存局部变量,但没有做清空操作,map会越来越大,造成内存泄漏;
非静态内部类持有外部类的引用:如使用Handler时,当activity被销毁了,handler发送的message没有执行完毕,那么handler就不会被回收。但是由于非静态内部类默认持有外部类的引用,又因为handler可达,并持有activity引用,所以jvm会错误的认为activity可达而不执行GC,这时activity就造成内存泄漏。
4.2解决方法
尽在释放无用对象的引用;
5、内存溢出
内存泄漏时内存溢出的主要原因,但不是唯一原因。
堆内存溢出:新生代中survivor和老年代仍然无法存放Eden复制过来的对象,导致无法在Eden中new对象。
方法区(全局区)内存溢出:加载的类过多,或者使用反射等生成的动态代理类过多时。
线程栈溢出:每个线程都独有一块内存结构,当递归太深或者调用层级过多时会导致溢出。
解决方法:加大JVM内存大小;减少String的使用,可以用StringBuffer,StringBuilder替代;尽量少用静态变量,因为静态变量放在全局区(方法区,永久代),基本不会GC;避免在循环中创建对象;不要一次性加载过多数据,如数据库取数据;内存泄漏的解决方法都加上;
6、WebView内存泄露
(1)通过布局的viewgroup,使用viewgroup.add方式添加webview,销毁的时候先通过viewgroup.removeView(webview)删除webview,然后webview.destroy;另外传入webview的Context采用弱引用的方式
(2)新开进程,关闭页面将进程关闭
7、类加载
7.1、7个过程
主要就是加载、验证、准备、解析、初始化、使用、卸载。简单说就是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行验证、解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
7.1加载:加载是类加载的第一个过程,在这个过程,将完成以下3件事:
(1)通过一个类的全限定名获取该类的二进制流。
(2)将该二进制流中的静态存储结构转化为方法去运行时数据结构。
(3)在内存中生成该类的Class对象,作为该类的数据访问入口。
7.2验证:目的是为了确保Class文件的字节流中的信息不会危害到虚拟机,在该阶段主要完成以下4中验证:
(1)文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型。
(2)元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否继承了不能被继承的类等。
(3)字节码验证:这是验证过程最复杂的阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
(4)符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析工作能正确执行。
7.3准备:为类的静态变量分配内存并将其初始化为默认值,这些内存都在静态区进行分配。此阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
public static int num = 2;//在准备阶段num为0,初始化阶段才会为2.
7.4解析:主要完成符号引用到直接引用的转换动作,解析动作并不一定在初始化完成之前,也有可能是之后。
7.5初始化:是类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余阶段完全由虚拟机主导和控制。到了初始化阶段,在真正开始执行类中定义的Java程序代码。
7.2、类加载器双亲委派模型机制
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,则由子类去完成类的加载。
7.3、类加载器的定义和种类
定义:实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
种类:
(1)启动类加载器:用来加载Java核心类库,无法被Java程序直接引用。
(2)扩展类加载器:加载Java的扩展库,Java虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里查找并加载Java类。
(3)系统类加载器:根据Java应用的类路径来加载Java类,一般来说,Java应用的类都是由他加载的,可以通过ClassLoader.getSystemClassLoader()来获取它 。
(4)用户自定义的类加载器:继承java.lang.ClassLoader类的方式实现。