Jvm基本的参数设置
-Xms1024m #设置堆最小大小
-Xmx2048m #设置堆最大大小
-Xmn10m #设置老年代大小
-XX:PermSize=256m #设置永久代(方法区大小)
-XX:MaxPermSize=256m #限制方法区大小
-Xss1m #用于设置栈大小
-XX:NewSize=256m #设置年轻代大小
-XX:+HeapDumpOnOutOfMemoryError #可以让虚拟机在出现内存溢时Dump出内存堆快照
-XX:+PrintGCDetails #收集器日志参数,打印内存回收日志,并在进出退出时打印各个区域分配情况
-XX:SurvivorRetio=8 #设置新生代Eden 和Survivor的分配情况
-XX:PretenureSizeThreshold=3145728 #设置大对象超过 3MB则直接放入到老年代中
jvm存在的异常
- 栈溢出异常
/**
* vm args: -Xss128k
**/
public class JavaVmStackSOF{
private final static Logger logger = LoggerFactory.getLogger(JavaVmStackSOF.class);
private int stackLength = 1;
public void stackLeak(){
stackLength+=1;
stackLeak();
}
public static void main(String args[]){
JavaVmStackSOF stacksof = new JavaVmStackSOF();
try{
stackLeak();
}catch(Exception e){
logger.error("Stack error stack length={}" , stackLeak.stackLength, e);
}
}
}
#result:
stack length:2402
Exception .... java.lang.StackOverflowError
- 内存溢出异常
/**
* vm args: -Xms20m -Xmx20m
**/
public class HeapOm{
private final static Logger logger = LoggerFactory.getLogger(HeapOm.class);
static class OOMObject{
}
public static void main(String []args){
List<OOMObject> list = Lists.newArrayList();
while(true){
list.add(new OOMObject);
}
}
}
#result:
java.lang.OutOfMemoryError: Java heap space
....
- 方法区内存溢出
/**
* vm args: -XX:PermSize=10M -XX:MaxPermSize=10M
**/
public class RuntimeConstantPoolOOM{
public static void main(String []args){
List<String> list = Lists.newArrayList();
int i=0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
#result:
java.lang.OutOfMermoryError: PermGen space
方法区用于存放Class相关信息,如类名 , 访问修饰符 , 常量池,字段描述 ,方法描述等。可以通过产生大量类去填充方法区。
/**
* 利用cglib直接操作字节码产生动态类 ,Spring ,Hibernate , JVM动态语言Groovy(会持续创建类来实现语言的动态性)
* vm args: -XX:PermSize=10M -XX:MaxPermSize=10M
**/
public class JavaMethodAreaOOM{
public static void main(String []args){
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSupperClass(OOMObject.class);
enhancer.setUserCache(false);
enhancer.setCallBack(new MethodInterceptor(){
public Object intercept(Object obj , Method method , Object []args , MethodProxy proxy)throw Throwable{
return proxy.invokeSuper(obj , args);
}
});
enhancer.create();
}
}
static class OOMObject{
}
}
- String.intern()
public class RuntimeConstantPoolOOM{
public static void main(String []args){
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println("str2.intern() == str2");
}
}
#这段代码在Jdk1.6中运行 , 返回连个false , 在Jdk1.7中运行得到一个true 和 false
#在Jdk1.6中 intern()会把首次遇到的字符串实例复制到永久代中,返回永久代中的实例引用。而StringBuilder#上构造的字符串存在在堆上面,因此必然不是同一个引用。
#在Jdk1.7中 intern()不会复制实例,只是在常量池中记录首次出现实例的引用。所以“计算机软件”第一次出现,因此返回true , 而 "Java"已经在常量池中了,所以返回false
哪些内存需要回收
程序计数器 、虚拟机栈 、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出有条不紊的执行出栈和入栈,所以这几个区域就不需要过多考虑回收了。
Java堆和方法区则不一样,一个接口的多个实现需要内存不一样,一个方法的多个分支需要的内存也不一样,这部分只有在运行时才知道会创建哪些对象,所有内存分配和回收都是动态的。垃圾收集器所关注的是这部分内存。
如何判断对象已死
- 程序计数器
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1。当引用失效时计数器值减1;任何时刻计数器值为0的对象就是不可能呢再被使用的。
引用计数器的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法,一些比较著名的应用案例,微软的COM技术,FlashPlayer,Python语言。Java虚拟机很少选中技术算法管理内存,因为她存在一个问题就是无法解决相互循环引用的问题。
public class ReferenceCountingGC{
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String []args){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
/** 相互引用导致执行到System.gc() 时两个对象的程序引用指针任然不为0 **/
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();/** 实际执行中GC收集器是回收了4MB的堆内存 **/
}
}
- 可达性分析
算法的基本思路是通过一系列被称为“GC Root”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径叫做引用链(Reference Chain),当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的,可以被回收的。
在Java语言中,可作为GC Roots的对象包括下面几种:
1、虚拟机栈中的引用对象
2、方法区中类静态属性引用对象
3、方法区中常量引用的对象
-
引用类型判断
JDK有四种引用,分别是强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)
-
对象的自我拯救
当一个对象在可达性分析算法中不可达,那么对象会被一次标记并且判断是否有必要执行finalize()方法,判断标准是如果没有覆盖finalize() 或者finalize()方法已经执行过,那么就会判断为“没有必要执行”。如果在执行finalize()方法体重新将对象挂载到GC roots上面,那么对象就不会被垃圾收集器回收。
-
回收方法区
方法区回收两部分内容: 废弃常量 和 无用的类
1、废弃常量:加入字符串“abc”进入到常量池中,但是目前系统中没有任何一个对象叫做“abc” , 那么则为废弃常量。
2、无用的类:类的所有实例都已经被回收了;加载该类的ClassLoader已经被回收;该类的java.lang.Class对象没有任何地方被引用。
垃圾收集算法
-
标记-清除算法
分为“标记”和“清除”两个阶段
不足点:
效率低下
会产生大量的内存碎片 -
复制算法
将内存划分为相同的两块,每次只是用其中的一部分,当一块的内存用完了,将上面还存活的内存复制到另外一块上面去,然后将已经使用过的内存一次清清理掉。
缺陷:
内存使用率变为原来的一半。
现在商业都是采用这个收集算法来回收新生代,IBM研究,新生代中98%是“朝生夕死”,所以不需要1:1的比例来划分内存空间。做法是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,Eden和Survivor中还存活的对象一次性复制到另外一块Survivor中,然后清理之前的Eden 和Survivor,HotSpot默认Eden 和Survivor比例 8:1。当Survivor空间不够用时,需要依赖其他内存(例如老年代)进行分配担保。 -
标记-整理算法 ——适用于老年代
先标记存活的对象,然后将存活的对象想一端移动,然后直接清理掉端边界之外的内存。
-
分代收集算法 —— 将内存分为几块,新生代 老年代 然后根据不同点用不同算法进行计算。
HotSpot算法实现
从GC Roots节点找引用链,为了分析工作一致性,在整个执过程中需要“Stop the World”来防止出现在分析对象引用是引用关系还在不断地发生变化。
虚拟机中通过一个OopMap(Ordinary Object Pointer)数据结构来记录哪些地方放着对象引用。
GC日志
[GC 、 [Full GC (发生了Stop The World)
[DefNew 使用默认的Serial收集器时的新生代日志
[ParNew 使用PatNew收集器的新生代日志
[PSYongGen 使用Parallel Scavenge 收集器的新生代日志
[Tentured 表示堆中的老年代区域内存收集日志
[Perm 表示堆外的永久代内存手机日志
内存分配与回收策略
大多数情况下,对象在新生代Eden中分配,当Eden中没有足够空间进行分配时,虚拟机将会发起一次Mintor GC,将Eden中存活的对象放入到Survivor上,当Survivor不足以放下对象时将对象发入到老年代中。
- 新生代Mintor GC
/**
* vm args : -Xms20M -Xmx20 -Xmn10 -XX:+PrintGCDetails -XX:SurvivorRatio=8
**/
public class MintorGC{
private static final _1MB = 1024 * 1024;
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]; // 出现一次Mintor GC
}
}
result:
在进行Mintor GC时,前三个对象会被转移到老年代中,最后一个对象存放在Eden中。
-
将大对象直接进入到老年代中
vm args: -XX:PretenureSizeThreshold=3145728 # 如果对象大小查过3MB,则直接放入到老年代中
-
长期存活的对象进入到老年代中
因为虚拟机采用分代收集的思想来管理内存,所以我们需要区分哪些对象属于新生代哪些对象属于老年代。虚拟机为每个对象定制了应对象年龄(Age)计数器,如果对象在Eden中进过Mintor GC 进入到Survivor中,那么Age++
,当年龄达到一定的程度(默认为15),那么就会进入到老年代。
vm args : -XX:MaxTenuringThreshold=1 #通过该参数设置进入老年代的年龄大小
-
动态对象年龄判定
如果Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象可以直接进入老年代。
-
空间分配担保
在发生Mintor GC之前,虚拟机需要先检查老年代最大可用的空间是否大于新生代中所有对象的总空间,如果这个条件成立,那么MintorGC可以确保安全的进行。如果不成立,则虚拟机会查看HandlePromotionFailture设置值时候允许担保失败,如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试着进行一次MintorGC,尽管这次MintorGC有风险;如果小于或者HandlePromotionFailture没有设置,那么将进行一次FullGC。