# jvm了解一下~

参考资料《深入理解java虚拟机》

java内存区域

运行时数据区域

image
  1. 程序计数器 :可以看成是当前线程所执行字节码的行号指示器。
    • 每个线程都需要一个独立的程序计数器,所以是私有的。(java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式)
    • 如果线程执行的是java方法,计数器记录的是字节码指令的地址;如果是native方法,计数器为空(uundifined),是唯一没有outOfMemoryError的区域。
      • native是本地方法,和平台有关,需要借助c语言。

  2. 虚拟机栈 :线程私有的,生命周期和线程相同。
    • 描述的是java方法执行的内存模型。每个方法在执行时都会创建一个栈帧,用来记录局部变量、动态链接,方法出口等。每个方法的执行 从开始到介绍就是一个栈帧从虚拟机栈入栈到出栈的过程。
      • 什么是栈帧呢?栈帧可以理解为一个方法的运行空间。它主要由两部分构成,一部分是局部变量表,方法中定义的局部变量以及方法的参数就存放在这张表中;另一部分是操作数栈,用来存放操作数。

    • 这个区域规定了两种异常
      • 如果线程请求的栈深度大于虚拟机允许的栈深度,则抛出 stackOverFlowError,比如递归调用
      • 如果虚拟机栈可以动态扩展,但是扩展时申请不到足够的内存,则抛出OutOfMemoryError,比如这个线程运行时创建大量的类。
  3. 本地方法栈 和虚拟机栈类似。区别是虚拟机栈为执行java方法服务,本地方法栈为运行native服务。
  4. java堆 重点来了~ 下面重点分析
    • 是java虚拟机中内存区域最大的一块
    • 是被所有线程共享的区域,虚拟机启动时创建。
    • 几乎所有的对象实例都会在这分配空间
    • 可以是物理上不连续的区域,只要是逻辑上连续即可
    • 如果堆中没有内存可以分配,并且不能扩展的话,抛出 OutOfMemoryError异常
  5. 方法区 non-heap (非堆)
    • 是各个线程的共享区域
    • 用于存放虚拟机加载的类信息,常量,静态变量、即时编译器编译的代码等。
    • 并不能完全等同于永久代(permanent generation)
    • 垃圾回收在这比较少出现,回收目标是常量池和对类型的卸载
  6. 运行时常量池,是方法区的一部分。
    • class文件除了记录类的版本,字段、方法等,还有常量池,用于存放编译期生成的字面量和符号引用,在类的加载后进入该区域。
    • 具有动态性,运行期间也可以将新的常量放入池中。比如string类中的 intern()方法

      string.intern() : 如果字符串常量池中已经包含一个等于string对象的字符串,则返回池中这个字符串的对象,否则,将此字符串对象包含的字符串放入常量池中,并返回次string对象的引用。

    • 受到方法区的限制,当不能申请内存时,抛出OutOfMemoryError异常
  7. 直接内存:不属于虚拟机内存,但是有可能导致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 list = new ArrayList();
        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没有任何引用链项链,就认为对象是不可用的。


      image
    • java中GC root对象包括以下几种
      • 虚拟机栈中引用的对象
      • 方法区中类静态属性引用的对象
      • 方法区中常量引用的对象
      • 本地方法栈中native引用的对象
引用
  • 强引用
    • 程序代码中普遍存在的,例如Object obj=new Object(),只要强引用存在,永远不会被回收
  • 软引用
    • 有用但并非必需的 。在内存溢出之前,会对这些对象列进回收范围进行二次回收。如果回收后还没有内存,就会抛出内存溢出异常。
  • 弱引用
    • 当垃圾收集工作时,不管内存是否足够,都会被回收。
  • 虚引用
    • 为一个对象设置虚引用的唯一目的,就是在回收时会得到一个通知。
生存还是死亡?
  • 如果对象在进行可达性分析之后发现没有GC root相连,那他将会进行一次标记,并进行一次筛选,筛选的条件是有没有必要执行finalize()方法。
    • 当对象没有覆盖finalize()方法,或者虚拟机已经执行过finalize(),则判定为不执行。
    • 如果判定为执行,会将对象放入一个F-Queue中,稍后去执行。执行时会进行二次标记,这个时候如果与引用链建立关联,就可以拯救自己了~~~~~~
回收方法区
  • 回收效率低,而且回收条件非常苛刻。
  • 主要回收废弃常量和无用的类。

垃圾收集算法

标记-清除(Mark-Sweep)

  • 首先标记出需要回收的对象,标记完成后统一回收
  • 缺点:
    • 效率不高:标记和清除效率都不高
    • 空间问题:会导致大量的内存碎片 程序需要分配较大对象时,无法找到连续的内存,不得不提前再进行内存回收。


      标记-清除

复制

  • 将内存氛围两份,每次只使用一份,当这一块用完了,就将还存活的对象复制到另一块上,然后再把这一块内存清空。
  • 好处
    • 不会产生内存碎片,只需移动堆顶指针,顺序分配,操作简单,运行高效。
  • 缺点
    • 将内存缩小一半,代价太大。


      image
  • 应用中 并不是按照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中!!!!

标记-整理

  • 复制算法在对象存活率较高的情况的下,需要进行大量的复制,效率不高。而且还需要额外的空间进行担保,所以老年代不用这种算法。
  • 算法和 标记-清除差不多,只不过是在标记之后不是对回收对象进行清除,而是让存活对象向一端移动,然后直接清理掉端边界以外的内存。


    image

分代收集

  • 根据对象存活周期的不同讲内存分为几块。
  • 一般java堆分为新生代和老生代
    • 新生代大批对象死去,少量存活,就采用 复制算法。
    • 老生代存活率高 就用标记-清除 或者标记-整理。

内存分配与回收

image
  1. 对象优先再Eden区分配
    • 当eden没有足够的内存分配时,虚拟机讲发起一次Minor GC
  2. 大对象直接进入老年代
    • 最典型的大对象就是那种很长的字符串或数组。
    • 虚拟机提供参数 -Xx:PretenureSizeThreshold 大于这个设置值的直接进入老年代
    • 目的:是为了避免eden和survivor之间发生大量的内存复制
  3. 长期存活的对象直接进入老年代
    • 虚拟机给每个对象定义了一个对象年龄计算器
    • 如果这个对象在eden出生,并经过一次minor gc扔存活,并且能够被survivor容纳,年龄+1
    • 对象在survivor中熬过一次minor gc,年龄+1
    • 当到达默认值(15),就晋升老年代。可以通过-Xx:MaxTenuringThreshold设置。
  4. 动态对象年龄判定
    • 如果在survivor中相同年龄所有对象大小的总和大于survivor内存的一半,年龄大于或等于该年龄的对象直接晋升老年代,不用非要限制上面那个参数设定的年龄。
  5. 空间分配担保
    • 只要老年代的连续内存空间>新生代对象总大小 或者 历次晋升的平均大小就会进行minor gc,否则进行full gc。

你可能感兴趣的:(# jvm了解一下~)