java内存分配和回收策略

简介:

一、导论

  java技术体系中所提到的内存自动化管理归根结底就是内存的分配与回收两个问题,之前已经和大家谈过java回收的相关知识,今天来和大家聊聊java对象的在内存中的分配。通俗的讲,对象的内存分配就是在堆上的分配,对象主要分配在新生代的Eden上(关于对象在内存上的分代在垃圾回收中会补上,想了解的也可以参考《深入理解java虚拟机》),如果启动了本地线程分配缓冲,讲按线程优先在TLAB上分配。少数情况下也是直接在老年代中分配。

二、经典的分配策略

1、对象优先在Eden上分配

  一般情况下对象都是优先分配在Eden上,当Eden没有足够的空间进行分配时,jvm会发起一次Minor GC。如果还是没有足够的空间分配,后面还有另外的措施,下面会提到。

  设置虚拟机的偶记日志参数-XX:+PrintGCDetails,在垃圾回收的时候会打印内存的回收日志,并且在进程退出的时候会输出当前内存各区域的分配情况。下面来看下具体的例子,首先需要设置jvm的参数-Xms20m -Xmx20m -Xmn10m,这三个参数说明java堆大小为20M,且不可扩展,其中10M分配给新生代,剩下的10M分配给老年代。-XX:SurvivorRatio=8是jvm默认的新生代中Eden和Survivor比例,默认为8:1。原因是新生代中的对象98%都会在下一次GC的时候回收掉,所以很适合采用复制算法进行垃圾回收,所以新生代10M的内存中,8M是Eden,1M是Survivor,另外的1M是未使用配合复制算法的内存块,也是Survivor。

 1 public class ReflectTest { 2  3     private static final int _1MB = 1024*1024; 4      5     public static void testAllocation(){ 6         byte[] allocation1 , allocation2 , allocation3 , allocation4; 7         allocation1 = new byte[2 * _1MB]; 8         allocation2 = new byte[2 * _1MB]; 9         allocation3 = new byte[2 * _1MB];10         allocation4 = new byte[6 * _1MB];11     }12     13     public static void main(String[] args) {14         ReflectTest.testAllocation();15     }16     17 }

 输出如下

Heap
 PSYoungGen      total 9216K, used 6651K [0x000000000b520000, 0x000000000bf20000, 0x000000000bf20000)
  eden space 8192K, 81% used [0x000000000b520000,0x000000000bb9ef28,0x000000000bd20000)
  from space 1024K, 0% used [0x000000000be20000,0x000000000be20000,0x000000000bf20000)
  to   space 1024K, 0% used [0x000000000bd20000,0x000000000bd20000,0x000000000be20000)
 PSOldGen        total 10240K, used 6144K [0x000000000ab20000, 0x000000000b520000, 0x000000000b520000)
  object space 10240K, 60% used [0x000000000ab20000,0x000000000b120018,0x000000000b520000)
 PSPermGen       total 21248K, used 2973K [0x0000000005720000, 0x0000000006be0000, 0x000000000ab20000)
  object space 21248K, 13% used [0x0000000005720000,0x0000000005a07498,0x0000000006be0000)

 可以看到eden占用了81%,说明allocation1 , allocation2 , allocation3 都是分配在新生代Eden上。

2、大对象直接分配在老年代上

  大对象是指需要大量连续内存空间去存放的对象,类似于那种很长的字符串和数组。大对象对于虚拟机的内存分布来讲并不是好事,当遇到很多存活仅一轮的大对象jvm更加难处理,写代码的时候应该避免这样的问题。虚拟机中提供了-XX:PretenureSizeThreshold参数,另大于这个值的对象直接分配到老年代,这样做的目的是为了避免在Eden区和Survivor区之间发生大量的内存copy,在之前讲过的垃圾回收算法复制算法有提到过,就不多说了。

public class ReflectTestBig {    private static final int _1MB = 1024*1024;    
    public static void testAllocation(){        byte[] allocation2 , allocation3 , allocation4;
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[6 * _1MB];
    }    
    public static void main(String[] args) {
        ReflectTestBig.testAllocation();
    }
    
}

 输出如下

Heap
 PSYoungGen      total 8960K, used 4597K [0x000000000b510000, 0x000000000bf10000, 0x000000000bf10000)
  eden space 7680K, 59% used [0x000000000b510000,0x000000000b98d458,0x000000000bc90000)
  from space 1280K, 0% used [0x000000000bdd0000,0x000000000bdd0000,0x000000000bf10000)
  to   space 1280K, 0% used [0x000000000bc90000,0x000000000bc90000,0x000000000bdd0000)
 PSOldGen        total 10240K, used 6144K [0x000000000ab10000, 0x000000000b510000, 0x000000000b510000)
  object space 10240K, 60% used [0x000000000ab10000,0x000000000b110018,0x000000000b510000)
 PSPermGen       total 21248K, used 2973K [0x0000000005710000, 0x0000000006bd0000, 0x000000000ab10000)
  object space 21248K, 13% used [0x0000000005710000,0x00000000059f7460,0x0000000006bd0000)

 可以看到allocation4已经超过了设置的-XX:PretenureSizeThreshold=3145728,随意allocation4直接被分配到了老年代,老年代占用率为60%。注意这里设置-XX:PretenureSizeThreshold=3145728不能写成-XX:PretenureSizeThreshold=3m,否则jvm将无法识别。

3、长期存活的对象将进入老年代

  虚拟机既然采用了分带收集的思想来管理内存,那内存回收就必须识别哪些对象应该放在新生代,哪些对象应该放在老年代。为了打到目的,jvm给每个对象定义了一个年龄计数器(Age)。如果对象在Eden出生并且能过第一次Minor GC后仍然存活,并且可以在Survivor存放的话,将被移动到Survivor中,并将对象的年龄设为1。对象每躲过一次Minor GC,年龄就会加1,当他的年龄超过一年的阈值的时候,该对象就会晋升到老年代。这个阈值jvm默认是15,可以通过-XX:MaxTenuringThreshold来设置。

   m = 1024 * 1024  [] a1 =  [1 * m / 4[] a2 =  [7 *[] a3 =  [3 * m];

 输出如下

[GC [DefNew: 7767K->403K(9216K), 0.0062209 secs] 7767K->7571K(19456K), 0.0062482 secs]   
[Times: user=0.00 sys=0.00, real=0.01 secs]   
a3 ok  
Heap  
 def new generation   total 9216K, used 3639K [0x331d0000, 0x33bd0000, 0x33bd0000)  
  eden space 8192K,  39% used [0x331d0000, 0x334f9040, 0x339d0000)  
  from space 1024K,  39% used [0x33ad0000, 0x33b34de8, 0x33bd0000)  
  to   space 1024K,   0% used [0x339d0000, 0x339d0000, 0x33ad0000)  
 tenured generation   total 10240K, used 7168K [0x33bd0000, 0x345d0000, 0x345d0000)  
   the space 10240K,  70% used [0x33bd0000, 0x342d0010, 0x342d0200, 0x345d0000)  
 compacting perm gen  total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000)  
   the space 12288K,   3% used [0x345d0000, 0x3462f548, 0x3462f600, 0x351d0000)  
    ro space 10240K,  55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000)  
    rw space 12288K,  55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)

 可以看到a2已经存活了一次,年龄为1,满足所设置的-XX:MaxTenuringThreshold=1,所以a2进入了老年代,而a3则进入了新生代。

4、动态对象年龄判定

  为了能更好的适应不同程序的内存状态,虚拟机并不总是要求对象的年龄必须达到-XX:MaxTenuringThreshold所设置的值才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年区,无须达到-XX:MaxTenuringThreshold中的设置值。

5、空间分配担保

  在发生Minor GC的时候,虚拟机会检测每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,则直接进行一次FUll GC。如果小于,则查看HandlerPromotionFailyre设置是否允许担保失败,如果允许那就只进行Minor GC,如果不允许则也要改进一次FUll GC。也就是说新生代Eden存不下改对象的时候就会将该对象存放在老年代。

三、常用的jvm参数设置

1、-Xms: 初始堆大小, 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。

2、Xmx: 最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。

3、-Xmn: 年轻代大小(1.4or lator), 此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。
增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

4、-XX:NewSize: 设置年轻代大小(for 1.3/1.4)。

5、-XX:MaxNewSize: 年轻代最大值(for 1.3/1.4)。

6、-XX:PermSize: 设置持久代(perm gen)初始值。

7、-XX:MaxPermSize: 设置持久代最大值。

8、-Xss: 每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

9、-XX:NewRatio: 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代),-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5。Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

10、-XX:SurvivorRatio: Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。

11、-XX:LargePageSizeInBytes: 内存页的大小不可设置过大, 会影响Perm的大小。

12、-XX:+DisableExplicitGC: 关闭System.gc()

13、-XX:MaxTenuringThreshold: 垃圾最大年龄,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率该参数只有在串行GC时才有效。

14、-XX:PretenureSizeThreshold: 对象超过多大是直接在旧生代分配,单位字节 新生代采用Parallel Scavenge GC时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象。

15、-XX:TLABWasteTargetPercent: TLAB占eden区的百分比。

四、补充

Minor GC和FUll GC的区别:

  新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大对数都是逃不过第一轮的GC,所以Minor GC使用很频繁,一般回收速度也比较快。

  老年代GC(FULL GC/Major GC) :指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在ParallelScavenge收集器的收集策略中就有直接进行Major GC的选择过程 )。Major GC的速度一般会比Minor GC慢10倍以上。 












本文转自xmgdc51CTO博客,原文链接:http://blog.51cto.com/12953214/1940468 ,如需转载请自行联系原作者




相关文章
|
7天前
|
存储 Java 编译器
Java内存区域详解
Java内存区域详解
19 0
Java内存区域详解
|
12天前
|
设计模式 监控 Java
设计模式 - 观察者模式(Observer):Java中的战术与策略
【4月更文挑战第7天】观察者模式是构建可维护、可扩展系统的关键,它在Java中通过`Observable`和`Observer`实现对象间一对多的依赖关系,常用于事件处理、数据绑定和同步。该模式支持事件驱动架构、数据同步和实时系统,但需注意避免循环依赖、控制通知粒度,并关注性能和内存泄漏问题。通过明确角色、使用抽象和管理观察者注册,可最大化其效果。
|
13天前
|
缓存 安全 Java
Java并发编程进阶:深入理解Java内存模型
【4月更文挑战第6天】Java内存模型(JMM)是多线程编程的关键,定义了线程间共享变量读写的规则,确保数据一致性和可见性。主要包括原子性、可见性和有序性三大特性。Happens-Before原则规定操作顺序,内存屏障和锁则保障这些原则的实施。理解JMM和相关机制对于编写线程安全、高性能的Java并发程序至关重要。
|
2天前
|
Oracle Java 关系型数据库
Java 开发者必备:JDK 版本详解与选择策略(含安装与验证)
Oracle Java SE 支持路线图显示,JDK 8(LTS)支持至2030年,非LTS版本如9-11每6个月发布且支持有限。JDK 11(LTS)支持至2032年,而JDK 17及以上版本现在提供免费商用许可。LTS版本提供长达8年的支持,每2年发布一次。Oracle JDK与OpenJDK有多个社区和公司构建版本,如Adoptium、Amazon Corretto和Azul Zulu,它们在许可证、商业支持和更新方面有所不同。个人选择JDK时,可考虑稳定性、LTS、第三方兼容性和提供商支持。
6 0
|
3天前
|
存储 缓存 监控
Java内存管理:垃圾回收与内存泄漏
【4月更文挑战第16天】本文探讨了Java的内存管理机制,重点在于垃圾回收和内存泄漏。垃圾回收通过标记-清除过程回收无用对象,Java提供了多种GC类型,如Serial、Parallel、CMS和G1。内存泄漏导致内存无法释放,常见原因包括静态集合、监听器、内部类、未关闭资源和缓存。内存泄漏影响性能,可能导致应用崩溃。避免内存泄漏的策略包括代码审查、使用分析工具、合理设计和及时释放资源。理解这些原理对开发高性能Java应用至关重要。
|
3天前
|
缓存 NoSQL Java
使用Redis进行Java缓存策略设计
【4月更文挑战第16天】在高并发Java应用中,Redis作为缓存中间件提升性能。本文探讨如何使用Redis设计缓存策略。Redis是开源内存数据结构存储系统,支持多种数据结构。Java中常用Redis客户端有Jedis和Lettuce。缓存设计遵循一致性、失效、雪崩、穿透和预热原则。常见缓存模式包括Cache-Aside、Read-Through、Write-Through和Write-Behind。示例展示了使用Jedis实现Cache-Aside模式。优化策略包括分布式锁、缓存预热、随机过期时间、限流和降级,以应对缓存挑战。
|
5天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
6天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
12 4
|
7天前
|
算法 安全 Java
内存分配与回收策略
内存分配与回收策略
14 0
内存分配与回收策略
|
10天前
|
NoSQL 安全 Redis
redis内存限制与淘汰策略
Redis内存管理包括限制和淘汰策略。`maxmemory`配置参数决定内存上限,无设置时64位系统默认不限制,可能导致系统资源耗尽,生产环境建议设定合理值。当内存满时,未设置淘汰策略会导致写入错误。Redis提供8种淘汰策略,如LRU(最近最少使用)和LFU(最不经常使用),以及随机或基于过期时间的删除。需根据数据重要性、访问频率和一致性选择合适策略。
13 0