JAVA垃圾回收个人总结.docx
《JAVA垃圾回收个人总结.docx》由会员分享,可在线阅读,更多相关《JAVA垃圾回收个人总结.docx(18页珍藏版)》请在冰豆网上搜索。
JAVA垃圾回收个人总结
GC:
(GarbageCollection,垃圾收集,垃圾回收)
程序计数器,java虚拟机栈,本地方法三个区随线程而生,随线程而灭。
其中栈中的栈帧随方法的进入和退出而有条不紊的执行着出栈入栈操作。
垃圾回收针对的区域:
Java堆和方法区(主要还是堆)
很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:
在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
无用的类”:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。
垃圾收集器:
主要处理的问题----
1)哪些对象需要回收?
2)何时回收这些对象?
3)如何回收这些对象?
垃圾回收算法:
1.引用计数器法
方法描述:
给对象中添加一个计数器,每当有一个地方应用它时,计数器值就加1;每当引用失效时,计数器值就减1.任何计数器为0的对象就是不可能在使用的对象。
优点:
实现简单,判定效率高。
缺点:
无法解决对象之间循环引用的问题。
(比如A引用了B,B也引用了A,除此之外这两个对象再无其他引用。
此时这两个对象已经无法在被访问了,但由于计数器不为0,无法回收他们。
)
注:
主流虚拟机不采用引用计数器法管理内存。
采用可达性分析算法(根搜索算法):
判断对象是否存活。
即:
通过一系列称为“GCRoots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为应用链,当一个对象到GCRoots没有任何引用链相连(用图论的话来说就是从GCRoots到这个对象不可达)时,则证明此对象是不可用的。
在Java语言里,可作为GCRoots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)的引用的对象。
2.标记清除算法
方法描述:
算法分为“标记”和“清除”两个阶段:
首先标记出所有需要回收的对象(一种可行实现:
在标记阶段,首先通过根节点,标记所有从根节点可达的对象),在标记完成后统一回收掉所有被标记的对象.
优点:
缺点:
一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之
后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过
程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收
集动作。
回收后的空间是不连续的。
(在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续空的工作效率要低于连续的空间)
3.复制算法/改进的复制算法----------现在的商业虚拟机都采用这种收集算法来回收新生代
复制算法:
方法描述:
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:
只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。
改进的复制算法:
方法描述:
新生代中的对象绝大部分是朝生夕死的,并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。
当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(HandlePromotion)
内存的分配担保也一样,如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。
4.标记压缩算法(标记整理算法)
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。
更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
方法描述:
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
5.增量算法
方法描述:
如果一次性将所有垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程与应用程序线程交替执行
优点:
减少系统停顿时间
缺点:
线程切换,和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量下降。
6.分代
方法描述:
根据对象的存活周期的不同将内存划分为几块
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
垃圾收集器:
1.新生代串行收集器(Serial收集器)-------------采用:
复制算法
描述:
单线程收集器.垃圾收集时,必须暂停其他所有线程工作,直到收集结束.
2.老年代串行收集器(SerialOld收集器)-------采用:
标记-整理算法
描述:
同上
3.并行收集器(ParNew收集器)----------采用:
复制算法
描述;多条线程进行垃圾收集,必须暂停其他所有线程工作,直到收集结束.
4.新生代并行回收器(ParallelScavenge收集器)-------
描述:
并行多线程收集,目的:
达到可控制的吞吐量
(吞吐量:
运行用户代码时间/(运行用户代码时间+垃圾收集时间))
5.老生代并行回收器(ParallelOld收集器)------采用:
标记-整理算法
6.CMS收集器(老生代)-------采用:
标记清除算法
描述:
获取最短回收停顿时间为目的的收集器。
(并发的收集器:
即用户线程和回收线程可同时运行)
初始标记(标记gcroots直接关联的对象)----并发标记(标记gcroots可达对象)
----重新标记(修正并发标记时,产生的一些变动)----并发清除(清理垃圾)
Cms缺点:
cms收集器对cpu特别敏感:
当cpu本身负载很大时,还要分出cpu给gc,虚拟机提供了:
增量并发收集器(交替运行,尽量减少gc线程独占线程的时间)
cms无法处理浮动垃圾:
在GC收集器,收集垃圾的同时,用户线程还要产生垃圾,所以清理垃圾时,要预留一部分空间,提供并发收集时的程序运作使用。
(如果不这样,可能导致采用老年代串行收集器fullgc)
cms运行时会产生大量碎片:
大对象无法分配,不得不提前触发一次Fullgc(内部:
会在顶不住要进行复制前,开启内存的合并整理过程。
也可以设置:
-XX:
CMSFullGCsBeforeCompaction执行多少次不压缩的fullgc后跟着来一次带压缩的。
)
7.G1收集器(都可以)--------采用:
整体来看:
标记-整理局部来看:
复制算法不产生内存碎片。
把java堆划分为,多个大小相等的区域,虽然还保留了,新生代和老年代的概念,但是他们已经不再是物理隔离的了,他们都是一部分区域的集合。
优先回收代价最大的区域。
初始标记(标记gcroots直接关联的对象)----并发标记(标记gcroots可达对象)
----最终标记(修正并发标记时,产生的一些变动)----筛选回收(并发的)(对各个区域的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
)
G1优点:
1.并行与并发:
2.分代收集:
3.空间整合:
整体上基于标记-整理算法,不产生碎片
4.可预测的停顿:
新生代GC(MinorGC):
指发生在新生代的垃圾收集动作,因为Java对象大多都具
备朝生夕灭的特性,所以MinorGC非常频繁,一般回收速度也比较快。
老年代GC(MajorGC/FullGC):
指发生在老年代的GC,出现了MajorGC,经常会伴随至少一次的MinorGC(但非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)。
MajorGC的速度一般会比MinorGC慢10倍以上。
虚拟机给每个对象定义了一个对象年龄(Age)计数器。
如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数-XX:
MaxTenuringThreshold来设置。
Java内存泄露:
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。
Vectorv=newVector(10);
for(inti=1;i<100;i++){
Objecto=newObject();
v.add(o);
o=null;
循环申请Object对象,并将所申请的对象放入一个Vector中,如果仅仅释放引用本身,那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。
因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。
}
Java引用类型:
强引用:
强引用指向的对象在任何时候都不会被系统回收。
软引用:
一个持有软引用的对象,不会被JVM很快地回收,jvm会根据当前堆的使用情况来判断何时回收。
(可以用于实现对内存敏感的cache)
弱引用:
在系统GC时,只要发现弱引用,不管系统队空间是否足够,都会将对象进行回收。
虚引用:
和没有引用几乎一样。
weakHashMap:
如果存放在weakHashMap中的key都存在强引用,那么weakHashMap就退化为HashMap。
weakHashMap会在内存紧张时,自动释放持有弱引用的数据。
Egg:
Mapmap=newWeakHashMap();
Listlist=newArrayList();
for(inti=0;i<1000;i++){
Integerii=newInteger(i);
list.add(ii);
map.put(ii,newbyte[i]);//当内存不时,他报OOM因为存在强引用,而无法清理。
}
Mapmap=newWeakHashMap();
Listlist=newArrayList();
for(inti=0;i<1000;i++){
Integerii=newInteger(i);
map.put(ii,newbyte[i]);//当内存不时,他会自动清理数据
}
并发与线程安全:
并发控制的方法:
1.volatile
本地内存:
是java内存模型控制的一个抽象概念,并不是真实存在。
本地线程存储了该线程读写变量的副本。
每个线程都有一个私有的本地内存。
主内存:
线程之间的共享的变量存储在主内存中。
//线程1
booleanstop=false;
while(!
stop){
doSomething();
}
//线程2
stop=true;
可能出现死循环:
因为:
每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
//线程1
booleanvolatilestop=false;
while(!
stop){
doSomething();
}
//线程2
stop=true;
但是用volatile修饰之后就变得不一样了:
第一:
使用volatile关键字会强制将修改的值立即写入主存;
第二:
使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:
由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
Volatile两层含义:
1)其他线程的修改能及时反映到当前线程上。
2)对Volatile变量的修改,会及时写到主存。
3)使用Volatile将迫使所有线程均读写主存中的变量。
但是不能保证原子性:
publicclassTest{
publicvolatileintinc=0;
publicvoidincrease(){
inc++;
}
publicstaticvoidmain(String[]args){
finalTesttest=newTest();
for(inti=0;i<10;i++){
newThread(){
publicvoidrun(){
for(intj=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
运行结果都不一致,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的
2.同步关键字synchronized(内部锁)jvm虚拟机总会在最后自动释放synchronized锁。
同步方法:
publicsynchronizedvoidmethod(){}
等价于:
publicvoidmethod(){
synchronized(this){}
}
同步块:
publicvoidmethod(){
somecodehere;
synchronized(this){}//仅同步必要的代码,有利于减小锁的竞争,提高吞吐量。
othercodehere;
}
Object的wait(),notify(),notifyAll()
函数wait():
可以让线程等待当前线程上的通知,在wait()过程中,线程会释放对象锁。
函数notify():
将唤醒一个等待在当前对象上的线程,如果当前有多个线程等待,那么notify()方法将随机选择其中一个。
notifyAll():
唤醒所有等待在当前对象上的线程。
实现一个阻塞队列:
PublicclassBlockQueue(){
PrivateListlist=newArrayList();
Publicsnchronizedbjectpop(){
while(list.seze()==0)
this.wait();//如果队列为空则等待。
if(list.seze()>0){
returnlist.remove(0);
}else{
returnnull;
}
}
Publicsnchronizedvoidput(Objecto){
List.add(o);
This.notify();//通知pop方法,可以取得数据
}
}
3.ReentrantLock重入锁------使用完后必须手动释放,一般放在finally里
提供更多方法:
Lock();//获得锁,如果已经占用,则等待。
TryLock();//尝试获得锁,如果成功则返回true否则false不等待。
TryLock(带参数);//给定时间获得锁,如果成功则返回true否则false不等待。
Unlock();//释放锁
4.ReadWriteLock读写锁
在读多写少的场合使用,读的绝对并行
ReadWriteLocklock=newReentrantReadWriteLock();
Lockread=lock.readLock();
Lockwrite=lock.writeLock();
针对读写分别上锁。
5.Semaphore信号量------信号量是对锁的扩展
ReentrantLock重入锁,synchronized(内部锁)一次只允许一个线程访问一个资源,而信号量可以指定多个线程同时访问一个资源。
通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。
6.ThreadLocal线程局部变量
ThreadLocal完全不提供锁,而是利用空间换时间的手段,为每个线程提供变量的独立副本。
Get()
Set()
Remove()
锁的优化:
1.减小锁持有时间
publicvoidmethod(){
somecodehere;
synchronized(this){}//仅同步必要的代码,有利于减小锁的竞争,提高吞吐量。
othercodehere;
}
2.减小锁粒度
ConcurrentHashMap将整个hashMap拆分成若干个段,每个段都是一个子hashMap.
增加一个新的表项时,并不是将整个HashMap加锁,而是根据hashCode值,得到该表项应该放在那个段中,然后对该段加锁,并完成Put操作。
在多线程下只要加入的数据不再同一段;就能做到真正的并行。
缺点:
访问全局时性能差一些。
3.锁粗化:
减少锁的申请和释放的开销。
synchronized(lock){
For(inti=0;i<100;i++){
}
}
For(inti=0;i<100;i++){
synchronized(lock){}
}
4.读写分离来代替独占锁
在对多写少的情况下,用读写分离来代替独占锁。
Java运行时数据区域:
一、程序计数器(寄存器)--------------------线程私有
∙当前线程所执行的字节码行号指示器
∙线程执行Native方法时,计数器记录为空(Undefined)
∙唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况区域
二、Java虚拟机栈----------------线程私有
∙线程私有,生命周期与线程相同
∙用于存储局部变量、操作栈、动态链接、方法出口
∙每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
虚拟机栈描述的是Java方法执行的内存模型:
每个方法被执行的时候都会同时创建一个栈帧(StackFrame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在Java虚拟机规范中,对于此区域规定了两种异常状况:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
三、本地方法栈
本地方法栈(NativeMethodStacks)与虚拟机栈所发挥的作用非常类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
四、Java堆
∙Java堆(javaheap)是Java虚拟机所管理的内存中最大的一块
∙它是被所有线程共享的一块内存区域,在虚拟机启动时创建
Java堆是垃圾收集管理的主要战场。
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。
(通过-Xmx和-Xms控制)
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
五、方法区
方法区(MethodArea)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。