目录
JVM体系结构
JVM各个模块简介
JVM是如何工作的
类加载器子系统
加载
链接
初始化
执行引擎
运行时数据区域
程序计数器(线程私有)
虚拟机栈(线程私有)
本地方法栈(线程私有)
堆
新生代(Toung Generation)
老年代(Old Generation)
元空间(MataSpace)
方法区
直接内存
判断一个对象是否可以被回收
引用计数算法
可达性分析算法
引用类型
强引用(Strong Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
方法区的回收
finalize()
垃圾收集算法
标记-清除
标记-整理
复制回收
分代收集
垃圾收集器
Serial
ParNew
Parallel Scavenge
Serial Old
Parallel Old
CMS
G1
比较
内存分配与回收策略
什么时候进行Minor GC,Full GC
内存分配策略
对象优先在Eden分配
大对象直接进入老年代
长期存活的对象进入老年代
动态对象年龄判定
空间分配担保
Full GC触发条件
调用System.gc()
老年代空间不足
空间分配担保失败
Concurrent Mode Failure
类加载机制
类的生命周期
类初始化时机
主动引用
被动引用
类加载过程
加载
验证
准备
解析
初始化
类加载器
类与类加载器
类加载器分类
双亲委派类型
Java虚拟机工具
jps(JVM Process Status Tool)虚拟机进程监控工具
jstat(JVM Statistics Monitoring Tool)虚拟机统计信息监视工具
jinfo(Configuration Info for Java)配置信息工具
jmap(Memory Map for Java)内存映像工具
jhat虚拟机堆转储快照分析工具
jstack(JVM Stack Trace)java堆栈跟踪工具
jconsole
jvisualvm
虚拟机是无力及其德软件实现。Java的开发遵循write once run anywhere(“一次编写到处乱跑”)理念,它运行在VM(虚拟机)上。编译器将Java文件编译成Java.class文件,之后,将.class文件输入到JVM中,加在并执行该类文件。下图为JVM的体系结构
如上面的体系结构图所示,JVM分为三个主要的子系统:
Java的动态类加载功能是由类加载器子系统处理的。它负责加载、链接,并且在运行时首次引用类的时候初始化类,而不是在编译期间。
这个组件负责加载类。BootStrap类加载器、Extension类加载器和Application类加载器是实现这个功能的三大类加载器。
上述类加载器在加载类文件时遵循委托层次结构算法。
这是类加载的最后阶段,所有的静态变量都将被分配原值,静态代码块将被执行。
运行时数据区被划分为五个主要部分:
分配到运行时数据区的字节码将被执行引擎执行,执行引擎读取字节码并逐一执行。
Java本地接口(JNI):JNI与本地方法库交互,并为执行引擎提供本地方法库。
本地方法库(Native Method Libraries):它是执行引擎所需的本地库集合。
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息,从调用直至完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈和出栈过程。
可以通过-Xss这个虚拟机参数来指定一个程序的Java虚拟机栈内存大小:
java -Xss=512M HackTheJava
该区域可能抛出以下异常:
本地方法一般是用其他语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
本地方法栈与Java虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
所有对象实例都在这里分配内存。
是垃圾收集的主要区域(“GC堆”)。现代的垃圾收集器基本都是采用分代收集算法(因为对象的生命周期不一样),主要思想是针对不同的对象采取不同的垃圾回收算法。虚拟机把Java堆分成以下三块:
当个一个对象被创建时,首先进入新生代,之后有可能被转移到老年代中。
新生代存放着大量的生命很短暂的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:
Java堆不需要连续内存,并且可以动态增加内存,增加失败会抛出OutOfMemoryError。可以通过-Xms和-Xmx两个虚拟机参数来指定一个程序的Java堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms=1024M -Xmx=1024M Test.java
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单来说,所有定义的方法的信息都保存在该区域,此区属于共享区间。
方法区:永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装在进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError : PermGen spave,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
JDK1.8之后:无永久代,常量池在元空间中存储。
在JDK1.4中加入NIO,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对Java堆和方法区进行。
描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器会加一;当引用失效时,计数器值会减一;任何时刻计数器为0的对象就是不可能再被使用的。
缺陷:GC频繁影响性能,很难解决对象间相互循环引用的问题
通过GC Roots作为起点进行搜索,能够到达的对象都是存活的,不可达的对象可被回收。
能够作为GC Roots对象的:
无论是通过引用计算还是可达算法,判定对象是否可被回收都与引用有关。在JDK1.2之后,Java对引用的概念进行了扩充,分为以下三种:
被强引用关联的对象不会被回收,使用new关键字创建为强引用
Object obj = new Object();
被软引用关联的对象只有在内存不够的情况下才会被回收
Object obj = new Object();
SoftReference
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前
Object obj = new Object();
WeakReference
又称为幽灵引用或者欢迎引用。一个对象是否又虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
Object obj = new Object();
PhantomReference pf = new PhantomReference(obj);
obj = null;
Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低:在堆中,尤其在新生代中,常规的应用一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
首先标记出所有需要回收的对象,在标记完成后同一回收所有标记的对象。
不足
标记过程仍然与“标记-清楚”算法一样高,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清除掉边界意外的内存。
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。主要不足是只是用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但并不是将新生代划分为大小相等的两块,而是分为一块较大Eden和两块较小Survivor空间,每次使用Eden空间和其中一块Survivor。在回收时,将Eden和Survivor中海存活着的对象一次性复制到另一块Survivor空间上,最后清理Eden和使用过的那一块Survivor。
HotSpot虚拟机的Eden和Survivor的大小比例是8:1:1,保证了内存的利用率达到90%。如果每次回收有多余10%的对象存活,那么一块Survivor空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
以上HotSpot虚拟机中的7个垃圾收集器,连线表示垃圾收集器可配合使用。
Serial为串行,也就是说它以串行的方式执行,它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
优点:高效,对于单个CPU环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率
他是Client模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒内,只要不是太频繁,这点停顿是可以接受的。
它是Serial收集器的多线程版本。是Server模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了Serial收集器,只有它能与CMS收集器配合工作。
在JDK1.5时期,HotSpot推出了CMS收集器(Concurrent Mark Sweep),它是HotSpot虚拟机中第一款真正意义上的并发收集器。
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,优势并行的多线程收集器。它的目标是达到一个可控制的吞吐量,它被称为吞吐量优先收集器。
停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序地运算任务,主要适合在后台运算而不需要太多交互地任务。
缩短停顿时间以牺牲吞吐量和新生代空间来换取地:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开GC自适应地调节策略,就不许要手动指定新生代地大小等参数了。虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适地停顿时间或最大地吞吐量,这种调节方式称为GC自适应地调节策略。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:
MaxGCPauseMillis参数允许地值是一个大于0的毫秒数,收集器将尽可能地保证内存回收所花费地时间不超过设定值。但GC地停顿时间缩短是以牺牲吞吐量和新生代空间来换取地。停顿时间下降,但吞吐量也降下来了。
GCTimeRatio参数地值是一个大于0且小于100地证书,也就是垃圾收集时间占总时间地比例,相当于吞吐量地倒数。区间1/(1+99)~1/(1+1),即1%~50%。
由于与吞吐量关系密切,Parallel Scavenge收集器经常称为吞吐量优先收集器
-XX:+UserAdaptiveSizePolicy:GC自适应调节此策略,打开参数后,就不需要手工指定新生代地大小等参数了。
Serial Old是Serial收集器地老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的最主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
CMS(Concurrent Mark Sweep),Mark Sweep指的是 标记-清楚 算法。CMS是一款优秀的收集器,主要优点:并发收集、低停顿,Sun公司称之为并发低停顿收集器
流程:
在整个过程中耗时最长的并发标记和并发清楚过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
缺点:
G1的第一篇paper发表于2004年,在2012年才在jdk1.7中可用。oracle官方计划在jdk9中将G1编程默认的垃圾收集器,以替代CMS。
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能。HotSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。
堆被分为新生代和老年代,其他收集器进行收集的范围都是整个新生代或者老年代,而G1可以直接对新生代和老年代一起回收。
G1把堆划分成多个大小相等的独立区域,新生代和老年代不再物理隔离。
通过引入Region的概念,从而将原来的一块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测大的停顿时间模型称为可能。通过记录每个Region垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
具备如下特点:
收集器 | 单线程/并行 | 串行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 |
Serial | 单线程 | 串行 | 新生代 | 复制 | 响应速度优先 | 单CPU环境下的Client模式 |
ParNew | 并行 | 串行 | 新生代 | 复制 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 串行 | 新生代 | 复制 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Serial Old | 单线程 | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后背预案 |
Parallel Old | 并行 | 串行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并行 | 并发 | 老年代 | 标记-清楚 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并行 | 并发 | 新生代+老年代 | 标记-整理+复制 | 响应速度优先 | 面向服务端应用,将来替换CMS |
大多数情况下,对象在新生代Eden区分配,当Eden区空间不够时,发起Minor GC。
大对象指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常会出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在Eden区和Survivor区之间的大量内存复制。
为对象定义年龄计数器,对象在Eden出生并经过Minor GC依然存活,将移动到Survivor中,年龄就增加1岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold用来定义年龄的阈值。
虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor中相同年龄所有对象大小的综合大于Survivor空间的一般,则年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
在发生Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的化,那么Minor GC可以确认是安全的。
如果不成立的化虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那么就要进行一次Full GC。
对于Minor GC,其触发条件非常简单,当Eden空间满时,就会触发一次Minor GC。而Full GC相对复杂,有以下条件:
只是建议虚拟机执行Full GC,但是虚拟机不一定真正执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过-Xmn虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
使用复制算法的Minor GC需要老年代的内存空间作担保,如果担保失败会执行一次Full GC。
执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足(可能GC过程中浮动垃圾过多导致暂时性地空间不足),便会报Concurrent Mode Failure错误,并触发Full GC。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机类加载机制。(类是在运行期间动态加载的)
懒加载:要用的时候再去加载
包括以下7个阶段:
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的动态绑定。
这七个阶段中的:加载、验证、准备、初始化、卸载的顺序时固定的。但他们并不一定时严格同步串行执行,特们之间可能会有交叉,但总是以“开始”的顺序总是按部就班的。至于解析则有可能在初始化之后才开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列5种情况必须对类进行初始化(加载、验证、准备都会随之发生):
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
通过数组定义引用类,不会触发此类的初始化。
SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
加载是类加载的一个阶段。加载过程完成以下三件事:
加载源:
目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
列出正在运行的虚拟机进程,并显示虚拟机执行主类名称,以及这些进程的本地虚拟机唯一ID。
options参数选项说明如下:
-q 不输出类名、Jar名和传入main方法的参数
-m 输出传入main方法的参数
-l 输出main类或Jar的全限名
-v 输出传入JVM的参数
使用jps -lv 查看所有java进程。
这个命令用于监视虚拟机各种运行状态信息。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,虽然没有GUI图形界面,知识提供纯文本控制台环境的服务器上,但它是运行期间定位虚拟机性能问题的首选工具。
这个命令可以实时查看和调整虚拟机各项参数
例如:查看MaxPerm大小
[root@Bill-8 bin]# jinfo -flag MaxPermSize 2788
-XX:MaxPermSize=134217728
用于生成堆转存的快照,一般是heapdump或者dump文件。如果不适用jmap命令,可以使用 -XX:+HeapDumpOnOutOfMemoryError参数,当虚拟机发生内存溢出的时候就会产生快照。或者使用kill -3 pid也可以产生。jmap的作用不仅仅是为了获取dump文件,它可以查询finalize执行队列,java堆和永久代的详细信息,如空间使用率,当前用的哪种收集器。
这个命令用于查看虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
生成线程快照的主要目的是:定位线程出现长时间停顿的原因,入线程间死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的县茨城到底在后台做些什么事情。
可以监视JVM内存的使用情况、线程堆栈跟踪、已装入的类和VM信息以及CE MBean。
是一个java GUI监视工具,可以以图标化的形式显示各种数据。并可以通过远程连接监视远程的服务器VM。用Java写的GUI程序,用来监控VM,并可以监控远程VM,非常易用,而且功能非常强。
JDK文档