JVM学习笔记(运行时数据区——>堆)

1、堆的核心概述

一个进程就有一个Jvm实例,一个实例就有多个线程,多个线程共享方法区还有堆空间,每个线程各自拥有一套(程序计数器、本地方法栈、虚拟机栈)。
在这里插入图片描述

  • 堆是jvm需要管理的最大的一块空间,堆的大小是可以调节的,
  • 几乎所有的对象实例和数组在运行的时候分配到堆上,
  • 栈中可能永远不会存储对象实例和数组,因为栈帧中保存的是引用,这个引用指向存储在堆上的数组和对象实例的位置,
  • 方法结束之后,堆中的对象不会马上被移除,仅在垃圾收集的时候才会被回收
    堆,是GC(Garbage Collection垃圾收集器)执行垃圾回收的重点区域,注意:栈里边没有GC,就入栈和出栈操作

java 7及之前堆内存逻辑上分为三部分:新生区(Young Generation Space)+养老区(Tenure Generation Space)+永久区(Permanent Space)

java 8及之后堆内存逻辑上分为三部分:新生区(Young Generation Space)+养老区(Tenure Generation Space)+元空间(Meta Space)
在这里插入图片描述

设置堆内存大小与OOM

首先找到Edit Configurations,然后找到VM options设置

  • -Xms (ms是memory start)用于表示堆区的其实内存,等价于“-XX:InitialHeapSize”,用来设置堆空间(年轻代+老年代)的初始化内存,默认是:电脑内存 / 64
  • -Xmx(mx是memory max)用于表示堆区最大内存,等价于“-XX:MaxHeapSize”,用来设置堆空间(年轻代+老年代)的最大内存,默认是:电脑内存 / 4

在这里插入图片描述
设置-Xms10m -Xmx10m -XX:+PrintGCDetails后,控制台输出。

Heap
PSYoungGen total 2560K, used 1555K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 75% used [0x00000000ffd00000,0x00000000ffe84e70,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 3065K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 333K, capacity 388K, committed 512K, reserved 1048576K

  • PSYoungGen年轻代 2560k(其中分配了两个512k,但是只会用一个,所以2560k+7168k的和(即内存)会少了0.5M)
  • ParOldGen老年代 7168k

OOM异常说明
HotSpot虚拟机的栈容量是不考研动态拓展的,以前的Classic虚拟机倒是可以。所以在HoySpot虚拟机上是不会有虚拟机栈无法拓展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然会出现OOM异常的

2、年轻代与老年代(Tenured)

堆空间
在这里插入图片描述
配置新生代与老年代在堆结构的占比

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

配置新生代中Eden空间(所有java对象)和两个Survivor空间

默认: 所占空间比是8:1:1,但是使用jVisualVm工具测试的时候发现并不是8:1:1,原因是自适应的问题,如果一定需要使用上面比例,可以使用修改后的,例下方
可以修改: 可通过-XX:SurvivorRatio=8

2.1、新生代对象分配与回收

  • 在Eden分配对象,当Eden区内存满的时候,则将还在使用的对象实例放到survivor0区(YGC或者Minor GC算法),然后删除Eden区的对象
  • Eden区继续使用,循环上一步,当Eden区再次满的时候,将Eden区和suovivor区还在使用的对象实例以及放到survivor1区(YGC或者Minor GC算法),然后删除Eden区和survivor0区的对象
  • 继续第二步操作,循环利用survivor0区和survivor1区,当这两个区中的对象实例年龄达到15的时候,则可以将对象实例放进Tenured/Old区(老年代)

图解分配过程
在这里插入图片描述

总结:

  • 针对幸存者(新生代的Survivor区)s0、s1区的总结:复制之后有交换,谁空谁是to
  • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。

3、Minor GC、Major GC、Full GC

jvm进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分回收的都是指新生代

针对HotSpot VM的实现,它里面的GC回收又分为两大类型:一种是部分搜集(Partial GC),一中是整堆收集(Full GC)

  • 部分收集:不是完整收集整个java堆的垃圾收集。其中又分为
    • 新生代收集(Minor GC / Young GC)只是新生代(Eden / survivor0、suovivor1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代(Tenured)的垃圾收集
      • 目前,只有CMS GC会有单独收集老年代的功能
      • 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • 混合搜集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
      • 目前,只有G1 GC会有这种行为
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

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

  • 当年轻代(Eden / s0、s1)空间不足时,就会触发Minor GC,这里的满指的是Eden区满,Survivor满不会触发GC。(每次Minor GC会清理年轻代的内存)。
  • 因为java对象大多数都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。

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

  • 指发生在老年代的GC,对象从老年代消失时会触发。
  • Major GC的速度一般比Minor GC速度慢10倍以上,STW时间更称。(因为老年代的空间非常大)
  • 如果Major GC之后内存还不足,就会报OOM。

Full GC触发机制:

  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后所需内存大于老年代可用内存,既老年代即将溢出的时
  • 由Eden区,survivor0(假如是From space)向survivor1(To space)区复制时,大于To space 的可用内存,然后转存到老年代,但是大于老年代的可用内存。

4、堆空间分代思想

为什么需要把java堆分代?

  • 经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
    • 新生代:由Eden、两块相同大小的Survivor(又称from/to,s0/s1)构成,to总为空。
    • 老年代:存放新生代中经历多次GC仍然存活的对象。
  • 分代的唯一理由就是优化GC性能。如果没有分代,所有对象放在一个区,则其进行扫描是否回收时,耗时极大。
// vm options设置为-Xms10m -Xmx10m -XX:PrintGCDetails
public class GCTest {
    public static void main(String[] args) {
        String str = "abcdefg";
        List<String> stringList = new ArrayList<>();
        try {
            while (true){
                stringList.add(str);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果:

// [PSYoungGen: 2006K->488K(2560K)] 2006K->855K(9728K),当新生代满的时候,则将数据放入老年代,前部分是新生代数据大小,后面的是整个堆区的大小
[GC (Allocation Failure) [PSYoungGen: 2006K->488K(2560K)] 2006K->855K(9728K), 0.0038590 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2110K->488K(2560K)] 2478K->2122K(9728K), 0.0021302 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1425K->488K(2560K)] 6577K->5639K(9728K), 0.0051247 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 488K->488K(2560K)] 5639K->5647K(9728K), 0.0044672 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
// 默认新生代与老年代在整个堆内存的占比是1:2,2560:7168 != 1:2的原因是自适应,将其关闭即可,这里还有Metaspace,这是自jdk8后,将jdk7的永久代改成了元空间(Metaspace)
[Full GC (Allocation Failure) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 5159K->2735K(7168K)] 5647K->2735K(9728K), [Metaspace: 3220K->3220K(1056768K)], 0.0239226 secs] [Times: user=0.09 sys=0.00, real=0.04 secs] 
// 经过Full GC之后则进行垃圾回收,然后尝试接着放数据
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5901K->5901K(9728K), 0.0010217 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 5901K->5901K(8704K), 0.0006819 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 5901K->3787K(7168K)] 5901K->3787K(8704K), [Metaspace: 3220K->3220K(1056768K)], 0.0599751 secs] [Times: user=0.06 sys=0.00, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 3787K->3787K(9216K), 0.0002665 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
// 最后发现老年代大于其所含空间,整个堆空间也不足,所有就报了OOM
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 3787K->3769K(7168K)] 3787K->3769K(9216K), [Metaspace: 3220K->3220K(1056768K)], 0.0792816 secs] [Times: user=0.08 sys=0.00, real=0.08 secs] 
Heap
 PSYoungGen      total 2048K, used 66K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 6% used [0x00000000ffd00000,0x00000000ffd10aa0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 7168K, used 3769K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 52% used [0x00000000ff600000,0x00000000ff9ae698,0x00000000ffd00000)
 Metaspace       used 3252K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 353K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at com.luo.java.GCTest.main(GCTest.java:14)

Process finished with exit code 1

5、内存分配策略

一般情况:

如果对象在Eden区出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当他年龄增加到一定程度(默认是十五岁,其实每个JVM,每个GC都有所不同)时,就会被晋升到老年代中。(对象晋升老年代的年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)。

针对不同年龄段的对象分配原则如下:

  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果survivor区中的相同年龄的所有对象大小的综合大于survivor空间的一般,年龄大于或者等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中的要求年龄。
  • 空间分配担保
    • -XX:HandlePromotionFailure

大对象直接分配到老年代测试:

vm options设置为-Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails

  • -Xms60m:堆空间分配60兆
  • -Xmx60m:堆空间最大60兆
  • -XX:NewRatio=2:新生代:老年代=1:2
  • -XX:SurvivorRatio=8:新生代中Eden区:Survivor0:Survivor1=8:1:1

内存分配情况
在这里插入图片描述
代码:

public class YoungOldAreaTest {
    public static void main(String[] args) {
        byte[] a = new byte[1024 * 1024 * 20];// 20M
    }
}

运行结果:

Heap
 PSYoungGen      total 18432K, used 2622K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
  eden space 16384K, 16% used [0x00000000fec00000,0x00000000fee8fa88,0x00000000ffc00000)
  from space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
  to   space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)
// 可以看见因为byte数组的1024*1024*20k > Eden区的16384k,则放不进Eden区,就直接放进了老年代,ParOldGen总共内存为40960k,使用了20480k,既20M全部放进来了
 ParOldGen       total 40960K, used 20480K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
  object space 40960K, 50% used [0x00000000fc400000,0x00000000fd800010,0x00000000fec00000)
 Metaspace       used 3220K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

6、为对象分配内存:TLAB(Thread Local Allocation Buffer)

  • 堆是一个线程的多个进程共享的区域,任何线程都可以访问,由于对象创建是非常频繁的,因此在高并发下堆区的划分内存空间是线程不安全的,为避免多个线程操作同一个地址,需要使用加锁等机制,进而影响分配速度。
  • 为了解决线程安全问题,从内存模型,而不是垃圾收集的角度,可以对Eden区进行划分,JVM为每个线程分配了一个私有缓冲区域,它包含在Eden区内,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此又叫做内存分配策略
    在这里插入图片描述
  • 程序中可以通过**-XX:UseTLAB**设置是否开启TLAB空间
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,可以通过-XX:TLABWasteTargetPercent设置TLAB空间所占Eden空间的百分比
  • 一旦TLAB分配失败,JVM就会尝试使用加锁机制确保数据操作的原子性,从而直接在Eden空间进行分配。

7、小结:堆空间的参数设置

-XX:+PrintFlagsInitial:查看所有的参数的默认初始值
-XX:+PrinaFlagsFinal:查看所有的参数的最终值(可能会存在需改,不再是初始值)
-Xms:初始堆空间内存(默认为物理内存的1/64-Xmx:最大堆空间内存(默认为物理内存的1/4-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和s0/s1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
-XX:HandlePromotionFailure:是否设置空间分配担保

8、堆是分配对象的唯一途径吗?(逃逸分析)

不是

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

  • 如何将堆上的对象分配到栈上,需要使用逃逸分析手段。
  • 通过逃逸分析,Java HotSpot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
  • 逃逸分析的基本行为就是分析对象的动态作用域
  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
  • 当一个对象在方法中被定义后,他被外部方法所引用,则认为发生了逃逸。例如作为调用参数传递到其他方法中。

如何快速的判断是否发生了逃逸分析?就观察new的对象在外边是否会被使用

public void test(){
	T t = new T();
	v = null;
}//r认为没有发生逃逸

public T test1(){
	T t = new T();
	return t;
}//认为发生了逃逸

public void test2(){
	T t = test1();
}//认为发生了逃逸

结论:
开发中能使用局部变量的,就不要在方法外定义。

追加 * :jVisualVM工具说明(可视化性能分析工具)

软件说明:Java VisualVM 是一个直观的图形用户界面,可在基于 Java 技术的应用程序(Java 应用程序)在指定的 Java 虚拟机 (JVM) 上运行时提供有关它们的详细信息。
位置:
在jdk的bin目录下在这里插入图片描述
打开方式:
可以双击直接打开,或者在cmd下输入jvisualvm直接打开,前提是配置了jdk环境变量。
使用:
启动程序后,先打开Visual GC(这个刚开始是没有的,可以在上面导航栏找到工具——>插件——>可用插件——>Visual GC,然后无脑安装),蓝色框框的是新生代,红色框框的是老年代,绿色框框的是元数据区。
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老哥不老

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值