JVM内存区域

话不多说直接丢重点。适合学过,巩固的人看。

一、运行时数据区

在这里插入图片描述灰色区域代表线程共享,白色为线程私有。
JVM在运行Java程序的过程中将它所管理的内存划分为若干个数据区域。

  1. 程序计数器:
    可以当做是字节码文件的行号指示器,记录正在执行的指令的地址,唯一一块不存在OutOfMemoryError异常的内存。
  2. Java虚拟机栈:
    描述的是Java方法执行的内存模型,每个方法都会创建一个栈帧(Stack Frame)存放局部变量表、操作数栈、动态链接、方法出口
    局部变量表存放了各种基本数据类型对象引用、returnAddress类型。
  3. 本地方法栈
    与虚拟机栈发挥作用相似。虚拟机栈为虚拟机执行Java方法服务;本地方法栈为虚拟机执行Native方法服务。HotSpot虚拟机将两者合二为一。
  4. Java堆
    线程共享,几乎所有的对象实例都存放在这里。Java堆是垃圾收集的主要区域。可以细分为Eden、From Survivor、To Survivor、老年代。前三个区域又称新生代。Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。可以通过**-Xms**(最小堆内存)和**-Xmx**(最大堆内存)的虚拟机参数实现堆内存的扩展。
  5. 方法区
    线程共享,用于存放已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。
    Java6之前,常量池存放在方法区中。
    Java7,将常量池放到了堆中。
    Java8,取消了整个永久代区域,取而代之的是元空间。
    • 运行时常量池
      方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区运行时常量池中存放。
      运行期间也可以将新的常量放入池中,如String.intern()
  6. 直接内存
    不属于虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域。但这部分内存也被频繁使用。
    在JDK1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,他可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

二、对象

对象的创建过程

内存管理方法

  • 指针碰撞:(Java堆中的内存是绝对规整的)
    所有用过的内存都放在一边,空闲内存放在一边,中间放着一个指针作为分界点的指示器,分配内存就是把指针向内存一边挪动与对象大小相等的距离。
  • 空闲列表:(Java堆中内存不规整)
    虚拟机维护一个列表,记录哪些内存块可用,在分配的时候从列表中找出一块足够大的空间划分给对象实例,并更新列表上的记录。

Java堆是否规整由采用的垃圾收集器是否带有压缩整理功能决定。
Serial、ParNew等带有Compact过程的收集器采用指针碰撞,CMS这种基于Mark-Sweep收集器采用空闲列表。

内存分配线程安全问题

在分配内存的时候可能会有线程安全问题:可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针分配内存的情况。

  • JVM采用CAS配上失败重试的方式保证更新操作的原子性。
  • 每个线程在Java堆中预先分配一小块内存(TLAB),哪个线程需要内存就在哪个线程的TLAB中分配,TLAB用完并分配新的TLAB时才需要同步。使用 -XX:+/-UseTLAB

对象的内存布局

对象头 + 实例数据 + 填充数据

  • 对象头
    运行时数据:哈希码、GC分代年龄、锁标志状态、线程持有的锁、偏向线程ID、偏向时间戳等。(Mark Word)
    类型指针:对象指向它的类元数据的指针。JVM通过这个指针来确定这个对象是哪个类的实例。
    如果对象是数组,对象头上还必须有一块用于记录数组长度的数据。
  • 实例数据
    对象真正存储的有效信息(所定义各种类型字段内容),这部分存储顺序会受到虚拟机分配策略参数(FieldAllocationStyle)和字段定义顺序的影响。HotSpot默认的分配策略是longs/doubles ints shorts/chars bytes/booleans oops(Ordinary Object Pointer)。
    相同宽度的字段总是被分配到一起。
    父类中变量在子类之前。如果设置CompactFields为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。
  • 对齐填充:占位符。

对象的访问定位

  • 使用句柄
    在Java堆中划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息。
    在对象移动的时候,只需要改变句柄中的实例数据指针,而不需要改变reference。
    使用句柄
  • 直接指针
    reference中存放对象地址。
    速度快,HotSpot使用直接指针的方法。
    直接指针

三、垃圾收集器

  1. 哪些对象需要回收
  2. 什么时候回收
  3. 如何回收

确定对象是否存活

引用计数法

用一个引用计数器,记录引用的数量。但是这种方式的不足是,不能找出相互引用的对象

a.obj = b;
b.obj = a;
可达性分析法

通过一系列被称为"GC ROOTS"的对象作为起点,然后向下搜索,从GC ROOTS到某个对象不可达,说明此对象不可用。
可作为GC ROOTS的对象有:

  • 虚拟机栈中局部变量表引用的对象
  • 本地方法栈中JNI引用的对象
  • 方法区中类静态变量引用的对象
  • 方法区中常量引用的对象

JDK1.2后出现四种引用

  • 强引用: Object obj = new Object(); 只要存在永远不会回收。
  • 软引用: 在系统抛出内存溢出异常之前会进行第二次垃圾回收(主要对软引用进行回收),如果还没有足够内存才抛出内存溢出异常。SoftReference实现。
  • 弱引用: 非必需的对象。被弱引用关联的对象只能存活到下一次GC之前。WeakReference实现。
  • 虚引用:(幽灵引用、幻影引用) 无法通过虚引用来取得一个对象实例。能在这个对象被垃圾收集器回收时收到一个系统通知。PhantomReference实现。

方法区回收

主要回收废弃常量、无用的类。
废弃常量就是没有被引用的常量。
无用的类需满足下列条件:

  • 该类的所有实例都已经被回收。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class,没有和任何地方被引用。(无法通过反射访问该类)

-Xnoclassgc 是否对类进行GC
-verbose:class、-XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。
在大量使用反射、动态代理、CGlib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader场景都需要虚拟机具备卸载功能,以保证永久代不会溢出。

垃圾回收算法

  • 标记-清除算法
    标记需要回收的对象,然后统一回收所有被标记的对象。
    不足:
    1. 效率低
    2. 容易产生大量不连续的内存碎片
  • 复制算法
    将内存划分为两块,每次只使用其中一块,当这一块内存用完了,将存活着的对象复制到另一块上面,然后再把使用过的内存清除掉。
    不足:
    1. 内存缩小为原来的一半
    HotSpot默认使用该算法,Eden:Survivor=8:1,当Survivor空间不足时,依赖老年代进行分配担保。
  • 标记-整理算法
    标记,然后让存活对象向一端移动,然后直接清理掉端边界以外的内存。
  • 分代收集算法
    不同区域使用不同收集算法。

垃圾收集器

  1. Serial收集器
    单线程,在GC时,必须暂停其他所有的工作线程,直到收集结束。
    新生代收集器,采用复制算法。
    Serial对于运行在client模式下的虚拟机来说是很好的选择。
  2. ParNew收集器
    Serial的多线程版本。
    在Server模式下,VM首选的新生代收集器。常与CMS配合工作。
  3. Parallel Scavenge收集器
    并行的多线程收集器
    CMS关注:尽可能地缩短垃圾收集时用户线程的停顿时间。
    Parallel Scavenge关注:达到一个可控制的吞吐量。
    吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    -XX:MaxGCPauseMillis:最大GC停顿时间(大于0的ms)
    -XX:GCTimeRatio:表示垃圾收集时间占总时间比率。
    GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。
    -XX:UseAdaptiveSizePolicy:JVM根据运行情况收集性能监控信息,动态调整新生代大小、Eden和Survivor比例、晋升老年代对象年龄等参数以提供最合适的停顿时间或最大的吞吐量
  4. Serial Old收集器
    在Server模式下,与Parallel Scavenge配合。或者作为CMS的后备预案。
  5. Parallel Old收集器
    使用标记-整理算法。
    在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old组合。
  6. CMS收集器
    CMS垃圾回收器详解
    Concurrent Mark Sweep并发标记清除。
    -XX:+UseConcMarkSweepGC:使用CMS。
    垃圾回收特点:回收老年代和永久代(JDK1.8为元数据区,需要设置CMSClassUnloadingEnabled)。
    CMS是一种预处理垃圾回收器,它不能等到老年代内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以cms垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%
    使用场景: GC过程短暂停,适合对时延要求比较高的服务,用户线程不允许长时间的停顿。
    缺点: 服务长时间运行造成严重的内存碎片。
    工作过程:
    CMS工作过程
    1. 初始标记:
      单线程执行
      1. 标记GC Roots可达的老年代对象。
      2. 遍历新生代对象,标记可达的老年代对象。
    2. 并发标记
      1. 该阶段GC线程和应用线程并发执行,遍历1阶段标记出来的存活对象,然后继续递归标记这些对象的可达对象。
      2. 因为该阶段并发执行,在运行期间可能会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等。这些对象需要重新标记。
      3. 为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card对象,避免扫描整个老年代。
    3. 预清理
      通过CMSPrecleaningEnabled关闭该阶段,默认启用。
      1. 处理新生代已经发现的引用
    4. 可终止的预处理
      1. 因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
      2. 该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
      3. 在该阶段,循环两件事情:处理From和To区的对象,标记可达的老年代对象;和上一阶段一样,扫描处理Dirty Card中的对象。打断循环的条件有三个:
        1. 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,意思没有循环次数的限制。
        2. 如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
        3. 如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件能够成立的前提是,在进行预清理时,Eden区的使用率小于十分之一)
    5. 并发重新标记
      1. 并发执行
      2. 有些标记过的或没有标记过的对象可能引用发生了变化,进行如下处理:
        1. 遍历新生代对象,重新标记
        2. 根据GC Roots,重新标记
        3. 遍历老年代的Dirty Card,重新标记
      3. 第一阶段需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在AbortablePreclean阶段中能够恰好的发生一次YGC,这样就可以避免扫描无效的对象。
        如果在AbortablePreclean阶段没来得及执行一次YGC,怎么办?
        CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。
        不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。
    6. 并发清理
      1. 这个阶段主要是清除那些没有标记的对象并且回收空间
      2. 并发执行可能会有新的垃圾,留在下一次GC清理,这一部分垃圾称为浮动垃圾。
    7. 并发重置
      这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。

CMS的并发GC不是full GC

  1. G1收集器
    待以后详细学习

垃圾回收器参数总结

UseSerialGC:JVM运行在Client模式下的默认值,Serial + Serial Old
UseParNewGC:ParNew + Serial Old
UseConcMarkSweepGC:ParNew + CMS + Serial Old(后备)
UseParallelGC:JVM运行在Server模式下的默认值,Parallel Scavenge + Serial Old
UseParallelOldGC:Parallel Scavenge + Parallel Old

SurvivorRatio:Eden:Survivor
PretenureSizeThreshold:晋升到老年代的对象大小
MaxTenuringThreshold:晋升到老年代的对象年龄:坚持一次Minor GC,年龄加1
UseAdaptiveSizePolicy:动态调整Java堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure:是否允许担保失败
ParallelGCThreads:gc时进行回收的线程数
GCTimeRatio:GC时间/总时间,默认99,即允许1%GC时间
MaxGCPauseMillis:GC最大停顿时间(只有Parallel有效)
CMSInitiatingOccupanyFraction:CMS在老年代空间被使用多少后触发垃圾回收,默认68%(CMS有效)
UseCMSCompactAtFullCollection:设置CMS在完成垃圾收集后是否要进行一次内存碎片整理。(CMS有效)
CMSFullGCsBeforeCompaction:CMS在进行若干次GC后在启动一次内存碎片整理(CMS有效)

内存分配与回收策略(Serial / Serial Old)

  1. 对象优先分配在Eden区域
    Minor GC:发生在新生代,频繁、速度快
    Major GC(Full GC):发生在老年代,出现Major GC,经常伴随至少一次的Minor GC(Parallel Scavenge有直接进行Major GC的策略)
    Major GC一般比Minor GC慢10倍以上
  2. 大对象直接分配在老年代
  3. 长期存活的对象将进入老年代
  4. 动态对象年龄判定
    在Survivor空间中 相同年龄所有对象 大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代。
  5. 空间分配担保

空间分配担保

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值