java虚拟机之堆空间,垃圾回收

运行时数据区

介绍

堆空间是线程共享的。
一个jvm实例只存在一个堆内存,堆是java内存管理的核心区域,
Java堆区域在jvm启动的时候即被创建,其空间大小也就确定了,是jvm管理的最大一块内存空间

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

所有的线程共享java堆,可以划分为线程私有的缓冲区。
那么多个程序就是多个jvm实例。

一、描述

设置堆空间大小,   -Xms10m -Xmx10m   初始10m, 最大10m
使用工具可以查看堆空间的分配 
   在jdk的安装目录,bin下一个视图工具jvisualvm

如果堆空间大小超过设置的最大值,那么会报outofmemoryerror异常


一般初始大小和最大大小设置成相同的,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分割计算堆区,
从而提高了性能
细说就是当初始值不等于最大值的话,如果空间不足,需要扩容,那么空间足够时,
又会去掉一些空间,那么这样一来一去,就会影响程序的性能问题。

几乎所有的对象实例以及数组都在这里分配,注意,是几乎所有,这里有一个伏笔。
数组和对象可能永远不会存储在栈上,因为栈帧中保存索引,这个引用指向对象或者数组在堆中的位置

在方法结束后,堆中的对象不会马上移除,仅仅在垃圾收集的时候才会移除,是垃圾回收的重点
现代垃圾收集器大部分都基于分代收集理论设计,

堆空间细分为:
 Java 7及以前内存逻辑分为三部分:新生区+养老区+永久区
        Youn generation space   新生区             
           又可以分为伊甸区(Eden)和幸存者区(Survivor)
        Tenure generation space 养老区
        Permanent space       永久区

 Java 8及以后堆内存逻辑上分为:新生区+养老区+元空间
      Meta space 元空间

-Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
      -X 是jvm的运行参数
      Ms是初始大小

-Xmx   
    Mx是最大大小

这些都表示字节大小
  如果表示kB    则 -Xms1024k 
  如果表示MB  则-Xms1024m
  GB      则-Xms1024g


默认的初始大小是总内存的  1/64
最大默认内存是总内存的1/4


可以使用Runtime.getRuntime()获取实例查看运行时数据,可以查看堆内存大小


注意: 查看的堆内存大小会比设置的大小或者默认的大小更小的,
      原因在于年轻代分为了伊甸区和两个幸存者区,虽然都分配的空间大小
      但由于算法的原因,会将伊甸区的数据复制一份到幸存者区的其中一个,
      所以计算的堆大小时,其中一个幸存者区是为空的,
      
注意:设置的堆内存大小不涉及永久代,


查看
Jps  使用命令行窗口  
会显示当前程序的进程及进程id
  1.Jstat -gc 进程id  查看进程使用的内存情况

  2.-XX:+PrintGCDetails    使用这种方式也可以打印内存信息

当堆空间满了,会报OutOfmemoryerror 

年轻代和老年代

老年代

存储在jvm中的java对象可以划分为两类:
   一类是生命周期较短的瞬时对象,这类的对象的创建和消亡都很快
   一类是周期非常长的,在某些极端的情况下还能够与jvm的生命周期保持

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

设置
默认 -XX:NewRatio=2   表示老年代占2,新生代占1
  命令行查看老年代占多少
  Jinfo -flag NewRatio  进程id

年轻代

年轻代里面的伊甸区和幸存者区 缺省所占的比例是8:1:1

开发人员可以通过  -XX:SurvivorRatio=8,调整空间比例  
   可以使用-Xmn:设置新生代的最大内存

对象分配过程

1.new的对象先放在伊甸园区,此区有大小限制
2.当伊甸园区的空间填满后,程序还需创建对象时,jvm的垃圾回收算器将对伊甸园区进行垃圾回收,
  将伊甸园区中的不在被其他对象所引用的对象进行销毁,再将新对象放在伊甸园区
3.然后将伊甸园中的剩余对象移动到幸存者04.如果在次触发垃圾回收,伊甸园中的幸存者不是指定被放在幸存者0区,而是那个幸存者区空着,
  就放在那个幸存者区,注意:一定会有一个是空的,但是不确定是那个, 
  新对象继续放在伊甸园区,而上次放在幸存者0区的,如果没有回收,就会放在幸存者1区,
5.再次经历回收,则如果幸存者区的对象没有被回收,那么会继续从一个幸存者区转移到另一个幸存者区
6.每一个垃圾回收没有被回收的对象都会有一个计数,相当于年龄一样,当幸存者区中的计数达到了15的计数时,
  再次垃圾回收,那么就会进入到老年区,进入到老年代后,回收的频率就较少了
  
但这个15是个默认的值,我们也可以去设置这个值,设置当计数到什么的时候才进入到老年代

设置参数:-XX:MaxTenuringThreshold=?  进行设置

垃圾回收

Minor GC  Major GC  Full GC

Jvm在进行gc时,并非每次对 新生代,老年代,方法区一起回收,但大部分的回收是指新生代
Gc按照区域分为两种类型:一是部分收集;一种是混合收集:

部分收集:就是不是完整的收集整个堆内存的垃圾,其中又分为:
   新生代收集:Minor gc/ Young GC  :只是新生代的垃圾收集
   老年代收集:Major GC /Old GC: 只是老年代的垃圾收集
      目前,只有CMS GC会有单独收集老年代的垃圾行为
      注意;很多时候Major gc和Full GC混淆使用,需要分辨是老年代回收还是整堆回收
      
混合收集(Mixed GC): 收集整个新生代以及部分老年代的垃圾收集
      目前只有G1 Gc有这种行为

整堆(全局)收集 Full gc  堆整个堆空间和方法区进行垃圾回收

标记清除/标记整理算法:FullGC又叫MajorGC(全局GC)

老年代一般是由标记清除或者是标记清除与标记整理的混合实现
标记清除(Mark-Sweep)
标记整理(Mark-Compact)



标记清除(Mark-Sweep)
1.标记:从根集合开始扫描,对存活的对象进行标记
2.扫描整个内存空间,回收未被标记的对象,使用free-list记录可以区域
  不需要额外的空间,两次扫描,耗时严重,会产生内存碎片,清理出来的空闲内存不是连续的


标记整理(Mark-Compact)
   可以进行碎片的处理,是空间连续 
  唯一的缺点就是效率不高,不仅要标记所有存活的对象,还要整理所有存活的对象的引用地址,从效率上看,该算法是要低于复制算法的




内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度)
内存整齐度: 复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法
 
没有最好的算法,只要最合适的算法===》分代收集算法

年轻代垃圾回收触发机制

注意:Young  gc/minor gc垃圾回收的触发是伊甸园满了才触发的,  
    幸存者区是不会触发垃圾回收的,但是幸存者区存在垃圾回收,
    也就是说幸存者区的垃圾回收是依赖于伊甸园区的触发条件,属于被动回收
    
    触发垃圾回收后,幸存的放在幸存者区,此时伊甸区是空的,
    幸存者区的数据是通过算法复制的,垃圾回收时,伊甸区和之前数据存放的幸存者区会被清除,
    也就是说一定会有一个幸存者区是空的,复制算法不会产生内存碎片,
     
    幸存区幸存的数据没经历一次垃圾回收,相当于年龄加一,当加到15时,在遇到垃圾回收,就会被移到老年代中

Minor Gc会引发STW ,暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行
   线程分为守护线程和用户线程, 而垃圾回收线程是属于守护线程

老年代 触发机制

 指对象在老年代消失,发生了Major gc 或者 Full Gc 
  出现了Major GC ,经常会伴随至少一次的Minor GC(但并非绝对的,
  在Paraller Scavenge收集器的策略里就有直接进行Major GC的策略选择过程)
  也就是在老年代空间不足时,会先尝试触发Minor Gc ,如果之后空间还不足,则会触发Major GC

 Major GC 的速度一般会比Minor gc 满10以上,StW的时间更长
 如果Major GC后,内存还是不足,则会报oom异常

触发full gc机制

有五种情况
  1.调用System.gc()时,系统建议执行full gc 但是不必然执行
  2.老年代空间不足
  3.方法区空间不足
  4.通过Minor gc后进入老年代的平均大小大于老年代的可用内存
  5.右Eden区,survivor spance0(from space)区向survivor spance2(to space)区复制时,
    对象大小大于to Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
Fill gc是开发中尽量避免的

为什么堆内存要分代 ?
原因,是为了优化gc性能,因为权威机构研究表明,大部分的对象都是临时对象,所以分代有利于优化gc

内存分配策略

优先分配到伊甸园区
  大对象直接分配到老年代 ,(长的字符串或数组等)
     尽量避免程序中出现过多的大对象
 长期存活的对象会放在老年区,经历过15次Minor gc的对象

动态对象年龄判断
当Survivor(幸存者区)当幸存者区的相同年龄的对象的大小总和大于幸存者区的总空间的一半是,
 那么,年龄大于或等于该对象年龄的对象则会进入到老年代,就不需要经过阈值来判断是否进入老年区了

空间分配担保
   -XX:HandlePromotionFailure  后面细说
   是布尔值

TLAB

Thread Local Allocation Buffer
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建在jvm频繁,因此在并发环境下从堆中划分内存是线程不安全的


从内存模型而不是垃圾收集的角度,对伊甸1园区进行划分,jvm为每个线程分配了一个私有的缓冲区,
它包含在伊甸园空间内
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此,我们可以将这种内存分配方式称为快速分配策略

尽管不是所有的对象实例都能够在tlab成功分配内存,但jvm确实将tlab作为了内存分配的首选

可以通过 -XX:UseTLAB设置是否开启TLAB空间
默认情况下,tlab空间小,仅占伊甸园的1%.可以通过参数设置大小
  -XX:TLABWasteTargetPercet设置空间占eden的空间占比。

一旦对象在TLAB空间分配失败后,jvm会尝试着通过锁机制确保数据的原子性,从而直接在eden区分配



查看所有参数的默认初始值
-XX:PrintFlagsInitial

查看所有的参数的最终值
-XX:PrintFlagsFinal

查看具体的参数的值,在cmd命令行窗口
Jps 显示进程
Jinfo -flag 参数 进程id

在发生Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对的总空间

如果大于:则此次Minor GC是安全的

如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
   如果参数值为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
      如果大于,则尝试进行一次Minor GC ,但这次Minor GC 依然是有风险的
      如果小于,则改为进行一次Full GC
     如果参数值为false,则改为进行一次Full GC

Jdk1.7后,该参数已经失效了,但是失效后,规则还是相当于改参数设置成了true一样

逃逸分析

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

虽然堆管存储,但是有一种情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么久可能被优化成栈上分配,那么就无需堆上分配内存,也无需进行垃圾回收,是最常见的堆外存储技术

将堆上的对象分配到栈上,需要使用逃逸分析手段
这是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法

通过逃逸分析,虚拟机能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上


逃逸分析对的行为就是分析对象动态作用域
   当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
  当一个对象在方法中被定义,但被外部方法引用了,则发生了逃逸

没有发生逃逸的对象就保存在栈中
Jdk1.7后默认开启了逃逸分析


得出结论,能在设置局部变量的,尽量设置局部变量,

代码优化

 1.栈上分配,也就是在栈上分配了内存,对象没有发生逃逸

 2.同步省略
      在动态变异同步快的时候,jit编译器可以借助逃逸分析来判断同步快所使用的锁对象是否只能够
      被一个线程所访  问而没有被发布到其他线程,如果没有,
      那么jit编译器在编译这个同步快的时候就会取消对这部分代码的同步,这个过程就叫做同步省略或者锁消除



3.标量替换和分离对象
    有些对象可能不需要一个连续的内存结构存在也可以被访问的到,
    那么部分对象或者全部对象就不需要存储在内存中,而是存储在cpu的寄存器中
 
    标量:是指一个无法再分解成更小的数据结构,java原始数据类型就是标量
        相对的,那些还可以分解的数据叫聚合量,java中的对象就是聚合量,
        因为他还可以分解成其他的聚合量和标量
      在jit阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过jit优化,
      就会把这个对象拆解成若干个其中包含的若干个成员变量来替代,这个过程就叫标量替换



如在一个方法里。
 Public void test(){
 User user=new User(“e”,454);
}

经过标量替换后,就变成了
 Public void test(){
 String x=”e”;
 Int y=454;
}
经过标量替换后,就可以存储到栈空间去了


标量替换参数设置
  -XX:+EliminateAlloctions:开启,默认也是开启
  允许将对象打散分配到栈上


标量替换参数设置
  -XX:+EliminateAlloctions:开启,默认也是开启
  允许将对象打散分配到栈上

其实对象实例就是存储在堆内存中的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值