垃圾收集机制与内存分配策略

垃圾收集机制

程序计数器、虚拟机栈、本地方法栈3个区随线程而生,随线程而灭:栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,每个栈帧中分配多少内存基本上是在类结构确定下来就已知(运行期JIT编译器会进行优化暂时忽略),这几个区域的内存分配和回收都具备确定性。Java堆和方法区则不一样,一个接口中有多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收是动态的。GC主要也是对这部分内存进行管理,当对象不可能再被任何途径使用时即被回收。

如何判断对象已死

GC在进行垃圾回收之前,需要判断哪些对象还“存活”着,哪些对象已经“死去”(不可能再被任何途径使用的对象,可被回收的对象)。

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就不可能再被使用的。但在Java虚拟机中没有选用引用计数算法来管理内存,主要的原因是它很难解决对象之间相互循环引用的问题。

public class ReferenceGC {

	public Object instace = null;
	
	private byte[] bigSize = new byte[2*1024*1024];
	
	public static void testGC() {
		ReferenceGC objA = new ReferenceGC();
		ReferenceGC objB = new ReferenceGC();
		
		objA.instace = objB;
		objB.instace = objA;
		
		objA = null;
		objB = null;
		
		System.gc();
	}
}

若虚拟机采用引用计数算法,上述对象相互引用将不会被回收,但实际上它们已经不可能再被其他对象所引用。

可达性分析算法

主流的商用程序语言(Java、C#)的主流实现是通过可达性分析(Reachability Analysis)来判断对象是否存活。这个算法的基本思想是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则该对象不可达,将被GC回收。

这里写图片描述

GC Roots对象包括如下:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即Native方法)引用的对象。

引用

JDK1.2及以前,引用的定义为:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。但实际使用中,我们可能希望在内存空间还足够的情况下,保留一些“食之无味,弃之可惜”的对象,JDK1.2之后对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,强度依次减弱:

  • 强引用:类似于"Object obj = new Object()"这类引用,符合JDK1.2及以前的定义。
  • 软引用:用来描述一些还有用但并非必需的对象。对于软引用关联的对象,在系统要发生内存溢出异常之前,会进行回收,如果回收后的内存依然不足才抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用:也是用来描述非必需的对象。被弱引用关联的对象只能生存到下次垃圾收集发生之前,当GC工作时,无论内存是否够用,都会被回收。在JDK1.2后,提供了WeakReference类来实现弱引用。
  • 虚引用:亦称为幽灵引用或幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。虚引用关联的唯一目的是能在这个对象被GC回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

对象的回收过程

通过可达性分析算法分析出的不可达对象,在被GC回收之前至少要经历二次标记过程:

  1. 当对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法(当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将视这二种情况为“没有必要执行”)。
  2. 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,并在稍后由虚拟机自动建立一个低优先级的Finalizer线程去执行这些对象的finalize()方法,虚拟机并不承诺会等待它运行结束,因为如果一个对象在finalize()方法中执行缓慢或发生死循环,将可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
    finalize() 方法是对象逃脱被回收的最后一次机会,可以通过重写finalize()方法,重新与引用链上的任何一个对象建立关联,那么在第二次标记时将会被移除出“即将回收”的集合。
/**
 * 1.对象可以在被GC时自我拯救
 * 2.自救机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 *
 */
public class FinalizeEscapeGC {

	public static FinalizeEscapeGC SAVA = null;
	
	public void isAlive() {
		System.out.println("i am alive");
	}
	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("finalize method executed");
		FinalizeEscapeGC.SAVA = this;
	}
	
	public static void main(String[] args) throws InterruptedException {
		SAVA = new FinalizeEscapeGC();
		//对象第一次成功拯救自己
		SAVA = null;
		System.gc();
		//因为finalize方法优先级很低,所以暂停0.5秒等待它执行完
		Thread.sleep(500);
		if(SAVA != null){
			SAVA.isAlive();
		}else {
			System.out.println("i am dead");
		}
		
		//对象第二次自救失败,因为finalize()方法只仅在第一次回收时执行一次
		SAVA = null;
		System.gc();
		//因为finalize方法优先级很低,所以暂停0.5秒等待它执行完
		Thread.sleep(500);
		if(SAVA != null){
			SAVA.isAlive();
		}else {
			System.out.println("i am dead");
		}
	}
}

finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,尽量避免使用,更多的会使用try-finally。

回收方法区

Java虚拟机规范中不要求虚拟机在方法区实现垃圾回收,而且在方法区中进行垃圾回收的“性价比”也比较低。永久代的垃圾收集主要回收废弃常量和无用类。
废弃常量:回收废弃常量与回收Java堆中对象类似,以常量池中字面量为例,当系统中没有任何引用引用这个字面量时,则发生回收。
无用的类:该类的所有实例被回收,加载该类的ClassLoader以及被回收,该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
无用类的回收不同于对象,不使用了不是必然被回收,HotSpot虚拟机提供了 -Xnoclassgc参数进行控制,还可以使用 -verbose:class以及-XX:+TraceClassLoading、+TraceClassUnLoading查看类加载和卸载信息。

内存分代

一个应用启动,操作系统会给他分配一个初始的内存大小,由上可知,这部分内存大部分应该属于堆内存,JVM 为了更好地利用管理这部分内存,对该区域做了划分,一部分成为新生代,另一部分称为老年代。

一开始对象的创建都发生在新生代,随着对象不断创建,如果新生代没有空间创建新对象,将会发生 GC ,这时的 GC 称之为 Minor GC,位于新生代的对象每经过一次 Minor GC 后,如果这个对象没有被回收,则为自己的标记数加1,这个标记数用于标识这个对象经历了多少次的 Minor GC,对于 Sun 的 Hotspot 虚拟机,如果这个次数超过 15 ,该对象才会被移动到老年代。

随着时间的推移,如果老年代也没有足够的空间容纳对象,老年代也会试着发起 GC,这时的 GC 被称为 Full GC。

相比 Minor GC,Full GC 发生的次数比较少,但是每发生一次 Full GC,整个堆内存区域都需要执行一次垃圾回收,这对程序性能造成的影响比 Minor GC 大很多,所以我们应该尽量避免或者减少 Full GC 的发生。

同时,在堆内存区域,发生最多的 GC 情形就是新生代的 Minor GC 了,因为所有的对象会优先去新生代开辟空间,所以这块的内存变化会很快,只有内存不够用,就会发生 GC,但是一般的 Minor GC 执行比 Full GC 快很多。

垃圾收集算法

标记——清除算法(Mark-Sweep)

最基础的收集算法,算法分为“标记”和“清除”二个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,标记过程就是前面的引用计数和可达性分析,后续的算法都是基于该算法进行改进而得到的。
缺点:

  1. 效率问题,标记和清除两个过程效率低。
  2. 空间问题,标记清除之后会产生大量不连续的内存碎片,导致再次分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
    这里写图片描述

复制算法(Copying)

为解决标记——清除算法的效率问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当一块的内存用完了,将还存活的对象复制到另一块上,然后把已使用的内存空间一次清理掉,这样使得每次可以对整个半区进行内存回收,同样解决了内存碎片问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:将可用内存缩小了原来的一般。
这里写图片描述
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司研究表明,新生代中的对象98%是“朝生夕死”的,所以讲内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时将Eden和Survivor中存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和用过的Survivor空间,HotSpot虚拟机默认Eden和Survivor比例为8:1,则每次新生代可用内存空间为90%,只会浪费10%的内存。但可能会出现多于10%内存的对象存活,当Survivor空间不够用时,则需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。
这里写图片描述

标记——整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低,同时如果不想浪费更多的内存,就需要额外的空间进行分配担保,以应对内存不够的情况,所以老年代一般不能直接选用这种算法。根据老年代的特点,提出了“标记——整理”(Mark-Compact)算法,标记过程与“标记——清除”算法一样,但清理过程是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。
这里写图片描述

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,JVM根据对象存活周期的不同将Java堆内存分成新生代和老年代(后续介绍),根据各个年代的特点采用最适合的收集算法,新生代采用的就是上述的复制算法,老年代中对象存活率高,且没有额外空间对它进行分配担保,就采用上述的“标记——清理”或“标记——整理”算法。

HotSpot的算法实现

前面介绍了对象存活判定算法和垃圾收集算法

垃圾收集器

虚拟机性能监控与故障处理工具

通过数据分析来定位问题,其中数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、对转储快照(heapdump/hprof文件)等,使用适当的虚拟机监控和分析工具加快分析数据、定位问题的速度。

JDK的命令行工具

JDK的bin目录中我们最熟悉的就是“java.exe”和“javac.exe”两个命令行工具,其他的可能就不知道了。JDK每个更新版本的bin目录下命令行工具的数量和功能都会增加和增强。JDK的命令行工具都很小巧,这是因为它们的具体实现是在jdk/lib/tools.jar中。
Sun JDK 监控和故障处理工具

名称主要作用
jpsJVM Process Status Tool,显示制定系统内所有的HotSpot虚拟机进程
jstatJVM Statistics Monitoring Tool,用于收集HosSpot虚拟机各方面的运行数据
jinfoConfiguration Info for Java,显示虚拟机配置信息
jmapMemory Map for Java,生成虚拟机的内存转储快照(heapdump文件)
jhatJVM Heap Dump Browser,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
jstackStack Trace for Java,显示虚拟机的线程快照

jps:虚拟机进程状态工具

可列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID),如果是本地虚拟机,LVMID与操作系统的进程ID(Process Identifier,PID)一致,使用Windows的任务管理器或UNIX的ps命令也可以查询到虚拟机进程的LVMID。
jps命令格式:
jps [options] [hostid]

jps可通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。
jps工具主要选项

选项作用
-q只输出LVMID,省略主类的名称
-m输出虚拟机进程启动时传递给主类main()函数的参数
-l输出主类的全名,如果进程执行的是jar包,输出jar路径
-v输出虚拟机进程启动时JVM参数

jstat:虚拟机统计信息监视工具

jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。它是在非GUI界面下的首选工具。
jstat命令格式:
jstat [option vmid [interval [s | ms] [count]]]
如果是本地虚拟机进程,VMID与LVMID是一致的,如果是远程虚拟机进程,那么VMID的格式是:
[protocol:][//]lvmid[@hostname[:port]/servername]
interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。
jstat工具主要选项

选项作用
-class监视类装载、卸载数量、总空间以及类装载所耗费的时间
-gc监视java堆状况,包括Eden区、两个survivor区,老年代、永久代等的容量、已用空间、GC时间合计等信息
-gccapacity监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到的最大、最小空间
-gcutil监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
-gcnew监视新生代GC状况
-gcnewcapacity监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
-gcold监视老年代的状况
-gcoldcapacity监视内容与-gcold基本相同,输出主要关注使用到的最大,最小空间
-gcpermcapacity输出永久代使用到的最大、最小空间
-compiler输出JIT编译器编译过的方法、耗时等信息
-printcompilation输出已经被JIT编译的方法

jinfo:Java配置信息工具

jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。使用jps -v 可查看虚拟机启动时显式指定的参数列表,使用jinfo的-flag选项可以查询未被显式指定的参数的系统默认值,-flag [+|-] name或 -flag name=value来查询或修改一部分运行期可写的虚拟机参数值。windows平台只提供了-flag选项。
jinfo命令格式:
jinfo [option] pid
###jmap:java内存映像工具
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。还可以查询finalize执行队列、Java堆和永久代的详细信息。与jinfo命令一样,jmap的功能在Windows平台下受限。
jmap命令格式:
jmap [option] vmid
jmap工具主要选项

选项作用
-dump生成Java堆转储快照。格式为:-dump[live,]format=b,file=,其中live子参数说明是否只dump出存活的对象
-finalizerinfo显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在Linux/Solaris平台下有效
-heap显示java堆详细信息,如使用那种回收器、参数配置、分代状况等。只在Linux/Solaris平台下有效
-histo显示堆中对象统计信息,包括类、实例数量、合计容量
-permstat以ClassLoader为统计口径显示永久代内存状态。只在Linux/Solaris平台下有效
-F当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在Linux/Solaris平台下有效

jhat:虚拟机堆转储快照分析工具

该命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/HTTP服务器,生成dump文件的分析结果可在浏览器中查看,但由于分析时耗时耗资源,且分析功能简陋,一般很少使用该工具来分析,使用较多的是Eclipse Memory Analyzer和IBM HeapAnalyzer等工具。
jhat命令格式:
jhat dumpfilename
###jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump 或 javacore文件)。线程快照就是当前虚拟机内每条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致的长时间等待等。
jstack命令格式:
jstack [option] vmid
jstack工具主要选项

选项作用
-F当正常输出的请求不被响应时,强制输出线程堆栈
-l除堆栈外,显示关于锁的附加信息
-m如果调用到本地方法的话,可以显示C/C++的堆栈

java.lang.Thread类中的getAllStackTrances方法也可以用于获取虚拟机中所有线程的StackTraceElement对象。

HSDIS:JIT生成代码反汇编

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值