jvm学习第五天—堆

标题:jvm学习第五天—堆

学习内容:

1、堆的概述
2、堆的细分结构
3、新生代,老年代的概述
4、Minor GC / Young GC、Major GC / old GC、Full GC
5、堆空间分代思想、内存分配策略、TLAB
6、逃逸分析


内容详情:

1、堆的概述

一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。
是JVM管理的最大一块内存空间。
堆内存的大小是可以调节的。

《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,
但在逻辑上它应该被视为连续的。

所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( Thread
Local Allocation Buffer,TLAB)。关于TLAB等会会描述。

《Java虚拟机规范》中对Java堆的描述是:
所有的对象实例以及数组都应当在运行时分配在堆上。

我要说的是:“几乎”所有的对象实例都在这里分配内存。—从实际
使用角度看的。

数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,
这个引用指向对象或者数组在堆中的位置。

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

堆,是Gc ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。

OOM异常:一旦堆中的大小超出了堆的最大内存,将会跑出outOfMemoryError异常。

2、 堆的细分结构

JDK7,分为新生代(年轻代),养老代(老年代),永久代
JDK8,将元空间永久替换了永久代。
从逻辑上是分为这三部分,但事实上,元空间的实现是在方法区。
就像大陆的政策现在还不能在台湾实施一样。

在这里插入图片描述
这张图片描述的还不够准确,新生代分为Eden区和survivor0区和survivor1区,至于哪个是from和to不敢保证,这是动态变化的,等会描述GC的再描述。

3、 新生代,老年代的概述与作用

配置新生代与老年代在堆结构的占比。

默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX :NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

几乎所有的Java对象都是在Eden区被new出来的,有些对象太大就直接去老年代了。
绝大部分的Java对象的销毁都在新生代进行了。
IBM公司的专门研究表明,新生代中 80%的对象都是“朝生夕死”的。可以使用选项”一xmn”设置新生代最大内存大小,这个参数一般使用默认值就可以了。

对象分配图解

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑Gt执行完内存回收后是否会在内存空间中产生内存碎片

1.new的对象先放伊甸园区。此区有大小限制。
2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃
圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
3.然后将伊甸园中的剩余对象移动到幸存者0区。
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6.啥时候能去养老区呢?可以设置次数。默认是15次。
可以设置参数:-XX:MaxTenuringThreshold=<N>进行设置。

这个步骤描述的也不太好,结合图我再描述一下。
在这里插入图片描述
首先正常情况下,new的对象都在Eden区,当Eden区满的时候会进行垃圾回收(Young GC/MinorGC),第一次垃圾回收由于s0和s1区都是空的,所以此时s0为to区,s1为from区,所以不是垃圾的进入to区,并给了一个age1,是垃圾的都被回收了,此时s1变为to区,s0变为form区,谁空谁是to.当Eden区再次满的时候又进行垃圾回收,不是垃圾的进入to区,s0中的对象也会被动进行回收,不是垃圾的进入to区,此时from和to又换了,就这样往复的回收,最后年龄到达默认值或设定的值就会进入老年代了。

当to区满了之后,里面的对象就会直接去老年代。

当对象比Eden区的内存还大时,直接进入老年代。

4、Minor GC / Young GC、Major GC / old GC、Full GC

VM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC) ,一种是整堆收集(Full GC)

部分收集:不是完整收集整个Java堆的垃圾收集。
其中又分为:
新生代收集(Minor GC / Young GC):只是新生代的垃圾收集

老年代收集(Major GC / old GC):只是老年代的垃圾收集。

日前,只有CMS GC会有单独收集老年代的行为。
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
日前,只有G1 GC会有这种行为

整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

年轻代GC(Minor GC)触发机制:

当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满
Survivor满不会引发Minor GC。(每次Minor GC会清理年轻代的内存。)

因为Java 对象大多都具备朝生夕灭的特性,所以 Minor GC非常频
繁,一般回收速度也比较快。这一定义既清晰又易于理解。

Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

老年代GC (Major GC/Full GC)触发机制:

指发生在老年代的GC,对象从老年代消失时,我们说“Major GC”或“Full GC发生了。
出现了Major GC,经常会伴随至少一次的Minor GC
(但非绝对的,在Parallelscavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
也就是在老年代空间不足时,会先尝试触发Minor GC。
如果之后空间还不足,则触发Major GC
Major GC的速度一般会比MinorGC慢10倍以上,STW的时间更长。
如果Major GC后,内存还不足,就报OOM了。

Full GC触发机制:(后面细讲)

触发Full GC执行的情况有如下五种:
(1)调用system.gc()时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区( From Space)区向 ( Tospace)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明: Full GC是开发或调优中尽量要避免的。这样暂时时间会短一些。

5、堆空间分代思想、内存分配策略、TLAB

为什么需要把Java堆分代?不分代就不能正常工作了吗?

其实不分代完全可以,分代的唯一理由就是优化GC性能。
如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教到
GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。
而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方
当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

内存分配策略

针对不同年龄段的对象分配原则如下所示:
优先分配到Eden
大对象直接分配到老年代
尽量避免程序中出现过多的大对象,可能是朝生夕死的大对象
长期存活的对象分配到老年代。
动态对象年龄判断
如果survivor区中相同年龄的所有对象大小的总和大于survivor空
间的一半,年龄大于或等于该年龄的对象可以直接进入老年代
无须等到MaxTenuringThreshold 中要求的年龄。

为什么有TLAB ( Thread Local Allocation Buffer ) ?

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据

由于对象实例的创建在JVM中非常频繁,
因此在并发环境下从堆区中划分内存空间是线程不安全的

为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

什么是TLAB ?

从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分
JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,
同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
据我所知所有openJDK衍生出来的JVM都提供了TLAB的设计。

在这里插入图片描述
先使用TLAB再使用蓝色区域。
TLAB的再说明:

尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。|
在程序中,开发人员可以通过选项“一XX:UseTLAB”设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%
当然我们可以通过选项“一XX:TLABWasteTargetPercent”
设置TLAB空间所占用Eden空间的百分比大小。
一旦对象在TAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性
从而直接在Eden空间中分配内存

6、逃逸分析

堆是分配对象存储的唯一选择吗?

在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配
标量替换优化技术将会导致一些微妙的变化
所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。
但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,
一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

此外,前面提到的基于openJDR深度定制的TaoBaoVM,其中创新的GCIH (GC
invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外
并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

开发中能使用局部变量的就不要在方法外定义,因为局部变量在栈内,不参与垃圾回收。

使用逃逸分析,编译器可以对代码做如下优化:

一、栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

代码优化之栈上分配
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,
就可能被优化成栈上分配。分配完成后,继续在调用栈内执行
最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

二、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

代码优化之同步省略(消除)
线程同步的代价是相当高的,同步的后果是降低并发性和性能。
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否
只能够被一个线程访问而没有被发布到其他线程。
如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。
这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在Cpu寄存器中。

代码优化之标量替换
标量(Scalar)是指一个无法再分解成更小的数据的数据,Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量
因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话
那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。
这个过程就是标量替换。

逃逸分析小结∶逃逸分析并不成熟

关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现
而且这项技术到如今也并不是十分成熟的。
其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。
虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。
但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。
那这个逃逸分析的过程就白白浪费掉了。

虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,
这在理论上是可行的,但是取决于JVM设计者的选择。据
我所知,oracle HotspotJVM中并未这么做,这一点在逃逸分析相关的文档里已经说明
所以可以明确所有的对象实例都是创建在堆上。

目前很多书籍还是基于JDK 7以前的版本,JDK已经发生了很大变化
intern字符串的缓存和静态变量曾经都被分配在永久代上
而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区
而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页