目录
一、java内存区域与内存溢出异常
1、运行时数据区域
1.1 程序计数器(线程私有)
1.2 java虚拟机栈(线程私有)
1.3 本地方法栈(线程私有)
1.4 Java堆(线程共享)
1.5 方法区(线程共享)
1.6 运行时常量池(线程共享)
2、Java堆溢出
二、垃圾回收与内存分配策略
1、垃圾回收
1.1 何判断对象已死
1.2 finalize( )方法:
1.3 引用类型
1.4 回收方法区
1.5 垃圾回收算法
1.6 垃圾回收器
2、内存分配策略
2.1 对象优先在Eden上分配
2.2 大对象直接进入老年代
2.3 长期存活对象进入老年代
2.4 动态对象年龄判定
2.5 空间分配担保
三、java内存模型
1、主内存与工作内存
四、volatile变量的特殊规则
1、保证此变量对所有线程可见性
2、使用volatile变量的语义是禁止指令重排
JVM会在会在java程序运行的过程中,将它所管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用途,各有各的创建于销毁时间,有的区域随着JVM的创建与启动而存在,有的区域依赖用户线程的启动与结束而创建和销毁。一般来说,JVM锁管理的区域包含以下几个运行时数据区域:
线程私有:程序计数器、java虚拟机栈、本地方法栈
线程共享:java堆、方法区、运行时常量池,直接内存
程序计数器是一块较小的内存空间;
如果当前线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;
如果正在执行的是一个Native方法,这个计数器值为空。
所谓的线程私有,就是说各个线程的计数器独立存储,互不影响。
虚拟机栈描述的是java方法执行的内存模型:每一个java方法执行的同时,都会创建一个栈帧用于存放局部变量表,操作数栈,动态链接,方发出口等信息。每一个方法从调用到执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。生命周期与线程相同。
之前一直说的栈区域实际上就是此处的虚拟机栈,再详细一点,就是虚拟机栈中的局部变量表。
局部变量表:存放了编译器所知的各种基本数据类型(8中基本数据类型),对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这些方法需要在栈帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。
在这个区域会产生以下两种异常:
本地方法栈与java虚拟机栈的作用是一样的,两者的区别是本地方法栈为虚拟机使用Native方法服务,而虚拟机栈为虚拟机使用java方法服务。
本地方法(Native方法):使用一些其他语言(C, C++ 或 汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
在Hotspot虚拟机中,Java虚拟机栈和本地方法栈是同一块内存区域。
Java堆是JVM所管理的最大区域,Java堆是所有线程共享的一块区域,在JVM启动时创建。此内存存放的都是对象实例。JVM规范中说到:所有的对象实例以及数组都要在堆上分配。
java堆是垃圾回收器管理的主要区域,因此又叫“GC 堆”。根据JVM规范规定的内容:Java堆可以处于物理上内存不连续的内存空间中。Java堆在主流的虚拟机中都是可扩展的(-Xmx设置最大值, -Xms设置最小值)。
如果堆中没有足够的内存完成实例分配并且对也无法在扩展时,会抛出OOM(OutOfMemoryError)异常。
方法区用于存放已被加载过的类信息,常量,静态变量,即时编译器编译(它把字节码转换为可执行的机器码)后的代码等数据。
和堆一样不需要连续的内存,可以动态扩展,当方法区无法满足动态内存需求时(扩展失败),一样会抛出OutOfMemoryError异常
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难以实现。
在JDK8之前的HotSpoot虚拟机中,方法区也被称为“永久代”(永久代并不是意味着进入方法区之后就永久存在),但是永久代的大小很难确定,因为它收很多因素的影响,并且每次FullGC后永久代的大小都会改变,所以经常会抛出OutOfMemoryError异常,为了更方便管理方法区,JDK8移除永久代,并把方法区移至元空间,它位于本地方法中,而不是虚拟机内存中。
运行时常量池是方法区的一部分,存放了 字面量 和符号引用
字面量:字符串(JDK1.7之后移到堆中)、final常量、基本数据类型的值
符号引用:类和结构的完全限定名,字段的名称和描述符、方法的名称和描述符。
在上面说过了Java堆是用来存放对象实例的,只要我们不停创建对象,并且保证GC Roots到对象之间有可达路径来避免GC清除这些对象,那么在对象数量达到最大对容量后就会产生内存溢出异常。
Java堆内存的OOM异常是实际应用中最常见的内存溢出情况。当出现Java堆内存溢出时,异常堆栈信息java.lang.OutOfMemoryError会进一步提示Java heap space。当出现Java heap space 时,表示OOM发生在堆上。
内存溢出和内存泄漏:
关于在这之前的Java运行时内存各个区域的分析:程序计数器、虚拟机栈、本地方法栈这三个部分的生命周期与线程相关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存自然就跟着线程回收了。因此下面说的内存分配和回收主要是针对Java堆区和方法区这两个区域。
为对象添加一个 引用计数器,当对象增加一个引用时计数器加一;当对象减少一个引用时计数器减一;引用对象为0时对象可以被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为0,导致无法对他们进行回收。正是由于这种循环引用的存在,Java虚拟机中不采用这种算法。
/**
* 循环引用举例
* */
public class Test
{
public Object instance = null;
public static void main (String[] args)
{
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
}
}
在Java虚拟机中,采用的是可达性分析算法来判断对象该不该被回收。
此算法的核心思想是:通过一系列称为“GC Roots”的对象作为起始点向下搜索,搜索走过的路径称为“引用链”,当一个对象到GCRoots没有任何引用链时(不可达),表名此对象不可用。
在java语言中,虚拟机使用可达性分析法判断对象是否可以被回收,GC Roots一般包含以下内容:
• 虚拟机栈中局部变量表中引用的对象
• 本地方法栈中JNI(Native方法)引用的对象
• 方法区中类静态属性引用的对象
• 方法区中常量引用的对象
上面之所以说的是可能会被回收,主要是因为还有一个finalize方法:
即使一个对象在可达性分析算法中不可达,这个对象也并非 “非死不可” ,这时候它只是出于暂缓 “行刑” 的一个阶段要宣告一个对象真正死亡,至少还要经过两次的标记过程:
注意:finalize方法只会执行一次,当一个对象快死了,它有可能直接死亡或者被finalize救活,当它被拯救后再一次快死亡的时候就直接死亡,不会再调用finalize方法。
在JDK1.2之前,java中的引用定义很传统:如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。(这种定义很狭隘,一个对象在这种定义下只有引用和被引用两种状态)
在JDK1.2之后,对引用的概念进行了扩充,将引用分为强引用, 软引用, 弱引用和虚引用 四种,这四种引用强度依次递减。
Object obj = new Onject();
SoftReference
Object obj = new Onject();
WeakReference sf = new WeakReference(obj);
sf = null;//使对象只被弱引用关联
Object obj = new Onject();
PhantomReference sf = new PhantomReference(obj);
sf = null;//使对象只被虚引用关联
方法区的垃圾回收主要收集两部分内容:废弃常量和无用的类。
回收废弃常量 : 和回收java堆中的对象十分类似:以常量池中的直接字面量为例:假如现在一个字符串“abc”已经进入了常量池,但是当前系统中没有一个String类对象引用常量池中的“abc”常量,也没有在其他地方引用这个字面量,如果此时发生GC且有必要的话,这个“abc”常量就会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判断一个类是否是无用类:需要同时满足一下三个条件
满足以上三个方法也仅仅是"可以",而不是"必然"
标记清除算法
标记清除算法:就是先遍历一遍标记出所有需要回收的对象,在遍历一遍回收所有标记的对象。
两个不足之处:
◆ 效率问题:遍历两次,效率低
◆ 标记清除后会产生大量碎片空间,导致程序需要分配一个大对象时没有足够的连续空间而不得不提前触发一次垃圾回收
复制算法(新生代回收算法)
复制算法:将内存按容量大小分为大小相等的两块,每次只使用其中的一块。当这一块需要垃圾回收时,会将此区域上的存活对象复制到另一块上面,然后把已经使用过的内存清理掉。这样做的好处是每次只对整个半区进行垃圾回收,但同时每次只能使用一半的内存。
现在的商业虚拟机都是采用的这种算法来收集新生代的,但并不是将内存划分为两块一样的空间,而是一块较大的Eden和两块较小的Survivor空间。每次只是用Eden和其中的一块Survivor,垃圾回收时将Eden和使用的Survivor中存活的对象复制到另一块Survivor上,在清除Eden和刚刚使用的Survivor的空间。
当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。
Hotspot默认Eden和Survivor的大小比例是 Eden :Survivor :Survivor = 8 :1 :1
复制算法在存活率高的时候会进行较多的复制,效率低下,因此在老年代一般不用此方法,老年代使用标记-整理算法
标记整理算法(老年代回收算法)
标记-整理算法:标记过程与标记-清除一致,但后续步骤不是直接清理,而是将存活的对象都向一端移动,然后直接清理掉端边界以外的部分。
分代收集算法
分代收集算法:当前JVM都采用的是分代收集算法,这个算法并没有什么新思想,而是根据对象的存活周期将内存分为不同的几个块。
一般情况下Java堆分为新生代和老年代。
常见问题: 请问了解Minor GC和Full GC吗?
- Minor GC 又称为新生代GC,指的是发生在新生代的垃圾收集。因为java对象大多数都是 “朝生夕死” 的对象,因此Minor GC非常频繁,一般回收速度也比较快;
- Full GC 又称为老年代GC或者Major GC,指的是发生在老年代的垃圾收集,出现Major GC,经常会伴随至少一次的Minor GC(并非绝对:在Parallel Scavenge收集器中就有直接进行Full GC 的策略选择过程)。Major GC至少比Minor GC慢10倍以上。
Hotspot虚拟机中的7个垃圾收集器如下图(连线表示各拉机器可以配合使用):
串行:垃圾收集器与用户线程交替执行
并行:垃圾收集器与用户线程同时执行
单线程:垃圾收集器只使用一个线程
多线程:垃圾收集器使用多个线程
吞吐量:CPU运行用户线程的时间与CPU总消耗时间的比值
引用场景:Serial收集器是虚拟机运行在Client模式下的默认新生代收集器
优点是:简单高效,在单个CPU下没有线程交互的开销,因此拥有最高的单线程收集效率
注意:它的单线程并不意味着他只会使用一个CPU或一条收集线程去完成垃圾收集工作,而更重要的是它在进行垃圾收集时,必须 暂停其他所有工作线程,直到它收集结束。
ParNew收集器是许多运行在Server模式下的虚拟机中首选新生代收集器
Parallel Scavenge收集器是新生代收集器,并行GC,使用的是复制算法
Parallel Old收集器:老年代收集器,并行GC,是Parallel Scavenge的老年版本,多线程,使用标记 - 整理算法
应用场景:在注重吞吐量以及CPU资源敏感的场合,可以考虑使用 Parallel Scavenge加Parallel Old收集器
优点:并发收集,低停顿
缺点:CMS收集器对CPU资源非常敏感,面向并发设计的程序都对CPU资源比较敏感
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC产生。
G1收集器它是一款面向服务端的垃圾回收器,在多CPU和大内存的场景下有很好的性能。
堆被分为新生代和老年代,其他收集器在进行垃圾收集时收集范围都是整个新生代或者整个老年代,而G1可以直接对新生代和老年代一起回收。
G1把堆分为多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过region的概念的引入,从而将原来一整块内存空间划分为多个小空间,使得每个小空间可以单独进行垃圾回收,这种划分方法带来的很大的灵活性,使得可预测的停顿时间模型变成可能。通过记录每个region垃圾回收时间以及回收所得的空间,并维护一个优先列表,每次根据允许的收集时间优先回收价值最大的region。
G1具备如下特点:
大多数情况下,对象在新生代Eden中分配。当Eden中没有足够的空间进行分配时,虚拟机将发生一次Minor GC。
大对象:指的是需要大量连续空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来放置大对象。
虚拟机提供了一个-XX:pretenureSizeThreshold参数,令大于这个设置的值的对象直接在老年代分配。这样做的目的是在于避免Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)
虚拟机给每个对象定义了一个对象年龄计数器(Age)。如果对象在Eden出生斌经过一次Minor GC后仍然存活,并且能被Survivor容纳,将被移动到Survivor空间中,并且把对象年龄设为1,对象在Survivor中每熬过一次Minor GC,年龄就会增加一岁。当他的年龄增加到一定程度,将晋升到老年代中,对象晋升到老年代中的年龄阈值,可以通过-XX:MaxTenuringThreshold设置。
为了更好的适应不同程序的内存状况,JVM并不是永远要求对象的年龄达到MaxTenuringThreshold才能景升到老年代。如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。无需等到MaxTenuringThreshold的年龄要求。
在发生Minor GC之前,虚拟机会检查老年代最大连续的可用空间是否大于新生代所有对象的总空间,如果大于,则此次MinorGC是安全的;如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果HandlePromotionFailure=true,,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。如果大于,则尝试进行一次Minor GC,但这次Minor GC是有风险的;如果小于或者HandlePromotionFailure = false,则改为进行一次Full GC。
JVM定义了一种java的内存模型(JMM),用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各个平台下都能达到一致的内存访问效果。(C/C++直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台下的内存模型的差异,可能导致程序在不同平台上运行出现并发访问错误)
java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存入内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段,静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为或两者线程是私有的,不会被线程共享。
java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存拷贝副本,线程对变量的所有操作(读取,赋值等等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存和工作内存之间的关系如下:
主内存与工作内存之间的具体交互协议,即一个变量如何从主内存中拷贝到工作内存、如何从工作内存中同步会主内存之类的实现细节,java内存模型中定义了如下八种操作来完成。JVM实现时必须保证下面体积的每一种操作的原子性(不可再分)
Java内存模型的三大特性:
volatile定义的变量,保证此变量对所有线程的可见性,这里的可见性指的是:当一条线程修改了这个变量的值,新值对于其他线程来说是立即得知的。(普通变量做不到这一点:普通变量的值在线程间传递均需要通过主内存来完成,例如 线程A修改一个普通变量的值,然后像主内存中进行回写,另一条线程B在线程A将新值写回主内存之后再从主内存中进行读取操作,新值才会对线程B可见)
volatile变量在各个线程中是一致的,但volatile变量的运算在并发下一样是不安全的,因为在java中并非原子操作。
volatile变量只保证可见性,在不符合以下两条规则的运算场景中,我们仍需要通过加锁(synchronized或lock)来保证原子性
volatile关键字的禁止指令重排序有一下意思:
//flag 为 volatile 变量
x = 2; // 语句1
y = 3; // 语句2
flag = true; // 语句3
x = 4; // 语句4
y = 5; // 语句5
// 此时不能将语句3放到语句1,2前面;也不能将语句3放到4,5后面;
// 但是此时语句1,2的顺序、语句4,5的顺序不会做任何保证