ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
本章主要介绍jvm,dvm,art。通过对这三个虚拟机的介绍让学生明白,android虚拟机是一步步从dvm发展到今天的art,以及在发展的过程中,android操作系统对虚拟机主要做了那些方面的优化并详细的讲解了java虚拟机在结构,编译流程,类加载以及内存管理等方面的知识。 ### **JVM结构详解** * JVM整体结构详解 * Java代码的编译和执行过程 * 内存管理和垃圾回收 #### **JVM整体结构详解** ![](https://box.kancloud.cn/2b543899e9e01a0c884fcbba9b4629d3_837x425.png) ![](https://box.kancloud.cn/46bc94d687353c672c6bff23cc36bf43_947x468.png) **Class文件的生成** * **编译流程** ![](https://box.kancloud.cn/ef7ecff94467495c512152ac7819989f_881x303.png) > **备注**: > 1. JVM字节码就是Class字节码 > 2. javac就是一个编译器程序 **类加载器** ![](https://box.kancloud.cn/efb8910ca54bbd0c1a2ec470fa3d7495_754x517.png) **加载流程** ![](https://box.kancloud.cn/29b07c51df8882a5348a41831d760af4_918x495.png) * **Loading**:类的信息从文件中获取并且载入到JVM的内存里 * **Verifying**:检查读入的结构是否符合JVM规范的描述 * **Preparing**:分配一个结构来存储类信息 * **Resolving**:把这个类的常量池中的所有的符号引用改变成直接引用 * **Initializing**:执行静态初始化程序,把静态变量初始化成指定的值 #### **JVM内存管理** 内存分配是一款虚拟机产品的核心内容,良好的内存分配策略可以提高虚拟机的处理效率。Dalvik虚拟机的内容分配策略和Java 虚拟机的内存分配策略类似。 Java 内存分配与管理是Java 的核心技术之一, 一般来说, Java 在内存分配时会涉及到以 下区域。 * 寄存器: 我们在程序中无法控制。 * 栈: 存放基本类型的数据和对象的引用,但**对象本身不存放在栈中,而是存放在堆中**。 * 堆: 存放用new 产生的数据。 * 静态域: 存放在对象中用static 定义的静态成员。 * 常量池: 存放常量。 * 非RAM 存储: 硬盘等永久存储空间 **Java栈区** * 作用:它存放的是Java方法执行时的所有数据 * 组成:由栈帧组成,一个栈帧代表一个方法的执行 > **备注**: > 一般我们类中的方法都是被嵌套调用的,A调用B,B调用C,C调用D,以此类推,一直嵌套或者返回。栈区如何描述这个过程,这时就需要栈帧这个概念,一个栈帧代表一个方法的执行,因此Java栈可以完整描述Java中方法嵌套调用,同时得知栈中主要核心就是栈帧,搞清楚栈帧自然就明白Java栈区。 在函数中定义的一些基本类型的变量数据,还有对象的引用变量都在函数的战内存中分配。当在一段代码块中定义一个变量时, Java 就在校中为这个变量分配内存空间,当该变量退出该作用域后, Java 会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。 栈也叫栈内存,是Java 程序的运行区,是在线程创建时创建的。它的生命期是跟随钱程的生命期,线程结束找内存也就释放。 栈中的数据都是以栈帧(Stack Frame)的格式存在的。栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。 **Java栈帧** * 作用:每个方法从调用到执行完成就对应一个栈帧在虚拟机中入栈到出栈。 举例:A方法运行时调用到了B方法,在A方法执行调用到B方法的那行代码时,JVM就会创建一个保存B方法的栈帧,然后把这栈帧压入到Java栈区;到B方法执行完要返回到A方法时,这个栈帧就会随之被弹出Java栈区。 * 组成:**局部变量表**、**栈操作数**、**动态链接**、**方法出口** 局部变量表中存放了编译期可知的各种基本数据类型(boolean 、byte 、char 、short 、mt 、float 、long 、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址) 。 栈帧中到底存在着什么数据呢?在栈帧中主要保存如下3 类数据。 * 本地变量(Local Variabl es):包括输入参数和输出参数以及方法内的变量。 * 栈操作(Operand Stack) : 记录出栈、入栈的操作。 * 栈帧数据(Frame Data) : 包括类文件、方法等。 当一个方法A被调用时,就产生了一个栈帧F1,并被压入栈中,A方法又调用B方法,于是产生了栈帧F2,F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“先进后出”原则。如下图所示 ![](https://box.kancloud.cn/d37ef253310104782933d6f979051e2c_468x755.png) 图中,在一个栈中有两个栈帧,栈帧2是最先被调用的方法,先入栈,然后方法2 又调用了方法1,栈帧1处于栈顶的位置, 栈帧2 处于栈底,执行完毕后,依次弹出栈帧l 和栈帧2,线程结束,栈释放。 **本地方法栈** 作用:本地方法栈是专门为native方法服务的。 > **备注**: > 1. 本地方法栈也是通过栈帧来记录每个方法的调用 > 2. 在Java系统中,在本地方法栈执行的是非Java语言编写的代码,比如C/C++ > 3. JVM规范对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(Sun HotSpot虚拟机)直接把Java栈和本地方法栈合二为一 虚拟机栈描述的是Java方法执行的内存模型: 每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。对于活动线程中栈顶的栈帧,称为当前栈帧,这个栈帧所关联的方法称为当前方法,正在执行的字节码指令都只针对当前有效栈帧进行操作。 在栈帧的基础上,不难理解虚拟机栈的内存结构。Java 虚拟机规范规定虚拟机栈的大小是可以固定的或者动态分配大小。Java 虚拟机实现可以向程序员提供对Java 栈的初始大小的控制,以及在动态扩展或者收缩Java 栈的情况下, 控制Java 栈的最大值和最小值。 下面列出的两种异常情况与Java 栈相关。 * 如果线程请求的枝深度大于虚拟机所允许的深度,则Java 虚拟机将抛出StackOverflowError异常。 * 如果虚拟机栈可以动态扩展((当前大部分的Java 虚拟机都可动态扩展, 只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),但是无法申请到足够的内存来实现扩展,或者不能得到足够的内存为一个新线程创建初始Java 栈,则Java 虚拟机将抛出OutOfMemoryError异常。 有人通常把Java 内存区分为堆内存(Heap)和栈内存(Stack),其中所指的“栈”就是现在讲的虚拟机栈, 或者说是虚拟机栈中的局部变量表部分。 **方法区** 作用:存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后等数据 方法区在虚拟机启动时创建,是一块所有**线程共享的内存区域**。方法区(Method Area)与Java 堆一样, 是各个线程共享的内存区域。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作Non-Heap(非堆)。 Java 虚拟机规范对这个区域的限制非常宽松,除了**和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展**外,还可以选择不实现垃圾收集。但是,垃圾回收在方法区还是必须有的,只是回收效果不是很明显。这个区域的**回收目标主要针对的是常量池的回收和对类型的卸载。** 方法区的**大小也可以控制**,以下异常与方法区相关:**如果方法区无法满足内存分配需求时命,将会抛OutOfMemoryError 异常**。 **堆区** **作用:所有通过new创建的对象的内存都在堆中分配 特点:是虚拟机中最大的一块内存,是GC要回收的部分** ![](https://box.kancloud.cn/c7d190a35f10c652c992c2f35a46f1af_920x470.png) 堆内存用来存放由关键字new 创建的对象和数组。在堆中分配的内存,由Java 虚拟机的自动垃圾回收器来管理。 数组和对象本身在堆中分配,即使程序运行到使用new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,**数组和对象在没有引用变量指向它时,才变为垃圾,不能再被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)** 。这也**是Java 比较占内存的原因**。 实际上, **栈中的变量指向堆内存中的变量,这就是Java 中的指针**。 **分为三部分**: * ( 1) **Pemanent Space 永久存储区** 永久存储区是一个常驻内存区域,用于存放JDK 自身所携带的Class Interface 的元数据。也就是说,**它存储的是运行环境必需的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭Java 虚拟机才会释放此区域所占用的内存**。 * (2) **Young Generation Space 新生区** **新生区是类的诞生、成长、消亡的区域, 一个类在这里产生、应用,最后被垃圾回收器收集,结束生命**。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new(新建)出来的。幸存区有两个: 0 区(Survivor 0 space)和l 区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象, Java 虚拟机的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0 区。若幸存0 区也满了,再对该区进行垃圾回收,然后移动到1 区。那如果l 区也满了呢?再移动到养老区。 * (3) **Tenure generation space 养老区** 养老区用于保存**从新生区筛选出来的Java 对**象, **一般池对象都在这个区域活跃**。 ![](https://box.kancloud.cn/2045b4ab1b4e08f03c16142359fc0ec9_432x329.png) **注意**:但是无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例。**进一步划分的目的是为了更好地回收内存,或者更快地分配内存**。 Java 堆是类实例和数组的分配空间, 是**一块所有线程共享的内存区域**。堆在虚拟机启动时创建, 是Java 虚拟机所管理的内存中最大的一块。**内存泄漏和溢出问题大都发生在堆区域**。所以对于大多数应用来说, Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。 Java 堆内存区域的唯一目的就是存放对象实例,**几乎所有的对象实例都在这里分配内存**。如果堆中没有可用内存完成类实例或者数组的分配,在对象数量达到最大堆的容量限制后将抛出OutOfMemoryError(**OOM**) 异常。 Java 堆也是垃圾收集器管理的主要区域,因此很多时候也被称作“ GC 堆”。‘ > **引用**: > Java 虚拟机规范: > 1. 所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。 > 2. 规定堆在内存单元中只要在逻辑上是连续的, Java 堆是可以是固定大小的,或者按照需求做动态扩展, 并且可以在一个大的堆变的不必要时收缩。Java 虚拟机的实现向程序员或者用户提供了对堆初始化大小的控制,以及对堆动态扩展和收缩的最大值和最小值的控制。 #### **运行时的数据区域** Java 通过**自身的动态内存分配和垃圾回收机制**,可以便Java 程序员不用像C++程序员那么头疼内存的分配与回收。 通过**Java 虚拟机的自动内存管理机制**,不仅降低了编码的难度,而且不容易出现内存泄漏和内存溢出的问题。但是这过于理想的愿望正是由于把内存的控制权交给了Java 虚拟机, **一旦出现内存泄漏和溢出,我们就必须翻过Java 虚拟机自动内存管理这堵高墙去排查错误。** 《Java 虚拟机规范》的规定, Java 虚拟机在执行Java 程序时,即运行时环境下会把其所管理的内存划分为几个不同的数据区域。有的区域伴随虚拟机进程的启动而创建,死亡而销毁:有些区域则是依赖用户线程的启动时创建, 结束时销毁。 所有线程共享:方法区和堆, 虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区,是每个线程稀有的。Java 虚拟机运行时的数据区结构如下图所示。 ![](https://box.kancloud.cn/f1704a6fb3495ab8b49d6de829aa17fd_781x242.png) Java 虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致。Java 通过多线程机制使得多个任务同时执行处理,所有的线程共享Java 虚拟机内存区域main memory ,而每个线程又单独的有自己的工作内存, 当线程与内存区域进行交互时, 数据从主存复制到工作内存,进而交由线程处理(操作码+操作数) 。 **程序计数器( Program Counter Register)** 程序计数器是一块较小的内存空间, 其作用相当于当前线程所执行的字节码的行号指示器。在Java 虚拟机的概念模型里, 字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令,很多基础功能都需要依赖这个计数器来完成。例如分支、循环、跳转、异常处理、线程恢复等。 由于在Java 虚拟机的多线程应用中, 是通过线程轮流切换并分配处理器执行时间的方式来实现的,所以在任何一个确定的时刻的处理器(对于多核处理器来说是一个内核),只会执行一条线程中的指令。为了在线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器, 并且各条线程之间的计数器互不影响,能够独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java 方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native(本地)方法,那么这个计数器值为空( Undefined) 。**此内存区域是唯一一个在Java 虚拟机规范中没有规定任何 OutOfM emoryError 情况的区域**。 因为操作系统使用的是时间片轮流的多线程并发方式,所以在任何时刻,处理器只会处理当前线程的指令。**线程间切换的并发要求每个线程都需要有一个私有的程序计数器,并且程序计数器问互不影响。** 当程序计数器存储当前线程下一条要执行的字节码的地址时, 会占用较小的内存空间。所有的控制执行流程(例如分支、循环、返回、异常等)功能都在程序计数器的指示范围之内, 字节码解释器通过改变程序计数器值的方式来获取下一条要执行的字节码的指令。 **运行时常量池** **运行时常量池( Runtime Constant Pool )是方法区的一部分**。在Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于**存放编译期生成的各种字面量和符号引用**,这部分内容将**在类加载后存放到方法区的运行时常量池**中。 常量池是每个类的Class 文件中存储编译期生成的各种字面量和符号引用的运行期表示,其**数据结构是一种由无符号数和表组长的类似于C 语言结构体的伪结构**。另外,常量池也是方法区的一部分, **类的常量池在该类的Java class 文件被Java 虚拟机成功地装载时创建,这部分内容在类加载后存放到方法区的运行时常量池中**。 **Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行**。但**对于运行时常量池来说**, Java 虚拟机规范**没有做任何细节的要求**,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,**一般来说,除了保存Class 文件中描述的符号引用外, 还会把翻译出来的直接引用也存储在运行时常量池中**。 **运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性**, **Java 并不要求常量一定只能在编译期产生**,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池, **运行期间也可能将新的常量放入池中**,这种特性被开发人员利用得**比较多的便是String类的intern()方法**。 既然运行时常量池是方法区的一部分,自然**会受到方法区内存的限制**,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常。以下异常与常量池有关: 在装载Class 文件时,如果常量池的创建需要比Java 虚拟机的方法区中需求更多的内存时,将会抛出OutOfMemoryError异常。 #### **为什么要把Java 虚拟机堆和Java 虚拟机栈区分出来呢? Java 虚拟机栈中不是也可以存储数据吗?** * (1)从**软件设计的角度**看, **JVM栈**代表了**处理逻辑**,而**JVM堆**代表了**数据**。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。 * (2) JVM堆与JVM栈的分离,使得JVM堆中的内容可以被多个JVM栈共享(也可以理解为多个线程访问同一个对象) 。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面, JVM堆中的共享常量和缓存可以被所有JVM栈访问,节省了空间。 * (3) JVM栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。但是由于JVM栈只能向上增长,就会限制住JVM栈存储内容的能力。而JVM堆不同, JVM堆中的对象是可以根据需要动态增长的,因此JVM栈和JVM堆的拆分,使得动态增长成为可能,相应JVM栈中只需记录JVM堆中的一个地址即可。 * (4) 面向对象就是JVM堆和JVM栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是, 面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在JVM堆中;而对象的行为(方法),就是运行逻辑,放在JVM栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。 #### **堆和栈的合作** Java 的**堆是一个运行时数据区**, 类的对象从中分配空间。这些对象通过new 、newarray 、anew aπay 和mu ltianewarray 等指令建立,它们**不需要程序代间来显式的释放**。堆是**由垃圾回收来负责的**。 优势:可以**动态地分配内存大小**,生存期也不必事先告诉编译器,因为**它是在运行时动态分配内存的**, Java 的垃圾收集器会自动收走这些不再使用的数据。 缺点:由于要在运行时动态分配内存,**存取速度较慢**。 栈的优势:**存取速度比堆要快**,仅次于寄存器, 栈数据可以共享。 缺点:**存在栈中的数据大小与生存期必须是确定的**,缺乏灵活性。 栈中主要存放一些基本类型的变量数据(int,short, long, byte, flo时, double, boolean, char)和对象句柄(引用) 。 栈有一个很重要的特殊性, 就是存在校中的数据可以共享。假设我们同时定义: ~~~ int a = 3; int b = 3; ~~~ 编译器先处理int a= 3 : 首先它会在栈中创建一个变量为a 的引用,然后**查找栈中是否有3这个值**, 如果没找到, 就将3 存放进来, 然后将a 指向3 。接着处理int b = 3 : 在创建完b 的引用变量后, 因为在栈中已经有3 这个值, 便将b 直接指向3 。这样,就出现了a 与b 同时均指向3 的情况。如果这时再令a=4 : 那么编译器会重新搜索栈中是否有4 值,如果没有,则将4 存放进来, 并令a 指向4 : 如果已经有了, 则直接将a 指向这个地址。因此a 值的改变不会影响到b 的值。 **要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的**,因为这种情况a 的修改并不会影响到b ,它是由编译器完成的, 有利于节省空间。但是如果一个对象引用变量修改了这个对象的内部状态, 就会影响到另一个对象引用变量。 **重点**: **※※※※※ String** 在Java 中, String 是一个特殊的包装类数据。可以用如下两种的形式来创建。 ~~~ String str1= new String("abc"); String str2="abc"; ~~~ 其中第一种是用new ()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对 象。而第二种是先在栈中创建一个对String 类的对象引用变量str2,然后通过符号引用去**字符串常量池里**找有没有“abc”,如果没有,则将“ abc ”存放进字符串常量池, 并令str2指向“ abc ”,如果已经有“ abc ”则直接令str2 指向“abc”。 **比较类里面的数值是否相等时使用equals()方法;当测试两个包装类的引用是否指向同一个对象时, 用==** 下面用例子说明上面的理论。 ~~~ String str1= new String("abc"); String str2= new String("abc"); String str3="abc";//单独执行该行代码,不一定保证创建了String类的对象str3,很可能已经存在该对象,str3只需要指向它就行 String str4="abc"; System.out.println(str1==str2);//false System.out.println(str3==str4);//true ~~~ 看出str3 和str4 是指向同一个对象的. 用new 的方式的功能是生成不同的对象, 每一次生成一个。因此用第二种方式创建多个“abc”字符串, 在内存中其实只存在一个对象而已。这种写法有利于节省内存空间,同时它可以在一定程度上提高程序的运行速度, 因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于代码`String str = new String(“abc”);`,则**一概在堆中创建新对象**,而不管其字符串值是否相等, 是否有必要创建新对象,从而加重了程序的负担。 另一方而,要注意在使用诸如`String str = "abc" ; `的格式定义类时,总是想当然地认为,创建了String类的对象str 。此时会担心对象可能并没有被创建,而可能只是指向一个先前已经创建的对象。只有通过方法new()才能保证每次都创建一个新的对象。 由于String类的immutable性质, **当String变量需要经常变换其值时,应该考虑使用StringBuffer类以提高程序效率**。因为String 不属于8 种基本数据类型,String是一个对象。所以对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其他对象没有的一些特性。由此可见,**new String()和new String("")都是申明一个新的空字符串,是空串不是null** 。 **示例1**: ~~~ String s0 = "kvill"; String s1 = "kvill"; String s2 = "kv" + "ill"; System.out.println(s0 == s1);//true System.out.println(s0 == s2);//true ~~~ **Java 会确保一个字符串常量只有一个复本**。因为上述例子中的s0 和s1中的“kvill”都是字符串常量,它们在编译期就被确定了,所以s0==sl 为true;而“ kv ”和“ill ”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2 也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中“ kvill ”的一个引用。所以我们得出`s0==s1==s2` ;用new String()创建的字符串不是常量,不能在编译期就确定,所以new String()创建的字符串不放入常量池中,它们有自己的地址空间。 再看下面的代码: ~~~ String s0 = "kvill"; String s1 = new String("kvill"); String s2 = "kv" +new String("ill"); System.out.println(s0 == s1);//false System.out.println(s0 == s2);//false System.out.println(s1 == s2);//false ~~~ 运行结果如下。 ~~~ false false false ~~~ 在上述代码中, s0还是常量池中"kvill"的应用, s1 因为无法在编译期确定,所以是运行时创建的新对象"kvill"的引用,s2 因为有后半部分new String("ill")所以也无法在编译期确定,所以也是一个新创建对象"kvill"的应用,明白了这些也就知道为何会得出此结果了。 另外,存在于.class 文件中的常量池,在运行时被Java 虚拟机装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法; 当一个String 实例str 调用intern()方法时, Java 查找常量池中是否有相同Unicode的字符串常量,如果有则返回其的引用,如果没有则在常量池中增加一个Unicode等于str的字符串并返回它的引用。 请看下面的演示示例 ~~~ String s0 = "kvill"; String s1 = new String("kvill"); String s2 = new String("kvill"); System.out.println(s0 == s1);//false System.out.println("**********"); s1.intern(); s2 = s2.intern();//把常量池中"kvill"的引用赋给s2 System.out.println(s0 == s1);//false,虽然执行了s1.intern(),但它的返回值没有赋给sl System.out.println(s0 == s1.intern());//true,说明s1.intern()返回的是常量池中"kvill"的引用 System.out.println(s0 == s2);//true ~~~ 另外,很多人认为使用String.intern()方法可以将一个String 类保存到一个全局String 表中,如果具有相同值的Unicode 字符串己经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中,这是错的。也就是说如果将这个全局的String 表理解为常量池的话,如果在表中没有相同值的字符串,则不能将自己的地址注册到表中。 请看下面的演示示例。 ~~~ String s1 = new String("kvill"); String s2 = s1.intern(); System.out.println(s1==s1.intern()); System.out.println(s1+"////"+s2); System.out.println(s2 == s1.intern()); ~~~ 运行结果如下。 ~~~ false kvill////kvill true ~~~ 在这个类中我们没有声名一个"kvill"常量,所以常量池中一开始是没有"kvill"的,当我们调用**s1.intern**()后就在常量池中新添加了一个"kvill"常量,原来的不在常量池中的"kvill"仍然存在,也就不是“将自己的地址注册到常量池中”了。 `s1==s1.intern()`为false 说明原来的"kvill"仍然存在; s2现在为常量池中"kvill"的地址,所以有`s2 == s1.intern()`为true 。 **通过使用equals(), String 可以比较两字符串的(内容)Unicode 序列是否相当,如果相等返回true 。而“==”是比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。** String的实例一旦生成就不会再改变了,比如下面的语句。 ~~~ String str="kv"+"ill"+" "+"ans"; ~~~ 上述str有4 个字符串常量, 首先"kv"和"ill"生成了"kvill"存在内存中,然后"kvill"又和" "生成“ kvill ”存在内存中, 最后又和生成了“ kvill ans ” 。并把这个字符串的地址赋给了str, 就是**因为String 的“ 不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了**,因为StringBuffer 是可改变的。 #### **垃圾收集** 参考[Java虚拟机二:垃圾回收机制](http://blog.csdn.net/yulong0809/article/details/77421615) **何谓垃圾收集** 垃圾收集(Garbage Collection, GC)提供了内存管理的机制,使得应用程序不需要关注内存是如何释放的,内存用完后, 垃圾收集会进行收集,这样就减轻了因人为管理内存而造成的错误,比如在C++语言里, 出现内存泄漏是很常见的。 Java 是目前使用最多的依赖于垃圾收集器的语言,在堆里面存放着Java 肚界中几乎所有的对象,在垃圾回收前首先要确定这些对象之中哪些还在存活, 哪些己经“死”了,即不可能再被任何途径使用的对象。 **常见的垃圾收集策略** 所有的垃圾收集算法都面临同一个问题,那就是**找出应用程序不可到达的内存块,并将其释放**。这里的**不可到达主要是指应用程序已经没有内存块的引用了**,而在Java 中,某个对象对应用程序是可到达的是指这个对象被根(根主要是指类的静态变量,或者活跃在所有线程拢的对象的引用)引用或者对象被另一个可到达的对象引用。 * **Reference Counting (引用计数)** 引用计数是最简单直接的一种方式, 这种方式**在每一个对象中增加一个引用的计数, 这个计数代表当前程序有多少个引用引用了此对象, 如果此对象的引用计数变为0 , 那么此对象就可以作为垃圾收集器的目标对象来收集**。 这种策略的**优点是简单、直接,不需要暂停整个应用。**其**缺点是需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作**,引用计数会影响执行效率,每引用一次都需要更新引用计数,比如每次将对象赋值给新的引用,或者对象的引用超出了作用域等, 并**且不能处理循环引用的问题**,即另外一个问题就是**引用计数不能解决交叉引用,或者环形引用的问题**。比如在一个环形链表里, 每一个元素都引用前面的元素,这样首尾相连的链表, 当所有元素都变成不需要时, 就没有办法识别出来,并进行内存回收。如下图所示: ![](https://box.kancloud.cn/4ef209644471a5741ec030b88019f9eb_833x339.png) A引用B,B又引用A,A和B都是不可达的,这时他们已经成为垃圾对象了,但是在引用计数的场景下,A和B并不能被GC回收。 * **可达性算法** ![](https://box.kancloud.cn/30974270dc4c61dfa8c43ee0ad2e1de3_1001x512.png) 在主流的商用程序语言的主流实现中,都是称通过可达性分析来判定对象是否存活的。 基本思想:通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下探索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话,就是从GC Roots到找个对象不可达)时,则证明此对象是不可用的。如上图中的ObjD、ObjE虽然关联,但是他们到GC Roots是不可达的,所以它们将会被判定为可回收的对象。 在Java语言中,可作为GC Roots的对象包括以下几种: * 虚拟机栈(栈帧中的本地变量表)中引用的对象; * 方法区中类静态属性引用的对象; * 方法区中常量引用的对象; * 本地方法栈中JNI(即一般说的Native方法)引用的对象; 总结就是,方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。 **引用的类型** * 强引用、软引用、弱引用、虚引用 * 最常使用的就是强引用和弱引用 ~~~ Object obj =new Object(); WeakReference<Object> wf=new WeakReference<Object>(obj); obj=null; wf.get(); ~~~ 上面的obj就是强引用。wf就是弱引用。 Java在1.2以后将引用分为了4种类型,即强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。 * **强引用**:一般就是我们平常写代码触发new的动作产生的对象。例如 `Object obj = new Object();` obj即为强应用,**只要强引用还引用着对象,GC就不会回收被引用的对象。** * **软引用(SoftReference)** : 一般就是我们目前还有点用,但是也不是必须的对象,**软引用所关联的对象在系统要发生内存溢出的时候,将会对这类对象进行二次回收,如果还是回收失败才会抛出oom**; * **弱引用(WeakReference)** : 和软引用的意义类似,但是比软引用更弱,**被软引用关联的对象,只能生存到下一次GC发生之前,当GC触发时无论内存是否足够都会回收掉该类对象**。 * **虚引用(PhantomReference)**:任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。 **垃圾回收算法** * **标记-清除**算法 ![](https://box.kancloud.cn/7a26efe9d8d226b3ac512057bdc28ea2_547x404.png) 是**最基础的收集算法**,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的 **思想** 先标记可以被清除的对象,然后统一回收被标记要清除的对象,标记过程中会对被标记的对象进行筛选,筛选就是判断是否有必要执行执行对象的finalize()方法,当对象没有重写finalize()方法或者finalize已经被虚拟机调用过的话,就被判定为不需要执行。 如果对象需要执行finalize方法的话那么这个对象将会加入到一个叫F-Queue的队列当中,然后会在一个优先级较低的Finalizer的线程中去执行finalize方法,但是虚拟机并不保证这个对象finalize的方法会完全执行完,因为可能在这个方法中你干了很耗时的操作的时候会导致整个F-Queue队列的其他对象都在等待或者导致系统崩溃。如果对象在finalize方法中将自己赋值给某个类的变量或者对象的成员变量,那么这个对象将逃脱这次的GC。对象的finalize方法只会被虚拟机调用一次,第二次再触发GC的时候不会在去调用finalize方法。 **缺点** 标记清除算法最大的缺点是空间问题,在垃圾回收之后会产生大量的内存碎片,而如果内存碎片多了,当我们再创建一个占用内存比较大的对象时就没有足够的内存来分配,那么这个时候虚拟机就还要再次触发GC来清理内存后来给新的对象分配内存。 缺点是效率问题,标记和清除两个过程的效率都不高; ![](https://box.kancloud.cn/790cbd2ffa00ab65373639e40b73a013_635x357.png) * **复制算法** ![](https://box.kancloud.cn/155aa93dfc3a32b524a08c59ab6af2aa_619x390.png) 复制算法会将内存空间平均分为大小相等的两块,每次只使用其中的一快,当触发GC操作的时候,会将存活的对象复制到另一个区域当中,然后将整块区域情况,这种算法最大的缺点是将原有内存分成了两块,每次只能使用区中一块,也就是损失了50%的内存空间,代价有点大。 ![](http://img.blog.csdn.net/20170821121114896?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVsb25nMDgwOQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) * **标记整理算法** ![](https://box.kancloud.cn/4699c08152298502d3d58fe6449ade16_494x426.png) **背景**: 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 **思路** 标记整理算法一般用在老年代中,因为在老年代如果也采用复制算法的话,第一会浪费一部分内存,第二是当存活对象较多的时候会进行大量的复制,这样会影响效率。所以提出了标记整理算法,标记过程还和标记清除一样,然后在清理的时候是先将存活的对象全部像一边移动,然后再清理掉边界以外的内存。 ![](http://img.blog.csdn.net/20170821123857857?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVsb25nMDgwOQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 三种算法相对各有优劣,在JVM三者结合使用,并不是只使用某一种算法。 * **分代收集算法** : 目前大部分商业虚拟机都使用了分代收集算法,它的思想是根据对象的存活周期将内存分为了新生代和老年代,在每个年代中采用合适的收集算法,这样更能提升效率。例如新生代中的对象大多都是很快就会死去,只有少量的存活,那么就采用复制算法,这样可以付出少量复制的成本就可以完成收集,而且可以解决内存碎片的问题。而老年代一般对象的存活率较高,不能浪费内存空间,所有一般采用标记清除或者标记整理算法。 **在什么时候进行垃圾回收** * Java虚拟机无法在为新的对象分配内存空间了 * 手动调用System.gc()方法(强烈不推荐) * 低优先级的GC线程,被运行时就会执行GC #### **DVM和JVM的不同** * **DVM** 执行的文件不同,一个是class,一个是dex 类加载的系统与JVM却别较大 JVM只能存在一个,可以同时存在多个DVM DVM基于寄存器,而JVM基于栈的 ![](https://box.kancloud.cn/778b1c93ced76c5510d462a07acb03c7_847x350.png) #### **DVM和ART区别** * DVM使用JIT来将字节码转换成机器码,效率低 * ART采用了AOT预编译技术,执行速度更快 * ART会占用更多的应用安装时间是存储空间 ### **参考链接**: 关于JVM、DVM、ART三者的区别,可参考以下文章: [Android系统的体系结构](https://www.kancloud.cn/alex_wsc/android/344866) [Android运行时ART简要介绍和学习计划](http://blog.csdn.net/luoshengyang/article/details/39256813) [Android ART运行时无缝替换Dalvik虚拟机的过程分析](http://blog.csdn.net/luoshengyang/article/details/39256813) [Android运行时ART执行类方法的过程分析](http://blog.csdn.net/luoshengyang/article/details/40289405) [JAVA虚拟机、Dalvik虚拟机和ART虚拟机简要对比](http://blog.csdn.net/jason0539/article/details/50440669) [ Android开发——JVM、Dalvik以及ART的区别]( http://blog.csdn.net/SEU_Calvin/article/details/52354964)