1.jvm的内存结构:
程序计数器
java虚拟机栈
本地方法栈
堆
本地方法区
首先引入一幅图说明Java各对象的存放位置:
(1) 程序计数器:记录当前线程正在执行字节码指令的地址(如果正在执行的是java方法,该计数器记录的是正在执行
的虚拟机字节码指令的地址;如果正在执行的是Native方法,该计数器的值为空(Undefined))。该区域是线程
私有的,生命周期随着线程的创建而创建,随着线程的消亡而消亡。读取指令举例:如顺序、分支、循环、跳转、
异常处理、线程恢复等
(2)Java虚拟机栈:即我们所说的栈,线程私有,由栈帧组成。
定义:虚拟机栈描述的是java方法执行的内存模型。每个方法执行时都会创建一个栈帧用于存储局部变量表,操作数栈、
动态链接、方法出口等信息,每个方法被调用直至调用完成的过程,就对应一个栈帧在虚拟机中从入栈到出栈的
过程。例如:函数1对应栈帧1、函数2对应栈帧2、函数3对应栈帧3、函数4对应栈帧4;函数1调用函数2、函数2
调用函数3、函数3调用函数4;当函数1被调用时时栈帧1入栈、函数2被调用时栈帧2入栈、函数3被调用时栈帧
3入栈、函数4被调用时栈帧4入栈;当函数4执行完栈帧4出栈并执行函数3;函数3执行完栈帧3出栈并执行函数2;
函数2执行完栈帧2出栈并执行函数1;函数1执行完栈帧1出栈。当前正在执行的函数锁对应的帧一定是位于栈
底的帧。
栈帧包括:局部变量表、操作数栈、常量池、动态链接、方法出口信息等
局部变量表:局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译
为Class文件时,就在方法的Code属性的max_locals数据项中确定了方法所需要分配的最大局部变量表的
容量。
动态链接:即用到某个类才加载到内存
静态链接:所有类都加载无论是否用到
操作数栈:Java 虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的,因此它的运行方式是基于栈的而
不是基于寄存器的。如下例:
比如 a = 1 + 2
iload_0 //将 1 压入操作数栈
iload_1 //将 2 压入操作数栈
iadd //从操作数栈中弹出 1、2,将算出的值 3 压入操作数栈
istore_2 //把 3 从操作数栈中弹出,保存到本地变量区
(3)本地方法栈:和虚拟机栈类似,区别在与虚拟机栈为虚拟机执行java方法时服务,而本地方法栈则为虚拟机使
用到的Native方法服务,本地方法被执行的时候,在本地方法会创建一个栈帧,用于存放局部变
量表、操作数栈、动态链接、出口信息。
(4)堆:此内存区域的唯一目的就是存放对象实例,Java虚拟机规范中的描述是:“所有对象实例以及数组都要在
堆上分配”,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致
一些微妙的变化发生,所有对象都分配在堆上也渐渐变得不是那么绝对了。
该区域是线程共享的,垃圾回收的主要场所,Java虚拟机规范规范规定:Java堆可以处于物理上不连续的
内存空间中,只要逻辑上连续即可。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
(5)方法区:堆的一个逻辑部分,线程共享,存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译
后的代码等。对于 HotSpot该区域叫做永久代。
及时编译器:Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代
码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效
率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次
的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文统称JIT编
译器)
编译和解释:编译型语言在编译过程中生成目标平台的指令,解释型语言在运行过程中才生成目标平台
的指令。
(6)运行时常量池:略。
(7)直接内存:略。
2.jvm的垃圾回收算法
(1)引用计数法:堆中的每一个对象都会有一个引用计数值,该对象每增加一个引用的时候,该计
数加一,反之则减一。在执行垃圾回收时,那些引用计数值为0的对象将会被回收。这种方法的
一个致命缺陷是无法处理循环带来的引用问题,即当两个对象互相引用时,它们的引用计数值永
远不可能为0。例如:
/**
* 执行后,objA和objB会不会被GC呢?
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//假设在这行发生了GC,objA和ojbB是否被回收
System.gc();
在testGC()方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,
除此之外这两个对象再无任何引用,实际上这两个对象都已经不能再被访问,但是它们因为相互引用着对象方,
异常它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们
(2)标记清除算法:分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记
的对象所占用的空间,该算法最大的问题是内存碎片产生严重
(3)复制算法:为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大
小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存
清掉。这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本
的一半。且存活对象增多的话,Copying算法的效率会大大降低。
(4)标记压缩法:标记阶段和标记算法相同,标记后不是清理对象而是将存活的对象移动到内存的一端,然后
清除边界外的对象。
(5)分代算法:分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同
生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)
和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新
生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
新生代采用复制算法,老年代采用标记压缩算法。
(6)分区算法:将整个内存分为N个小的独立空间,每个小空间都可以独立使用,这样细粒度控制一次
回收多少个小空间和哪些个小空间,而不是对整个空间进行GC,从而提升性能
3.垃圾回收器
(1)串行回收器:使用单线程进行垃圾回收的回收器,每次回收时,串行回收器只有一个工作线程,对于并行能力
较弱的服务器,串行回收器会有更好的表现,串行回收器可以在新生代和老年代使用,根据作用于不同的堆空
间有新生代串行回收器和老年代串行回收器
(2)并行回收器:它在串行回收器的基础上做了改进,它可以使用多个线程同时进行垃圾回收,对于计算能力较强
的服务器而言可以有效的缩短垃圾回收的时间
ParNew回收器:是一个工作在新生代的垃圾回收器,它只是简单的将串行回收器多线程化,它的回收策略和算
法和串行回收器一样。
使用-XX:+UseParNewGC开启并行回收器,此时老年代默认使用串行回收器
ParnNew回收器使用“-XX:ParallelGCThreads参数指定”工作线程数量,一般最好和计算机的CPU数量相
当。
ParallelGC回收器:是工作在新生代的垃圾回收器,使用了复制算法的收集器,也是多线程独占(独占回
收器:GC来的时候应用停顿只执行GC)形式的收集器非常关注系统的吞吐量,如下两个参数控制系统的
吞吐量:
-XX:MaxGCPauseMills:设置最大垃圾回收停顿时间,将此值减少可以减少GC垃圾会减少垃圾收集的停顿
时间,但是会导致GC频繁,重而增加了垃圾回收的总时间,降低了吞吐量,所以需要根据实际情况设置该
值。
-XX:GCTimeRatio:设置吞吐量大小,他是一个0到100之间的整数,默认情况下他的取值是99,那么系统
将花费不超过1/(1+n)的时间用于垃圾回收,也就是1/(1+99)=1%的时间
-XX:+UseAdaptiveSizePollcy:打开自适应模式,在这种模式下新生代的大小、eden、from/to的比例,
以及晋升到老年代的对象年龄参数会自动调整,以达到堆大小、吞吐量和停顿时间之间的平衡点。
ParallelOldGC:是一种多线程关注吞吐量的回收器,它使用了标记压缩算法进行实现。
-XX:+UseParallelOldGC进行设置使用
-XX:+ParallelGCThreads:设置垃圾回收时的线程数量。
(3)CMS回收器:它使用的是标记清除法,主要关注的是系统的停顿时间。CMS并不是独占的回收器,也就是
说CMS回收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断产生,所以在使用CMS过程中应
该确保应用程序的内存足够可用,CMS不会等到应用程序饱和的时候在去回收垃圾,而是到达某个阈值的
时候就去回收,回收的阈值可以通过指定的参数来设置-XX:CMSInitiatingOccupancyFraction来指定,默
认值是68,也就是说当老年代的使用率达到68%的时候会执行CMS垃圾回收,如果内存使用增长的很快,
在CMS过程中出现了内存不足的情况,CMS回收就会失败,虚拟机将启用老年代串行回收器进行垃圾回收,
这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC停顿时间可能过长,所以设置
-XX:CMSInitiatingOccupancyFraction要根据实际情况。
如下两个参数:
-XX:UseCMSCompactAtFullCollection:使CMS完成之后进行碎片整理。
-XX:CMSFULLGCsBeforeCompaction:设置多少次CMS回收后对内存进行一次压缩。
(4)G1回收器:是JDK1.7中提出的垃圾回收器,从长期目标看是为了取代CMS回收器,属于分代垃圾回收器
区分新生代和老年代,依然有eden和from、to区,它不要求新生代、老年代、eden、from、to区空间
都连续,使用了分区算法。据说JDK1.7之后使用的是G1回收算法,不过有待考证,目前不能确定,但是
在JDK1.7里它还不太成熟。
并行性:G1回收期间可多线程同时工作。
并发性:G1拥有与应用程序交替执行的能力,部分工作与应用程序同时进行,在整个GC期间不会完全阻
塞应用程序。
分代GC:G1依然是一个分代的收集器,但是它兼顾新生代和老年代一起工作,之前的垃圾回收器或者在
新生代工作或者在老年代工作,因此这是一个很大的不同。
空间整理:G1在回收过程中不会像CMS那样在经过若干次GC后需要进行碎片整理,G1采用了有效复制对
象的方式减少空间碎片。
可预见性:由于分区的原因,G1可以只选取部分区域进行回收,缩小了回收范围,提升了性能
使用:-XX:+UseG1GC应用G1收集器
使用:-XX:MaxGCPauseMillis指定最大停顿时间
使用:-XX:ParallelGCThreads设置并行回收的线程数量。