参考资料《深入理解java虚拟机》
java内存区域
运行时数据区域
-
程序计数器 :可以看成是当前线程所执行字节码的行号指示器。
- 每个线程都需要一个独立的程序计数器,所以是私有的。(java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式)
- 如果线程执行的是java方法,计数器记录的是字节码指令的地址;如果是native方法,计数器为空(uundifined),是唯一没有outOfMemoryError的区域。
native是本地方法,和平台有关,需要借助c语言。
-
虚拟机栈 :线程私有的,生命周期和线程相同。
- 描述的是java方法执行的内存模型。每个方法在执行时都会创建一个栈帧,用来记录局部变量、动态链接,方法出口等。每个方法的执行 从开始到介绍就是一个栈帧从虚拟机栈入栈到出栈的过程。
什么是栈帧呢?栈帧可以理解为一个方法的运行空间。它主要由两部分构成,一部分是局部变量表,方法中定义的局部变量以及方法的参数就存放在这张表中;另一部分是操作数栈,用来存放操作数。
- 这个区域规定了两种异常
- 如果线程请求的栈深度大于虚拟机允许的栈深度,则抛出 stackOverFlowError,比如递归调用
- 如果虚拟机栈可以动态扩展,但是扩展时申请不到足够的内存,则抛出OutOfMemoryError,比如这个线程运行时创建大量的类。
- 描述的是java方法执行的内存模型。每个方法在执行时都会创建一个栈帧,用来记录局部变量、动态链接,方法出口等。每个方法的执行 从开始到介绍就是一个栈帧从虚拟机栈入栈到出栈的过程。
- 本地方法栈 和虚拟机栈类似。区别是虚拟机栈为执行java方法服务,本地方法栈为运行native服务。
-
java堆 重点来了~ 下面重点分析
- 是java虚拟机中内存区域最大的一块
- 是被所有线程共享的区域,虚拟机启动时创建。
- 几乎所有的对象实例都会在这分配空间
- 可以是物理上不连续的区域,只要是逻辑上连续即可
- 如果堆中没有内存可以分配,并且不能扩展的话,抛出 OutOfMemoryError异常
-
方法区 non-heap (非堆)
- 是各个线程的共享区域
- 用于存放虚拟机加载的类信息,常量,静态变量、即时编译器编译的代码等。
- 并不能完全等同于永久代(permanent generation)
- 垃圾回收在这比较少出现,回收目标是常量池和对类型的卸载
- 运行时常量池,是方法区的一部分。
- class文件除了记录类的版本,字段、方法等,还有常量池,用于存放编译期生成的字面量和符号引用,在类的加载后进入该区域。
- 具有动态性,运行期间也可以将新的常量放入池中。比如string类中的 intern()方法
string.intern() : 如果字符串常量池中已经包含一个等于string对象的字符串,则返回池中这个字符串的对象,否则,将此字符串对象包含的字符串放入常量池中,并返回次string对象的引用。
- 受到方法区的限制,当不能申请内存时,抛出OutOfMemoryError异常
- 直接内存:不属于虚拟机内存,但是有可能导致OutOfMemoryError异常
虚拟机对象
……
OutOfMemoryError异常分析
堆溢出
* VM Args: -Xms(堆的最小值)20m -Xmx(堆的最大值)20m:都设置成20m 防止堆内存自动扩展
* - XX:+HeapDumpOnOutOfMemoryError oom时生成dump文件
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<HeapOOM.OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
运行结果:
Exception in thread "main" Heap dump file created [2314620069 bytes in 32.813 secs]
java.lang.OutOfMemoryError: Java heap space
- 内存泄露
- 查看泄露对象到gc root的引用链(不太会找 哈哈)
- 内存溢出
- 检查堆参数 xms xmx,是否还能调大。检查代码是否存在对象生命周期过长等情况。
虚拟机栈和本地方法栈溢出
单线程
==Xss:设置每个线程的堆栈大小==
/**
* hotspot虚拟机不区分虚拟机栈和本地方法栈 所以只设置xss
* VM Args:-Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}
运行结果:
stack length: 978
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
……
- 如果线程请求的栈深度大于虚拟机允许的栈深度,则抛出 stackOverFlowError
- 虚拟机栈扩展栈时申请不到足够的内存,则抛出OutOfMemoryError
当栈空间无法分配时,是已使用的栈空间太大,还是内存空间太小?不管是调用xss减少栈内存容量,还是增大本方法中本地变量表的长度,当内存无法分配时,都抛出stackOverFlowError异常。
多线程
- 多线程下的内存溢出,与栈空间是否大不存在任何联系
- 同等物理内存下,为栈每个栈空间分配的内存越大,可以创建的线程就越少。
- 如果不能减少线程数,就只能减少最大堆(增加虚拟机栈的内存?)和减少栈容量来获得更多线程。
public class JavaVMStackOOM {
private void dontStop() {
while(true) {
}
}
// 多线程方式造成栈内存溢出 OutOfMemoryError
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
方法区和运行时常量池溢出
方法区用于存放class的信息,如类名、修饰符、常量池、字段描述等。对于该区域的测试,==思路就是运行时产生大量的类去填满方法区,直到溢出。==
- CGLib动态生成类导致的方法区溢出
/**
* -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method m, Object[] objs, MethodProxy proxy) throws Throwable {
// TODO Auto-generated method stub
return proxy.invokeSuper(obj, objs);
}
});
enhancer.create();
}
}
}
运行结果:
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
......
对程序的讲解参考 CGLIB enhancer讲解
在经常动态生成大量class的应用中,要特别注意类的回收情况,
-
CGLib:Code Generation Library
- CGLIB是一个强大的、高性能的代码生成库。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作
- 原理:动态生成一个要代理类的子类,之类要实现代理类的所有方法(除了final修饰的)。在之类中利用方法拦截技术拦截所有代理类的方法的调用,顺势织入横切逻辑。
-
CGLIB和Java动态代理的区别
- Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类;
- Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效
本地内存直接溢出(感觉不常用,略,有兴趣可以参考《深入理解java虚拟机 2.4.4小节》)
垃圾收集器和内存分配
对象还存活吗?
- 判断对象是否存活,并不是给对象添加一个引用计数器。尽管有时候效率还是很高,java虚拟机并没有采用,因为没办法解决对象之间相互循环引用的问题。
- java中是采用可达性分析算法来判断对象是否存活的。
-
算法思想:通过GC root的对象作为起点,从这个节点向下搜索,搜索经过的路径叫做引用链,当一个对象到GC root没有任何引用链项链,就认为对象是不可用的。
- java中GC root对象包括以下几种
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中native引用的对象
-
引用
- 强引用
- 程序代码中普遍存在的,例如Object obj=new Object(),只要强引用存在,永远不会被回收
- 软引用
- 有用但并非必需的 。在内存溢出之前,会对这些对象列进回收范围进行二次回收。如果回收后还没有内存,就会抛出内存溢出异常。
- 弱引用
- 当垃圾收集工作时,不管内存是否足够,都会被回收。
- 虚引用
- 为一个对象设置虚引用的唯一目的,就是在回收时会得到一个通知。
生存还是死亡?
- 如果对象在进行可达性分析之后发现没有GC root相连,那他将会进行一次标记,并进行一次筛选,筛选的条件是有没有必要执行finalize()方法。
- 当对象没有覆盖finalize()方法,或者虚拟机已经执行过finalize(),则判定为不执行。
- 如果判定为执行,会将对象放入一个F-Queue中,稍后去执行。执行时会进行二次标记,这个时候如果与引用链建立关联,就可以拯救自己了~~~~~~
回收方法区
- 回收效率低,而且回收条件非常苛刻。
- 主要回收废弃常量和无用的类。
垃圾收集算法
标记-清除(Mark-Sweep)
- 首先标记出需要回收的对象,标记完成后统一回收
- 缺点:
- 效率不高:标记和清除效率都不高
-
空间问题:会导致大量的内存碎片 程序需要分配较大对象时,无法找到连续的内存,不得不提前再进行内存回收。
复制
- 将内存氛围两份,每次只使用一份,当这一块用完了,就将还存活的对象复制到另一块上,然后再把这一块内存清空。
- 好处
- 不会产生内存碎片,只需移动堆顶指针,顺序分配,操作简单,运行高效。
- 缺点
-
将内存缩小一半,代价太大。
-
- 应用中 并不是按照1:1的比例划分内存。而是把eden和survivor按照8:1分配。回收时,将eden和from sur中存活的对象一次性的放到to sur中。当survivor内存不够时,需要old gen进行分配担保。这些对象将直接进入老年代区域。(==详细的参考下面的内存分配与回收==)
小问题,为啥要有两个survivor?
当你把eden和from sur复制到to sur中后,清除eden和from 。现在只有to中有数据了。
to中有数据 怎么做下一次的minor gc呢? 所以 要把to中的数据 再复制到from中!!!!
标记-整理
- 复制算法在对象存活率较高的情况的下,需要进行大量的复制,效率不高。而且还需要额外的空间进行担保,所以老年代不用这种算法。
-
算法和 标记-清除差不多,只不过是在标记之后不是对回收对象进行清除,而是让存活对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集
- 根据对象存活周期的不同讲内存分为几块。
- 一般java堆分为新生代和老生代
- 新生代大批对象死去,少量存活,就采用 复制算法。
- 老生代存活率高 就用标记-清除 或者标记-整理。
内存分配与回收
- 对象优先再Eden区分配
- 当eden没有足够的内存分配时,虚拟机讲发起一次Minor GC
- 大对象直接进入老年代
- 最典型的大对象就是那种很长的字符串或数组。
- 虚拟机提供参数 -Xx:PretenureSizeThreshold 大于这个设置值的直接进入老年代
- 目的:是为了避免eden和survivor之间发生大量的内存复制
- 长期存活的对象直接进入老年代
- 虚拟机给每个对象定义了一个对象年龄计算器
- 如果这个对象在eden出生,并经过一次minor gc扔存活,并且能够被survivor容纳,年龄+1
- 对象在survivor中熬过一次minor gc,年龄+1
- 当到达默认值(15),就晋升老年代。可以通过-Xx:MaxTenuringThreshold设置。
- 动态对象年龄判定
- 如果在survivor中相同年龄所有对象大小的总和大于survivor内存的一半,年龄大于或等于该年龄的对象直接晋升老年代,不用非要限制上面那个参数设定的年龄。
- 空间分配担保
- 只要老年代的连续内存空间>新生代对象总大小 或者 历次晋升的平均大小就会进行minor gc,否则进行full gc。