1.如何判断对象为垃圾对象
为了查看垃圾回收的信息 -verbose:gc -XX:+PrintGCDetails
就可以看到gc信息
1.1引用计数法
在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就减-1.
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。
这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。看下面这段代码:
public class TestMain {
private Object instance;
public static void main(String[] args) {
TestMain testMain1 = new TestMain();
TestMain testMain2 = new TestMain();
testMain1.instance=testMain2;
testMain2.instance=testMain1;
testMain1=null;
testMain2=null;
}
}
最后面两句将testMain1和testMain1赋值为null,也就是说testMain1和testMain1指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。
1.2可达性分析法
java虚拟机就是采用可达性分析法。
该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
那么哪些点可以作为GC Roots呢?一般来说,如下情况的对象可以作为GC ROOTs:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI(Native方法)的引用的对象
最后总结一下平常遇到的比较常见的将对象判定为可回收对象的情况:
1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象,比如下面的代码:
1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象,比如下面的代码:
Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;
2)局部引用所指向的对象,比如下面这段代码:
void fun() {
.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
}
循环每执行完一次,生成的Object对象都会成为可回收的对象。
3)只有弱引用与其关联的对象,比如:
WeakReference<String> wr =
new WeakReference<String>(new String("world"));
GC垃圾回收的相关算法
1.Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的。一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储常量class类、、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
GC垃圾回收器
Serial收集器
Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
ParNew
ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
Parallel Scavenge
Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。
-XX:MaxGCPauseMillis 垃圾收集器最大停顿时间
这个单位是毫秒,要根据实际情况设置
-XX:GCTimeRatio吞吐量大小
(0,100)默认最大99
吞吐量=(执行用户代码时间)/(执行用户代码时间+垃圾回收所占用的时间)
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
CMS(Concurrent Mark Sween)
CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。
工作过程:初始标记》》》并发标记》》》重新标记》》并发清理
G1
G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
优势:
1 并发与并行
2 分代收集
3 空间整合
4 可预测的停顿
回收器 | 概述 | 年轻代 | 老年代 |
---|---|---|---|
串行回收器(serial collector) | 客户端模式的默认回收器,所谓的串行,指的就是单线程回收,回收时将会暂停所有应用线程的执行 | 参见本文第三部分 | serial old回收器标记-清除-合并。标记所有存活对象,从头遍历堆,清除所有死亡对象,最后把存活对象移动到堆的前端,堆的后端就空了 |
并行回收器 | 服务器模式的默认回收器,利用多个线程进行垃圾回收,充分利用CPU,回收期间暂停所有应用线程 | Parallel Scavenge回收器,关注可控制的吞吐量(吞吐量=代码运行时间/(代码运行时间加垃圾回收时间)。吞吐量越大,垃圾回收时间越短,可以充分利用CPU。但是 | parrellel old回收器,多线程,同样采取“标记-清除-合并”。特点是“吞吐量优先” |
CMS回收器 | 停顿时间最短,分为以下步骤:1初始标记;2并发标记;3重新标记;4并发清除。优点是停顿时间短,并发回收,缺点是无法处理浮动垃圾,而且会导致空间碎片产生 | X | 适用 |
G1回收器 | 新技术,将堆内存划分为多个等大的区域,按照每个区域进行回收。工作过程是1初始标记;2并发标记;3最终标记;4筛选回收。特点是并行并发,分代收集,不会导致空间碎片,也可以由编程者自主确定停顿时间上限 | 适用 | 适用 |
附转载的GC参数汇总以及一个使用实例,转载来源是
JVM垃圾回收器工作原理及使用实例介绍
1. 与串行回收器相关的参数
-XX:+UseSerialGC:在新生代和老年代使用串行回收器。
-XX:+SuivivorRatio:设置 eden 区大小和 survivor 区大小的比例。
-XX:+PretenureSizeThreshold:设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。
-XX:MaxTenuringThreshold:设置对象进入老年代的年龄的最大值。每一次 Minor GC 后,对象年龄就加 1。任何大于这个年龄的对象,一定会进入老年代。
2. 与并行 GC 相关的参数
-XX:+UseParNewGC: 在新生代使用并行收集器。
-XX:+UseParallelOldGC: 老年代使用并行回收收集器。
-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
-XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
-XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。
-XX:+UseAdaptiveSizePolicy:打开自适应 GC 策略。在这种模式下,新生代的大小,eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
3. 与 CMS 回收器相关的参数
-XX:+UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用 CMS+串行收集器。
-XX:+ParallelCMSThreads: 设定 CMS 的线程数量。
-XX:+CMSInitiatingOccupancyFraction:设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%。
-XX:+UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收。
-XX:+CMSParallelRemarkEndable:启用并行重标记。
-XX:CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
-XX:UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。
-XX:+CMSIncrementalMode:使用增量模式,比较适合单 CPU。
4. 与 G1 回收器相关的参数
-XX:+UseG1GC:使用 G1 回收器。
-XX:+UnlockExperimentalVMOptions:允许使用实验性参数。
-XX:+MaxGCPauseMills:设置最大垃圾收集停顿时间。
-XX:+GCPauseIntervalMills:设置停顿间隔时间。
5. 其他参数
-XX:+DisableExplicitGC: 禁用显示 GC。
常用参数如下
调优实例
import java.util.HashMap;
public class GCTimeTest {
static HashMap map = new HashMap();
public static void main(String[] args){
long begintime = System.currentTimeMillis();
for(int i=0;i<10000;i++){
if(map.size()*512/1024/1024>=400){
map.clear();//保护内存不溢出
System.out.println("clean map");
}
byte[] b1;
for(int j=0;j<100;j++){
b1 = new byte[512];
map.put(System.nanoTime(), b1);//不断消耗内存
}
}
long endtime = System.currentTimeMillis();
System.out.println(endtime-begintime);
}
}
通过上面的代码运行 1 万次循环,每次分配 512*100B 空间,采用不同的垃圾回收器,输出程序运行所消耗的时间。
使用参数-Xmx512M -Xms512M -XX:+UseParNewGC 运行代码,输出如下:
clean map 8565
cost time=1655
使用参数-Xmx512M -Xms512M -XX:+UseParallelOldGC –XX:ParallelGCThreads=8 运行代码,输出如下:
clean map 8798
cost time=1998
5 JAVA性能优化
大多说针对内存的调优,都是针对于特定情况的。但是实际中,调优很难与JAVA运行动态特性的实际情况和工作负载保持一致。也就是说,几乎不可能通过单纯的调优来达到消除GC的目的。
真正影响JAVA程序性能的,就是碎片化。碎片是JAVA堆内存中的空闲空间,可能是TLAB剩余空间,也可能是被释放掉的具有较长生命周期的小对象占用的空间。
下面是一些在实际写程序的过程中应该注意的点,养成这些习惯可以在一定程度上减少内存的无谓消耗,进一步就可以减少因为内存不足导致GC不断。类似的这种经验可以多积累交流:
- 减少new对象。每次new对象之后,都要开辟新的内存空间。这些对象不被引用之后,还要回收掉。因此,如果最大限度地合理重用对象,或者使用基本数据类型替代对象,都有助于节省内存;
- 多使用局部变量,减少使用静态变量。局部变量被创建在栈中,存取速度快。静态变量则是在堆内存;
- 避免使用finalize,该方法会给GC增添很大的负担;
- 如果是单线程,尽量使用非多线程安全的,因为线程安全来自于同步机制,同步机制会降低性能。例如,单线程程序,能使用HashMap,就不要用HashTable。同理,尽量减少使用synchronized
- 用移位符号替代乘除号。eg:a*8应该写作a<<3
- 对于经常反复使用的对象使用缓存;
- 尽量使用基本类型而不是包装类型,尽量使用一维数组而不是二维数组;
- 尽量使用final修饰符,final表示不可修改,访问效率高
- 单线程情况下(或者是针对于局部变量),字符串尽量使用StringBuilder,比StringBuffer要快;
- String为什么慢?因为String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。如果不能保证线程安全,尽量使用StringBuffer来连接字符串。这里需要注意的是,StringBuffer的默认缓存容量是16个字符,如果超过16,apend方法调用私有的expandCapacity()方法,来保证足够的缓存容量。因此,如果可以预设StringBuffer的容量,避免append再去扩展容量。如果可以保证线程安全,就是用StringBuilder。示例下面两个示例·:
示例一:
StringBuffer st = new StringBuffer(50);
st.append("let us cook");
st.append(" ");
st.append("a matcha cake for our dinner");
String s = st.toString();
示例二:
public String toString() {
return new StringBuilder().append("[").append(name).append("]")
.append("[").append(Message).append("]")
.append("[").append(salary).append("]").toString();
}
内存分配
内存分配策略:
- 优先分配到eden区
- 大对象直接分配到老年代
- 长期存活的对象分配到老年代
- 空间分配担保
- 动态对象年龄判断
Eden区
-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-Xmn10M 设置新生代内存大小 -XX:SurvivorRatio=8设置eden 内存的大小
大对象直接分配到老年代
修改大对象的大小
-XX:PretenureSizeThreshold=8M标记大对象最小为8M
长期存活的对象分配到老年代
-XX:MaxTenuringThreshold
空间分配担保
-XX:+HandlePromotionFailure(-不启用)
内存逃逸与栈上分配:几种情况对象的作用域仅在当前方法中有效没有发生逃逸
public class StaticAllocation {
public StaticAllocation staticAllocation;
//方法返回staticAllocation对象发生内存逃逸
public StaticAllocation getInstance() {
if(staticAllocation==null) {
staticAllocation= new StaticAllocation();
}
return staticAllocation;
}
/**使用成员属性发生逃逸 **/
public void setStaticAllocation() {
this.staticAllocation= new StaticAllocation();
}
//对象的作用域仅在当前方法中有效没有发生逃逸
public void userStaticAllocation() {
StaticAllocation s= new StaticAllocation();
}
//引用成员变量发生逃逸
public void userStaticAllocation2() {
StaticAllocation s= new StaticAllocation();
}
}
虚拟机工具
先是JDK自带的工具
Sun JDK监控和故障处理工具 |
|
名称 |
主要作用 |
jps |
JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟进程 |
jstat |
JVM Statistcs Monitoring Tool,用于手机HotSpot虚拟机各方面的运行数据 |
jinfo |
Configuration Info for java,实时查看和调整虚拟机各项参数信息 |
jmap |
Memory Map for Java,生成转储快照(一般称为headdump或dump文件) |
jhat |
JVM Heap Analysis Tool,与jmap搭配使用,分析jmap生成的转储快照 |
jstack |
Stack Trace for Java,生成虚拟机当前时刻的线程快照(一般称为threddump或javacore文件) |
- jps工具 java process status
jps –m 显示程序所接收的参数
jps –l 运行的主类全面或者JAR包名称
jps –v 虚拟机参数
jps -mlv
- jstat
这个必须和jps 工具看到的进程号相关联
Jstat –gcutil 8332
- jinfo
实时查看和调整虚拟机的各个参数
- jmap
- Jhat(JVM heap Anakysis Tool)
- jstack
查看当前进程的线程堆栈信息情况
Jstack l 线程号
可视化工具
- jconsole工具
jconsole内存监控
jconsole线程监控
jconsole死锁监控
VisualVM
下载地址:
https://visualvm.github.io/download.html
网上学习研究
说明本次只是个人学习总结,有些内容摘自别人。水平有限请大家指教。