Java虚拟机学习

引用计数算法(Reference Counting)
很多教科书上判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方去引用它的时候,计数器加一,当引用失效时计数器减一;任何时刻计数器的值都为零的对象是不可能被引用的。
引用计数算法的实现简单,效率也高,但是Java没有选用引用计数算法对内存进行管理,最主要的原因是因为它难以解决对象之间的相互循环引用问题。
下面进行一个小测试:

楼主 collmakkaksm  发布于 2016-04-29 12:39:00 +0800 CST  
public class test{
public Object instance=null;
private static final int _1M=1024*1024;
private byte[] bigsize=new byte[2*_1M];
public static void main(String args[]){
test objA=new test();
test objB=new test();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
System.gc();
}
}




可以看到即使两个对象相互引用(即计数器不会置为零),但是仍然执行了Gc方法,说明Java内存的回收并没有使用引用计数的方法进行判断对象是否存活(图片截于“深入理解JAVA虚拟机”)。

楼主 collmakkaksm  发布于 2016-04-29 13:04:00 +0800 CST  
根搜索算法(GC Roots Tracing)
基本思路:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(用树图更好理解一点就是说从GC Roots到这个对象不可达),则证明对象不可用。
在Java里,能够作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的类静态属性引用对象
方法区中常量引用的对象
本地方法栈JNI引用的对象



楼主 collmakkaksm  发布于 2016-04-29 13:23:00 +0800 CST  
无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判断对象的存活都跟“引用有关"。
目前来讲(自JDK1.2以后),Java将引用分为强引用(Strong Reference)软引用(Soft Reference)弱引用(Weak Reference)虚引用(Paton Reference),这四种引用强度依次减弱。
强引用:是指在程序代码里面普遍存在的,类似于Object obj=new Object()这类引用,只要强引用还存在,垃圾收集器就永远不会回收被引用的对象
软引用:用来描述一些还有用但非必须的一些对象。对于软引用关联的对象,在系统将要发生内存溢出之前,将这些对象列为可回收范围内并进行第二次回收。如果这次回收,内存还不够就会抛出内存溢出异常。
弱引用:用来非必须的对象,它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集。当垃圾收集器开始工作时,无论内存是否充足,都会回收只被弱引用关联的对象。
虚引用:虚引用最弱,一个对象设置虚引用关联的唯一目的是当对象被回收时,发出一个系统通知。

楼主 collmakkaksm  发布于 2016-04-30 22:45:00 +0800 CST  
为什么需要使用软引用

首先,我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。这时我们通常会有两种程序实现方式:一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运行速度。
如果使用软引用

SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null。看下面代码:
MyObject aRef = new MyObject();
SoftReference aSoftRef = new SoftReference( aRef );
此时,对于这个MyObject对象,有两个引用路径,一个是来自SoftReference对象的软引用,一个来自变量aRef的强引用,所以这个MyObject对象是强可及对象。
随即,我们可以结束aRef对这个MyObject实例的强引用:
aRef = null ;
此后,这个MyObject对象成为了软可达对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个SoftReference对该对象的引用而始终保留该对象。Java虚拟机的垃圾收集线程对软可达对象和其他一般Java对象进行了区别对待:软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可达对象,对那些刚刚构建的或刚刚使用过的软可达对象会被虚拟机尽可能保留。在回收这些对象之前,我们可以通过:
MyObject anotherRef =(MyObject) aSoftRef .get()
重新获得对该实例的强引用。而回收之后,调用get()方法就只能得到null了。

楼主 collmakkaksm  发布于 2016-04-30 23:11:00 +0800 CST  
在根搜索算法中,不可达的对象,也并非“非死不可”。
如果对象在进行根搜索后,发现没有与GC Roots相连的引用链,那么此对象会进行一次标记并进行一次筛选,筛选的条件是该对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经虚拟机调用过,虚拟机将这两种情况。
如果这个对象被判定为有必要执行finalize()方法,那么对象会放置在一个名为F-Queue的队列中,并稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行。
第一次标记后,GC会对F-Queue队列中的对象进行第二次小规模标记,如果在第二次标记之前,对象重新与引用链上的任何对象进行了关联,那么在第二次标记时,他会被清除出队列,不进行回收。
如果被进行第二次标记那么对象就会被回收。

楼主 collmakkaksm  发布于 2016-05-02 19:23:00 +0800 CST  
测试代码:
public class test{
public static test SAVE_HOOK=null;
public void isAlive(){
System.out.println("yes,i am still alive");
}

@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
super.finalize();
System.out.println("finalize method executed!");
test.SAVE_HOOK=this;
}

public static void main(String args[]) throws InterruptedException{


SAVE_HOOK=new test();

SAVE_HOOK=null;
System.gc();

Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no,i am dead");
}

}

}
结果:
finalize method executed!
yes,i am still alive


由代码可知对象是被标记过的但后来又成功逃脱。

楼主 collmakkaksm  发布于 2016-05-02 19:51:00 +0800 CST  
在上面我们已经说过,Java虚拟机在实现垃圾回收之前首先要判断对象是否需要回收,主要介绍了两种算法。接下来主要介绍几种垃圾收集算法:
标记-清除算法(Mark-Sweep)
复制算法(Copying)
标记-整理算法(Mark-Compact)
分代收集算法(Generational Collection)

楼主 collmakkaksm  发布于 2016-05-03 21:03:00 +0800 CST  
标记-清除算法
标记清除算法是垃圾收集算法里最基础的算法,后续的收集算法都是基于这种思想并对其缺陷进行改进。
正如它的名字,整个算法分为两个部分:标记和清除,首先标记所有需要回收的对象,在标记完成后进行统一回收。
它的主要缺点有两个:
一、效率问题,标记和清除的过程中效率都不高。
二、空间问题,由于标记的内存大部分都是分割开来的,并不连续,清楚之后会导致大量的不连续的内存碎片,空间碎片太多,可能会导致当需要分配较大对象时无法找到足够的连续内存而导致不得不提前进行另一次垃圾收集动作,甚至可能出现内存溢出等异常。


楼主 collmakkaksm  发布于 2016-05-03 21:19:00 +0800 CST  
复制算法

为了解决效率问题,复制算法被提出。它将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活的对象复制到另一块上,然后把已使用过的内存空间进行一次清理。这样使得每次都是对其中一块进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价相当于将内存缩小了一半。


楼主 collmakkaksm  发布于 2016-05-03 21:30:00 +0800 CST  
标记-整理算法
标记过程和“标记清除算法一样”,但后续过程不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。


楼主 collmakkaksm  发布于 2016-05-03 21:48:00 +0800 CST  
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集算法”,根据对象的存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,这样可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选择复制算法。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法进行回收。
一般将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor,当回收时,将Eden和Survivor中存活的对象一次性拷贝到另一块Survivor中,最后清理Eden和Survivor空间。

楼主 collmakkaksm  发布于 2016-05-03 22:03:00 +0800 CST  
(2).ParNew垃圾收集器:
ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。

楼主 collmakkaksm  发布于 2016-05-07 19:06:00 +0800 CST  
Parallel Old收集器:
Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。
在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。
新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图:


楼主 collmakkaksm  发布于 2016-05-07 19:08:00 +0800 CST  
(7).G1收集器:
Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与CMS收集器,G1收集器两个最突出的改进是:
a.基于标记-整理算法,不产生内存碎片。
b.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。
区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。

楼主 collmakkaksm  发布于 2016-05-07 19:09:00 +0800 CST  


楼主 collmakkaksm  发布于 2016-05-07 19:43:00 +0800 CST  
我也是看书来的

楼主 collmakkaksm  发布于 2016-05-07 20:55:00 +0800 CST  
Java虚拟机内存分配和回收策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
对象的内存分配,从大方向上将,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况也可能直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
本节中的代码在测试时使用Client模式虚拟机运行,没有手工指定收集器组合,换句话说,验证的是使用Serial/Serial Old收集器下(ParNew/Serial Old组合的规则也基本一致)的内存分配和回收策略。

楼主 collmakkaksm  发布于 2016-05-09 15:20:00 +0800 CST  
1. 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。
虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前内存各区域的分配情况。在实际应用中,内存回收日志一般都是打印到文件后通过日志工具进行分析。
private static final int _1MB = 1024*1024;
/** * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails */
public static void testAllocation(){
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[2*_1MB];
allocation2 = new byte[2*_1MB];
allocation3 = new byte[2*_1MB];
allocation4 = new byte[4*_1MB];//出现一次Minor GC }

楼主 collmakkaksm  发布于 2016-05-09 15:21:00 +0800 CST  
运行结果:


123456789101112 [GC [DefNew: 6487K->152K(9216K), 0.0041641 secs] 6487K->6296K(19456K), 0.0042253 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4412K [0x326e0000, 0x330e0000, 0x330e0000)
eden space 8192K, 52% used [0x326e0000, 0x32b08fe0, 0x32ee0000)
from space 1024K, 14% used [0x32fe0000, 0x330062b0, 0x330e0000)
to space 1024K, 0% used [0x32ee0000, 0x32ee0000, 0x32fe0000)
tenured generation total 10240K, used 6144K [0x330e0000, 0x33ae0000, 0x33ae0000) the space 10240K, 60% used [0x330e0000, 0x336e0030, 0x336e0200, 0x33ae0000) compacting perm gen total 12288K, used 375K [0x33ae0000, 0x346e0000, 0x37ae0000) the space 12288K, 3% used [0x33ae0000, 0x33b3dcb0, 0x33b3de00, 0x346e0000) ro space 10240K, 54% used [0x37ae0000, 0x3805d9f8, 0x3805da00, 0x384e0000) rw space 12288K, 55% used [0x384e0000, 0x38b813f8, 0x38b81400, 0x390e0000)
尝试分配3个2MB和1个4MB的对象,在运行时通过-Xms20M、-Xmx20M和-Xmn10M这3个参数限制Java堆大小为20M,且不可扩展,其中10MB分配给新生代,剩下的10M就分配给老年代了。-XX:SurvivorRatio=8决定了新生代中Eden和Survivor区的空间比例为8:1,从运行结果可以看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)。
执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代由6487K变为152K,而总内存占用了几乎没有减少(因为allocation1,2,3三个对象都是存活的虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配所需的4MB内存时,发现Eden区已经被占用了6MB,剩余空间不足以分配4MB,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB对象无法全部放入Survivor空间(Survivor只有1MB),所以只好通过分配担保机制提前转移到老年代。
这次GC结束后,4MB的allocation4对象被顺利分配到Eden中。因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(allocation1,2,3占用)。
Minor和Full GC有什么不一样?

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次的Minor GC(但并非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

楼主 collmakkaksm  发布于 2016-05-09 15:22:00 +0800 CST  

楼主:collmakkaksm

字数:12910

发表时间:2016-04-28 01:51:00 +0800 CST

更新时间:2021-02-23 17:47:55 +0800 CST

评论数:232条评论

帖子来源:百度贴吧  访问原帖

 

热门帖子

随机列表

大家在看