JVM之堆和方法区的笔记

一 堆(Heap)

1、概念

一个JVM实例只存在一个堆,Java堆区在启动JVM时即被创建,其空间大小也就确定了,但可以通过参数进行调整空间大小。

是所有线程共享的一块内存区域,是“几乎”所有的对象实例分配内存的区域。

2、堆结构

Java堆是垃圾收集器管理的内存区域,也被称作“GC堆”。现代垃圾收集器大部分都是基于分代收集理论设计,细分为:

Java7及之前堆内存逻辑分为:新生代、老年代及永久代

Java8及之后分为:新生区、来老年区及元空间

3、设置堆内存大小与OOM

Java堆(年轻区和老年区)是用于存储Java对象实例,大小可以通过参数进行设置

-Xms #用于表示堆区起始内存
-Xmx #表示堆区最大内存

一旦堆区中内存大小超过“-Xmx“所指定的最大内存时将会抛出OOM异常,

查看设置的参数:方式一

jps
jstat -gc 进程ID

方式二:-XX:+ PrintGCDetails

可视化工具:自带的jconsole

4、年轻代和老年代

Java堆区进一步细分分分为:年轻代(YoungGan)和老年代(OldGen),其中年轻代可以划分为:Eden空间、survivor0 空间和 survivor1空间。

5、对象分配过程

(1)new的对象先放在eden区,当Eden满时JVM的垃圾回收起将对Eden进行垃圾回收,将不再被其他对象所用的对象进行销毁。

(2)然后Eden区中的剩余对象移动到幸存者S0区

(3)如果再次触发垃圾回收,此时上次幸存者下来的放到幸存者S0区,如果没有回收就会放到幸存者S1区

(4)如果再次经历垃圾回收,此时会重新放回幸存者S0区,接着再去幸存者1区

(5)啥时候区养老区?可以设置次数,默认15次

-XX:MaxTenuringThreshold=<N>

总结:

关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

6、常用优化工具

JDK命令行、Jconsole、VisualVM、Jprofiler、Java Flight Recorder、GCViewer、GC Easy

7、Minor GC、Major GC 及Full GC的对比

(1)针对hotspot VM的实现,它里面的GC按照回收区域又分为两大类型:部分收集(Partial GC)和整堆收集(Full GC)

部分收集:不是完整收集整个Java堆垃圾收集,其中又分为:

  • 新生代收集(MinorGC/YoungGC):只是新生代(Eden和S0,S1)的垃圾收集

  • 老年代收集(MajorGC/OldGC):只是老年区代垃圾收集。

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

    • 目前只有G1GC会有这种行为

整堆收集(FullGC):收集整个Java堆和方法区垃圾收集

(2)年轻代GC(MinorGC)触发机制

  • 当年轻代空间不足时就会触发MinorGC,这里的年轻代满指的是Eden代满,survive满不会触发GC

  • 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

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

(3)老年代GC(MajorGC/FullGC)触发机制

  • 当老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC

  • Major GC 的速度比MinorGC慢10陪以上,导致STW的时间更长

  • 如果Major GC后内存还不足,就报OOM

(4)FullGC(后面细讲)

  • 触发Full GC执行的情况有如下五种:
    • 调用system.gc() 时,系统建议执行Full GC,但不是必然执行
    • 老年代空间不足
    • 方法区空间不足
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    • 由Eden区、survivor space 0(From space)区向survivor space1(To space)区复制时,对象大小大于Tospace可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

说明:Full GC 是开发或调优中尽量避免的

GC举例及日志分析

源码:

import java.util.ArrayList;

public class GCTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            ArrayList<String> list = new ArrayList<>();
            String str = "cathax";
            while (true) {
                list.add(str);
                str = str + str;
                i++;
            }
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("遍历次数:" + i);
        }
    }
}

参数设置:

-Xms9m -Xmx9m -XX:+PrintGCDetails
8、对象分配过程:TLAB

堆区是线程共享区域,任何线程 都可以访问到堆中的共享数据。由于对象实例的创建在JVM中非常频繁,因此在开发环境下堆区 中划分内存空间是线程不安全的,为了避免多个线程操作同一个地址,需要使用加锁等机制,进二影响分配速度。

9、总结:堆空间的参数设置
-Xms:初始化堆空间
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:NewRatio:设置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden区和S0/S1空间比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGGCDetails:输出详细的GC处理日志
-XX:+PrintGC:打印GC简要信息
-verbose:gc:打印GC简要信息
-XX:HandlePromotionFailure:是否设置空间分配担保

10、堆空间分代思想

堆分代目的优化GC性能,

二、方法区

1、栈、堆、方法区的交互关系

141628847214_.pic_hd.jpg

Person person = new Person();
Person存放方法区
person存放栈
new Person()存放Java堆中
2、方法区的理解

方法区与Java堆一样都是各个线程共享的内存区域,方法区在JVM启动时候被创建,并且它的实际的物理内存空间中和 Java堆区一样都可以是不连续的,空间大小可以选择固定或者可扩展的。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机会抛出溢出异常。

3、HostPot中方法区的演进

在JDK7及以前习惯把方法区称为永久代,到JDK8开始使用元空间取代了永久代了

4、设置方法区大小与OOM

JDK 7及以前:

通过-XX:PermSize来设置永久代初始分配空间,默认值是20.75M

-XX:MaxPermSize来设置永久代最大可分配空间,32位机器默认是64M,64位机器默认是82M

如果JVM加载的类信息容量超过了这个值会抛出OOM:PermGen space

JDK8开始

元数据大小可以使用参数-XX:Metaspacesize和-XX:MaxMetaspaceSize指定代替JDK7及之前的参数

-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m 
5、举例方法区OOM异常
6、如何解决OOM

(1)通过内存映像分析工具对DUMP出来的堆转储快照进行分析,分析到底是内存泄露(Memory Leak)还是内存溢出(Memory OverFlow)

(2)如果是内存泄露,我们可以通过工具查看泄漏对象到GC Roots的引用链,这样可以找到泄漏的对象是怎么样的路径与GC Roots相关联导致垃圾收集器无法自动回收。

(3)如果不存在内存泄漏,内存中的对象确实还必须存活着,那就检查堆参数(-Xmx 与 -Xms)

7、方法区内部结构

方法区主要存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓冲等。

常量池中有:数量值、字符串值、类引用、字段引用、方法引用

运行时常量池:是方法区的一部分

常量池是class文件的一部分,用于存放编译期生成的各种字面亮与符号引用,这部分内容将在类加载放到方法区的运行时常量池中。

8、JDK7 、JDK8的区别

Hotspot虚拟机中方法区的变化:

JDK6及之前,有永久代(Permanent generation),静态变量存放在永久代上

JDK7有永久代,但是已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中

JDK8及之后,没用永久代、类型信息、字段、方法区、常量保存在本地内存的元空间,但是负资产常量池、静态变量仍在堆中

9、永久代为什么换成元空间?

在某种场景下,如果动态加载类过多,就很容易产生Perm区的OOM,就比如Web工程因为功能点比较多,在运行时要不断动态加载很多类,这样就容易出现OOM。元空间和永久代之间最大区别是:元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制。还有永久代调优比较困难,空间大小很难确定。

10、方法区的垃圾收集

方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型

三、对象的创建过程

对象创建步骤:
  1. 判断对象对应的类是否加载、链接、初始化:当我们通过new对象,它首先查看这个指令的参数能否在metaSpace的常量池中定位到一个类的引用符号,且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有加载那么就在双亲委派模式下使用当前类加载器以classLoader+包名+类名为Key进行查找对应的class文件,如果没有找到文件会抛出classNotFountException异常,如果找到则进行加载并生成对应class类对象。
  2. 为对象分配内存
  3. 处理并发安全问题(因为堆是共享的,所有可能会出现并发问题):采取CAS失败重试,区域加锁保证更新的原子性 和 每个线程预分配一块TLAB-通过-XX:+/-UseTLAB参数来设定。
  4. 初始化分配到的空间:所有属性默认值,保证对象实例字段在不赋值是可以直接使用
  5. 设置对象的对象头:就是将对象的hashCode和对象 GC信息、锁信息等数据存储在对象头
  6. 执行init方法进行初始化

简介步骤:

1、加载类元信息;2、为对象分配内存;3、处理并发问题;4、属性默认初始化;5、设置对象头信息;6、属性显示初始化、代码块中初始化、构造器初始化

对象在堆中内存布局
  • 对象头(header):

    • 运行时元数据(Mark Word):包括哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
    • 类型指针:指向类元数据instanceKlass,确定改对象所属的类型
  • 实例数据(Instance Data)

  • 对齐填充

四、对象访问定位

JVM是如何通过栈桢中的对象引用访问到其内部的对象实例?

更多收录于GitHub:https://github.com/metashops/GoFamily

Go学习路线
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值