Java 内存模型

http://www.infoq.com/cn/articles/java-memory-model-1
http://www.jianshu.com/p/c9ac99b87d56
https://www.ziwenxie.site/2017/06/01/java-jvm-memory-model/

参考

秋水Leo - Java 内存区域和GC机制

Java GC 机制任务

  • 确定哪些内存需要回收
  • 确定什么时候需要回收
  • 如何回收

Java 内存区域

Java 内存模型_第1张图片
Java 内存区域
  1. 程序计数器(Program Counter Register)
    用于指示当前线程所执行的字节码到了第几行. 每个程序计数器只用来记录一个线程的行号, 所以它是线程私有的(一对一).
    如果在执行的是Java方法, 那么计数器记录当前正在执行的字节码的指令地址, 如果在执行的是本地native方法, 那么值是未定义的. 由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区 域中唯一一个没有定义OutOfMemoryError的区域.

  2. 虚拟机栈(JVM Stack)
    一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
    栈中定义了两种异常:

    1. StackOverFlowError
      线程调用的栈深度超过虚拟机允许的最大深度, 则会抛出该异常
    2. 如果java虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常。
  3. 本地方法栈(Native Method Stack)
    本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

  4. 堆区(Heap)
    堆区是JVM所管理内存中最大的一块, 由所有线程共享. 堆区的存在是为了存储对象实例. 原则上来说, 所有对象都是在堆区分配内存. 不过也有例外, 有些情况也可以在栈上直接分配内存.
    OutOfMemoryError会在此抛出.

  5. 方法区(Method Area)
    方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

  • 运行时常量池
    是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。
  1. 直接内存
    直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是 JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

Java内存分配机制

分代分配, 分代回收
依据对象的存货时间分为以下几代:

  • 年轻代(Young Generation)
  • 老年代(Old Generation)
  • 永久代(Permanent Generation, 方法区)

年轻代(Young Generation)

对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代). 大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消 亡的),这个GC机制被称为Minor GC或叫Young GC. Minor GC表示在Eden区上的GC.
内存分配:

  1. Eden区(伊甸园)
    内存首次分配的区域
  2. 存活区(Survivor0, Survivor1)

内存分配过程图


Java 内存模型_第2张图片
内存分配过程图
  1. 大部分刚创建的对象会被分配在Eden区. Eden区是连续的内存空间, 所以分配速度快.
  2. 当Eden区满的时候, 执行Minor GC, 将需要清理的对象清理掉, 并将剩余的对象复制到一个存活区Survivor0(假设此时Survivor1是空的, 两个Survivor区总有一个是空白的).
  3. 每次Eden区满了之后, 都会执行一次Minor GC(或者Young GC), 将存活对象复制到存活区Survivor0
  4. 当Survivor0也满的时候, 将其中仍然存活的对象直接复制到Survivor1, 以后Eden区执行Minor GC后, 就将存活的对象添加到Survivor1(此时Survivor0是空白的).
  5. 当两个存活区切换了一定次数(虚拟机设置)之后, 仍然存活的对象(一小部分, 比如我们自己定义的对象)就会被移入到老年代(Old Generation).

Eden区是连续的空间, 且2个Survivor中总会有一个是空的. 经过一次Minor GC和复制后, 一个Survivor中保存着仍然存活的对象, 而Eden区和另一个Survivor区的内容可以直接被清空(应该已经不再需要了).

该方法称为 停止-复制(Stop-and-copy)清理法.

在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配:

  1. bump-the-pointer
    因为Eden区是连续的, 所以直接跟踪最后一个创建的对象, 再创建新的对象时, 直接检查最后一个对象后面是否有足够内存即可.
  2. TLAB(Thread-Local Allocation Buffers)
    TLAB技术是对于多线程而言的,将Eden区分为若干 段,每个线程使用独立的一段,避免相互影响.

老年代(Old Generation)

对象如果在年轻代存活了足够时间仍然没有被清理掉, 则会复制到老年代, 老年代的空间一般比年轻代大. 在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。

如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。

可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

Java GC机制

年轻代

算法: 停止-复制清理算法
因为绝大部分对象都是短时间就不会再次被引用, 所以Eden区比Survivor区要大, 默认是8:1:1, 可通过-XX:SurvivorRatio来配置这一比例.

老年代

算法: 标记-整理算法. 即, 标记出仍然存活的对象(存在强引用的),将所有存活的对象向一端移动,以保证内存的连续。
Major GC(Full GC)触发条件: 在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC, 否则,就查看是否设 置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不 允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。

方法区(永久代):

永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:

  1. 类的所有实例都已经被回收
  2. 加载类的ClassLoader已经被回收
  3. 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

你可能感兴趣的:(Java 内存模型)