一、Jvm内存区域
1.程序计数器:较小的一块内存,可看做是当前程序所执行字节码的行数。Java虚拟机的多线程是通过线程切换来并分配处理器实现的,为了切换线程后还能恢复到原来位置,每个线程都需要一个程序计数器来保存当前程序执行情况。而且程序计数器是线程私有的。
2.虚拟机栈:Java方法执行的内存模型,每个方法执行时都会产生一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法执行的过程就是一个栈帧在虚拟机栈的入栈和出栈的过程。对于执行引擎来说,只有栈顶栈帧是有效的,所对相应的方法为当前方法。虚拟机栈是线程私有的。
(1)局部变量:局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,包括基本数据类型、引用和returnAddress类型。局部变量表所需的内存空间在编译期间完成分配。局部变量表的容量以变量槽(Slot)为最小单位。一个 Slot 可以存放一个32位以内的数据类型,对于 64 位的数据类型(long和double),虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始到局部变量表最大的 Slot 数量,对于 32 位数据类型的变量,索引 n 代表第 n 个 Slot,对于 64 位的,索引 n 代表第 n 和第 n+1 两个 Slot。如果是非static方法,slot的第一个位置为对象实例this,然后再按照内部顺序分配空间。
(2)操作栈数:操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。
Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称 Java 虚拟机是基于栈的,这点不同于 Android 虚拟机,Android 虚拟机是基于寄存器的。基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。
(3)动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
(4)方法返回地址:方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令。
在这个区域有两种Java异常:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
3.本地方法栈:类似于虚拟机栈,本地方法栈服务与native方法。本地方法栈是线程私有的。
4.Java堆:是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块区域。在虚拟机启动时创建,目的是存储对象实例,几乎所有的对象实例和数组都在堆上分配。Java堆是线程共享的。如果堆上没有足够的内存将会抛出OutOfMemoryError异常。成员变量存在堆中。
5.方法区:线程共享区域,用于存储已经被虚拟机加载的类的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时将会抛出OutOfMemoryError异常。
对象实例化分析:
Object obj = new Object();
这段代码的执行会涉及 Java 栈、Java 堆、方法区三个最重要的内存区域。obj 会作为引用类型(reference)的数据保存在 Java 栈的本地变量表中,而会在 Java 堆中保存该引用的实例化对象,但可能并不知道,Java 堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中。由于 reference 类型在 Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到 Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是 reference 中存放的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前 Java 默认使用的 HotSpot 虚拟机采用的便是是第二种方式进行对象访问的。
String str = new String("abc");
这个时候创建了几个对象?先在常量池里看看有没有"abc"字符串,如果没有就创建,然后再在堆中创建,入过有就直接在堆中创建。直接赋值的话就直接在常量池操作,不在堆中创建对象。
二、垃圾收集机制
如何判断对象已经死去?
1.引用计数法:给对象添加一个计数器,每当有一个地方引用它时,计数器+1,当引用失效,计数器-1,当对象计数器为0,即为死去对象。
2.可达分析算法:以GC Roots为起点,从这些节点开始向下搜索,搜索走过的路径叫做引用链,当一个对象与GC Roots没有引用链相连即为不可达对象,将会判定为可回收对象。
对象引用
1.强引用:直接用new关键词生成大的对象,这类的引用只要强引用还存在,就不会被回收。
2.软引用:SoftReference。在系统即将发生内存泄漏之前,将会把这些对象进行二次回收。
3.弱引用:WeakReference。当垃圾回收工作时,不管内存是否足够都会进行回收。
4.虚引用:目的是当对象被回收时收到一个系统通知
触发GC工作的条件
1.当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
2.Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
3.显式调用System.gc()通知JVM进行垃圾回收,但真正回收具体在什么时候开始是无法预料的。
垃圾收集算法
1.标记-清除:首先标记出所有需要回收的对象,标记完成后统一回收。缺点:标记和清楚的效率都很低,且,清除后会产生大量的内存碎片。
2.复制算法:将内存容量划分成大小相等的两块,每次只使用其中一块,当这一块内存用完,就将还活着的对象复制到另一半空间,然后把这一块使用过的空间全部清除。缺点:需要占用一般的内存,当死亡的对象数量过少需要进行大量的复制算法。
3.标记-整理算法:与标记-清楚方法类似,只不过不是直接把死去的对象清除,而是把他们移动到内存的一段,然后直接清理掉边界以外的内存。
4.分代收集算法:按照对象的存货周期分成新生代和老年代,针对不同的年代采用不同的算法,也是当前使用最多的算法。