Java 内存分配及容量扩充

一、Java 进程的内存使用

当运行一个Java应用程序时,Java 运行时会创建一个操作系统进程,作为操作系统进程,Java 运行时面临着与其他进程完全相同的内存限制

架构提供的内存寻址能力依赖于处理器的位数,举例来说,32位或者64位进程能够处理的位数决定了处理器能寻址的内存范围:32位提供了 2^32 的可寻址范围,也就是 4,294,967,296 位,或者说 4GB。而 64 位处理器的可寻址范围明显增大:2^64,也就是 18,446,744,073,709,551,616,或者说 16 exabyte(百亿亿字节)。
OS和C运行时占用的内存数量取决于所用的OS,但通常数量较大:Windows 默认占用的内存是2GB。剩余的可寻址空间就是可供运行的实际进程使用的内存。

对于Java应用程序,用户空间是 Java 进程占用的内存,实际上包含两个池:Java 堆和本机(非 Java)堆。Java 堆的大小由 JVM 的Java堆设置控制:-Xms 和 -Xmx 分别设置最小和最大 Java 堆。在按照最大的大小设置分配了Java堆之后,剩下的用户空间就是本机堆。下面展示了一个 32 位 Java 进程的内存布局

一个 32 位 Java 进程的内存布局示例视图

可寻址范围总共有 4GB,OS 和 C 运行时大约占用了其中的 1GB,Java 堆占用了将近 2GB,本机堆占用了其他部分。请注意,JVM 本身也要占用内存,就像 OS 内核和 C 运行时一样,而 JVM 占用的内存是本机堆的子集。

二、Java 对象内存使用情况

Java代码使用new操作符创建一个Java对象的实例时,实际上分配的数据要比您想的多得多。例如,一个int值与一个Integer对象的大小比率是 1:4.额外的开销源于 JVM 用于描述 Java 对象的元数据.其中通常包括:
类:一个指向类信息的指针,描述了对象类型。举例来说,对于 java.lang.Integer 对象,这是 java.lang.Integer 类的一个指针。
标记:一组标记,描述了对象的状态,包括对象的散列码(如果有),以及对象的形状(也就是说,对象是否是数组)。
锁:对象的同步信息,也就是说,对象目前是否正在同步。
对象元数据后紧跟着对象数据本身,包括对象实例中存储的字段。对于 java.lang.Integer 对象,这就是一个 int。
如果您正在运行一个 32 位 JVM,那么在创建 java.lang.Integer 对象实例时,对象的布局可能如图

一个 32 位 Java 进程的 java.lang.Integer 对象的布局示例 

有128位的数据被占用,其中用于存储int值的为32位,而对象元数据占用了其余的96位。

三、Java 数组对象内存使用情况

数组对象(例如一个 int 值数组)的形状和结构与标准 Java 对象相似。主要差别在于数组对象包含说明数组大小的额外元数据。
因此,数据对象的元数据包括:
类:一个指向类信息的指针,描述了对象类型。举例来说,对于 int 字段数组,这是 int[] 类的一个指针。
标记:一组标记,描述了对象的状态,包括对象的散列码(如果有),以及对象的形状(也就是说,对象是否是数组)。
锁:对象的同步信息,也就是说,对象目前是否正在同步。
大小:数组的大小。
int 数组对象的布局示例

一个 32 位进程的 int 数组对象的布局示例 

有160位的数据用于存储int值内的32位数据,而数组元数据占用了其余160位。

四、比较32位和64位Java对象内存使用情况
64 位处理器的内存可寻址能力比 32 位处理器高得多。对于 64 位进程,Java 对象中的某些数据字段的大小(特别是对象元数据)也需要增加到 64 位。其他数据字段类型(例如 int、byte 和 long )的大小不会更改。
一个 64 位进程的 java.lang.Integer 对象和 int 数组的布局示例

一个 64 位 Java 进程的 java.lang.Integer 对象和 int 数组的布局示例 

对于一个 64 位 Integer 对象,现在有 224 位的数据用于存储 int 字段所用的 32 位,开销比例是 7:1。对于一个 64 位单元素 int 数组,有 288 位的数据用于存储 32 位 int 条目,开销比例是 9:1。在 32 位 Java 运行时中运行的应用程序若迁移到 64 位 Java 运行时,其 Java 堆内存使用量会显著增加。通常情况下,增加的数量是原始堆大小的 70% 左右。
,一个在 32 位 Java 运行时中使用 1GB Java 堆的 Java 应用程序在迁移到 64 位 Java 运行时之后,通常需要使用 1.7GB 的 Java 堆。请注意,这种内存增加并非仅限于 Java 堆。本机堆内存区使用量也会增加,有时甚至要增加 90% 之多。

五、Java 集合的内存使用

大量数据都是使用核心 Java API 提供的标准 Java Collections 类来存储和管理的。非常有必要了解各集合提供的功能以及相关的内存开销。总体而言,集合功能的级别越高,内存开销就越高,
其中部分最常用的集合如下:
HashSet
HashMap
Hashtable
LinkedList
ArrayList
(HashSet 是包围一个 HashMap 对象的包装器,它提供的功能比 HashMap 少,同时容量稍微小一些。)

属性汇总,内存开销

集合 性能 默认容量 空时的大小 10K 条目的开销 准确设置大小? 扩展算法
HashSet O(1) 16 144 360K x2
HashMap O(1) 16 128 360K x2
Hashtable O(1) 11 104 360K x2+1
LinkedList O(n) 1 48 240K +1
ArrayList O(n) 10 88 40K x1.5
StringBuffer O(1) 16 72 24 x2
Hash 集合的性能比任何 List 的性能都要高,但每条目的成本也要更高。对于并不那么注重访问性能的较小集合而言,List 则是合理的选择。ArrayList 和 LinkedList 集合的性能大体相同,但其内存占用完全不同:ArrayList 的每条目大小要比 LinkedList 小得多,但它不是准确设置大小的。List 要使用的正确实现是 ArrayList 还是 LinkedList 取决于 List 长度的可预测性。如果长度未知,那么正确的选择可能是 LinkedList,因为集合包含的空白空间更少。如果大小已知,那么 ArrayList 的内存开销会更低一些。

六、ArrayList和HashMap容量增长方式

(1)、ArrayList 容量增长的方式

public boolean add(E e) { 
  ensureCapacity(size + 1); // 容量增长
  elementData[size++] = e; 
  return true; 
  }


public void ensureCapacity(int minCapacity) { 
  modCount++; 
  int ldCapacity = elementData.length; 
  if (minCapacity > oldCapacity) { 
  Object oldData[] = elementData; 
  int newCapacity = (oldCapacity * 3)/2 + 1; 
  if (newCapacity < minCapacity) {
  newCapacity = minCapacity; 
}
    elementData = Arrays.copyOf(elementData, newCapacity); //依次拷贝
  } 
} 


int newCapacity = (oldCapacity * 3)/2 + 1;  也就是原有容量的1.5倍+1。然后通过底层的复制方法将原有数据复制过来
如果数据量很大,那么造成数组重新分配的次数会增加,但对于一般的数据量下,
1千需要分配 11次
1万一级需要分配17次
10万 需要分配23次
100万需要分配28次
所以,大家根据实际情况,大致分配一个初始化的容量还是有必要的。但是如果你初始容量太大,而数据增长很慢,那么就在浪费内存了。
如何取舍,还是看具体的应用场景了。


(2)、HashMap 容量增长方式
HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算,哈系运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。
transient Entry[] table;
Entry 就是HashMap存储数据所用的类,它拥有的属性如下
final K key;
V value;
final int hash; 
Entry<K,V> next; //next就是为了哈希冲突而存在的。


当put一个元素时,如果达到了容量限制,HashMap就会扩容,新的容量永远是原来的2倍。
if (size++ >= threshold){
   resize(2 * table.length);
}
达到 threshold指定的值时就开始扩容, threshold=最大容量*加载因子。
如果数据大小是固定的,那么最好给HashMap设定一个合理的容量值
根据上面的分析,HashMap的初始默认容量是16,默认加载因子是0.75,也就是说,如果采用HashMap的默认构造函数,当增加数据时,数据实际容量超过16*0.75=12时,HashMap就扩容,扩容带来一系列的运算,新建一个是原来容量2倍的数组,对原有元素全部重新哈希,如果你的数据有几千几万个,而用默认的HashMap构造函数,那结果是非常悲剧的,因为HashMap不断扩容,不断哈希,在使用HashMap的场景里,不会是多个线程共享一个HashMap,除非对HashMap包装并同步,由此产生的内存开销和cpu开销在某些情况下可能是致命的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值