JVM运行时数据区主要分为以下几个:
程序计数器:
当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器。这是JVM规范中唯一一个没有规定会导致OutOfMemory(内存泄露,下文简称OOM)的区域。
虚拟机栈:
这块内存区域就是我们常常说的“栈”,它用于存放变量。
本地方法栈:
本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。
Java堆:
对象实例以及数组内存都要在堆上分配。堆是共享的一块区域,它用来存放对象实例,也是垃圾回收GC的主要区域。
方法区:
方法区是共享的区域,储存已被虚拟机加载的类信息、常量、静态变量等数据。
可见在jdk8中 字符串常量由永久代转移到堆中。
运行时常量池:
是方法区一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容在加载后存放到方法区的运行时常量池中。
直接内存:
NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理内存的类DirectBuffer,直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
对象的访问:
例:Object obj = new Object(); Object obj 会放在Java栈的本地变量表中,作为一个reference类型数据出现。 new Object()会放在堆中。在Java堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
句柄访问:
Java堆中会划分一块句柄池,reference储存的就是对象的句柄地址,句柄中包含了对像实例数据,类型数据的地址信息。
直接指针访问:
reference中直接储存的就是对象的地址;
垃圾回收
垃圾回收算法,
标记清除法:首先标记出所有需要回收的对象,在标记完成后统一回收。
问题:效率问题;空间问题,会产生大量内存碎片。
复制算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
问题:将内存缩小到原来的一半,太高了一点。
常用于回收新生代,
标记整理算法:
让所有存活的对象都向一端移动,直接清理掉端边界以外的内存。可用于老年代回收。
分代收集算法:
堆分区分为新生代,老年代;老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收;
新生代回收算法:
新生代分为Eden,s0,s1区,新生成的对象放在Eden区,回收时把Eden区存活的放在s0区,清空Eden;当s0满时候把Eden和S0存活的对象放在s1区,清空那两个区;s0和s1交换,保持s1时空的;
当s1空间不足放Eden,s0的存活对象,就将存活对象放到老年代,老年代满了就进行Full Gc;
老年代回收算法:
在年轻代经历过N次的垃圾回收仍存活的对象放到老年代;满了时候进行Full Gc ;
收集器
垃圾收集器是内存回收的具体体现,两个收集器之间有连线,代表可以搭配使用。
Serial(串行)收集器:
新生代收集器,使用复制算法,它进行垃圾回收时候,必须暂停其他所有工作线程,直到收集结束。JVM client模式下默认的新生代收集器。
优点:简单高效,单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
ParNew(并行GC)收集器:
ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。
Parallel Scavenge(并行回收GC)收集器:
也是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器;目标则是达到一个可控制的吞吐量。无法与CMS收集器一起使用;是server级别默认采用的GC方式;
Serial Old(串行GC)收集器:
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
Parallel Old(并行GC)收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS(并发)收集器:
是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记一清除”算法实现的,整个过程分为4个步骤,包括:
初始标记;并发标记;重新标记;并发清除;
**优点:**并发收集,低停顿;
缺点:
CMS收集器对CPU敏感,当CPU不足4个时候影响较大;
无法处理浮动垃圾;一部分垃圾出现在标记过程之后,这部分垃圾只能下次回收。因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
收集结束之后会产生内存碎片;可以通过 XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
G1收集器:
G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。另外G1将整个Java堆(包括新生代,老年代)进行回收。
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。
G1将新生代,老年代的物理空间划分取消了。G1算法将堆划分为若干个区域(Region)。它仍然属于分代收集器。G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。如下图:
Humongous区域:如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。
PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。
G1提供了两种GC模式,Young GC和Mixed GC
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分2步:
- 全局并发标记(global concurrent marking)
- 拷贝存活对象(evacuation)
global concurrent marking的执行过程分为五个步骤:- 初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
- 初始标记(initial mark,STW)
- 根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。 - 并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断 - 最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。 - 清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
特点:
并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间。
分代收集:与其他收集器一样,分代概念在G1中依然得以保留。
空间整合: G1从整体来看是基于“标记—整理”算法实现的收集器,G1运作期间不会产生内存空间碎片。
可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
哪些内存需要回收
引用技术算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1,只要计数器为0的对象就是不可能被使用的。
优点是可以很快执行,缺点是相互引用时候永远不能是0。
可达性分析算法:如果一个对象不可达,那么就是可以回收的;它是通过一系列称为“GC Roots” 的对象作为起始点,当一个对象到 GC Roots 没有任何引用链相接的时候,那么这个对象就是不可达,就可以被回收。
这个GC Root 对象可以是:
1 、 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2、 本地方法栈中JNI(即一般说的native方法)引用的对象。
3、 方法区中的静态变量和常量引用的对象。
一个对象死亡,至少要经历两次标记过程。
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
方法区回收:废弃常量和无用的类。
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
GC是什么时候触发的
那么对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC;
对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次 Full GC。
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。
内存分配策略:
对象主要分配在新生代的Eden区,如果启动本地缓存就优先在TLAB上分配。如使用(Serial /Serial old 或者 parNew /Serial old 收集器)。
大多数情况下,对象在新生代Eden区分配,当Eden区空间不足时候,虚拟机进行一次Minor GC。
大对象直接进入老年代
长期存活的对象进入老年代
类加载机制
类从加载到虚拟机内存中开始,到卸载出内存,生命周期为:
加载 --> 连接(包括 验证、准备、解析)–> 初始化 --> 使用 --> 卸载
有4种情况立即对类进行初始化:
- 遇到new,getstatic,putstatic,invokestatic字节码指令,最常见的是new关键字;
- 使用java.lang.reflect 包的方法对类进行反射掉用。
- 当初始化一个类时候,这个类的父类还没有进行初始化,需先触发父类的初始化;
- 当虚拟机启动时候,用户需要指定一个要执行的主类,虚拟机会先初始化。
除此之外所有的引用都是被动引用,如:
通过子类应用父类的静态字段,不会导致子类初始化;
通过数组来定义引用类,不会触发此类的初始化;
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量类的初始化。
死锁
一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁.
死锁例子:
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
public class DeadLockTest
{
private static Object A = new Object(), B = new Object();
public static void main(String[] args)
{
new Thread(() -> {
System.out.println("线程1开始执行...");
synchronized (A)
{
try
{
System.out.println("线程1拿到A锁");
//休眠两秒让线程2有时间拿到B锁
Thread.sleep(2000);
} catch (Exception e)
{
e.printStackTrace();
}
synchronized (B)
{
System.out.println("线程1拿到B锁");
}
}
}).start();
new Thread(() -> {
System.out.println("线程2开始执行...");
synchronized (B)
{
try
{
System.out.println("线程2拿到B锁");
//休眠两秒让线程1有时间拿到A锁
Thread.sleep(2000);
} catch (Exception e)
{
e.printStackTrace();
}
synchronized (A)
{
System.out.println("线程2拿到A锁");
}
}
}).start();
}
}
预防死锁:
1、避免一个线程同时获取多个锁
2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3、尝试使用定时锁,使用lock.tryLock来代替使用内置锁。
类加载过程:
加载(Loading);
验证(Verification);
准备 (Preparation);
解析(Resolution);
初始化(Initialization);
使用(Using)
卸载 (Unloading)
类加载器
(1) Bootstrap ClassLoader(引导类加载器) : 它用来加载 Java 的核心库
(2) Extension ClassLoader(扩展类加载器) : 它用来加载 Java 的扩展库。
(3) Application ClassLoader或叫System Classloader (系统类加载器): 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。
委托机制:当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。加载这个类的请求由Application类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器。
一篇很好的文章:
http://www.importnew.com/23792.html
内存溢出处理策略:
https://blog.csdn.net/xuqu_volition/article/details/53786096