在公司有一个需求是要核对一批数据,以前的作法是直接用SQL各类复杂操做给怼出来的,不只时间慢,并且后期也很差维护,就算原做者来了过一个月估计也忘了SQL什么意思了,因而有一次我就想着问一下以前作这个需求的人为何不将这些数据查出来后在内存里面作筛选呢?直接说了你不怕把内存给撑爆吗?此核算服务器是单独的服务器,配置是四核八G的,配置堆的大小是4G。本着怀疑的精神,就想要弄清楚几百万条数据真的放入内存的话会占用多少内存呢?java
计算机的存储单位
计算机的存储单位经常使用的有bit、Byte、KB、MB、GB、TB后面还有可是咱们基本上用不上就不说了,咱们常常将bit称之为比特或者位、将Byte简称为B或者字节,将KB简称为K,将MB称之为M或者兆,将GB简称为G。那么他们的换算单位是怎样的呢?数据库
换算关系
首先咱们得知道在计算机中全部数据都是由0 1来组成的,那么存储0 1这些二进制数据是由什么存放呢?就是由bit存放的,一个bit存放一位二进制数字。因此bit是计算机最小的单位。数组
大部分计算机目前都是使用8位的块,就是咱们上面称之为的字节Byte,来做为计算机容量的基本单位。因此咱们通常称一个字符或者一个数字都是称之为占用了多少字节。服务器
了解了上面关于位和字节的关系后,咱们能够看一下其余的单位换算关系ide
11B(Byte 字节) = 8bit(位) 21KB = 1024B 31MB = 1024KB 41GB = 1024MB 51TB = 1024GB
Java中对象占用多少内存
在了解了上面的换算关系后,咱们来了解一下新建一个Java对象须要多少内存。函数
Java基本类型
咱们知道Java类型分为基本类型和引用类型,八大基本类型有int、short、long、byte、float、double、boolean、char工具
至于为何Java中的char不管是中英文数字都占用两个字节,是由于Java中使用Unicode字符,全部的字符均以两个字节存储。布局
Java引用类型
在一个对象中除了有基本数据类型之外,咱们也会有一些引用类型,引用类型的对象比较特殊,由于这些对象真正存储在虚拟机中的堆内存中,对象中只是存储了一个引用而已,若是是引用类型那么就会存储一个指向该引用的指针。指针默认状况下是占用4字节,是由于开启了指针压缩,若是没有开的话,那么一个引用就占用8个字节。线程
对象在内存中的布局
在HotSpot虚拟机中,对象在内存中存储的布局能够分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。3d
对象头
在对象头中存储了两部分数据
运行时数据:存储了对象自身运行时的数据,例如哈希码、GC分代的年龄、锁状态标志、线程持有的锁、偏向线程ID等等。这部分数据在32位和64位的虚拟机中分别为32bit和64bit
类型指针:对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。若是对象是一个Java数组的话,那么对象头中还必须有一块用于记录数组长度的数据(占用4个字节)。因此这是一个指针,默认JVM对指针进行了压缩,用4个字节存储。
咱们以虚拟机为64位的机器为例,那么对象头占用的内存是8(运行时数据)+4(类型指针)=12Byte。若是是数组的话那么就是16Byte
实例数据
实例数据中也拥有两部分数据,一部分是基本类型数据,一部分是引用指针。这两部分数据咱们在上面已经讲了。具体占用多少内存咱们须要结合具体的对象继续分析,下面咱们会有具体的分析。
从父类中继承下来的变量也是须要进行计算的
对齐填充
对齐填充并非必然存在的,也没有特别的含义。它仅仅起着占位符的做用。因为HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而若是对象头加上实例数据不是8的整数倍的话那么就会经过对其填充进行补全。
实战演练
咱们在上面分析一大堆,那么是否是就如咱们分析的同样,新建一个对象在内存中的分配大小就是如此呢?咱们能够新建一个对象。
lass Animal{ private int age; }
那么怎么知道这个对象在内存中占用多少内存呢?JDK提供了一个工具jol-core能够给咱们分析出来一个对象在内存中占用的内存大小。直接在项目中引入包便可。
--Gradle compile 'org.openjdk.jol:jol-core:0.9' --Maven <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
而后咱们在main函数中调用以下
public class AboutObjectMemory { public static void main(String[] args) { System.out.print(ClassLayout.parseClass(Animal.class).toPrintable()); } }
就能够查看到输出的内容了,能够看到输出结果占用的内存是16字节,和咱们分析的同样。
aboutjava.other.Animal object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Animal.age N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
String占用多少内存
String字符串在Java中是个特殊的存在,好比一个字符串"abcdefg"这样一个字符串占用多少字节呢?相信会有人回答说是7个字节或者是14个字节,这两个答案都是不许确的,咱们先看一下String类在内存中占用的内存是多少。
咱们先本身进行分析一下。在String类中有两个属性,其中对象头固定了是12字节,int是4字节,char[]数组其实在这里至关于引用对象存的,因此存的是地址,所以占用4个字节,因此大小为对象头12Byte+实例数据8Byte+填充数据4Byte=24Byte这里的对象头和实例数据加起来不是8的倍数,因此须要填充数据进行填充。
private final char value[]; private int hash; // Default to 0
那么咱们分析的到底对不对呢,咱们仍是用上面的工具进行分析一下。能够看到咱们算出的结果和咱们分析的结果是一致的。
java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 char[] String.value N/A 16 4 int String.hash N/A 20 4 (loss due to the next object alignment) Instance size: 24 bytes
那么一个空字符串占用多少内存呢?咱们刚才获得的是一个String对象占用了24字节,其实char[]数组仍是会占用内存的,咱们在上面讲对象头的时候说过,数组对象也是一个实例对象,它的对象头比通常的对象多出来4字节,用来描述此数组的长度,因此char[]数组的对象头长度为16字节,因为此时是空字符串,因此实例数据长度为0。所以一个空char[]数组占用内存大小为对象头16Byte+实例数据0Byte=16Byte。一个空字符串占用内存为String对象+char[]数组对象=40Byte
那么咱们上面举的例子abcdefg占用多少内存呢?其中String对象占用的内存是不会变了,变化的是char[]数组中的内容,这里咱们须要知道字符串是存放于char[]数组中的,而一个char占用2个字节,因此abcdefg的char[]数组大小为对象头16Byte+实例数据14Byte+对齐填充2Byte=32Byte。那么abcdefg占用内存大小就是String对象+char[]数组对象=56Byte
用List存储对象
那么咱们在内存中放入二千万个这个对象的话,须要占用多少内存呢?根据上面的知识咱们能大概估算一下。咱们定义一个List数组用于存放此对象,不让其回收。
List<Animal> animals = new ArrayList<>(20000000); for (int i = 0; i < 20000000; i++) { Animal animal = new Animal(); animals.add(animal); }
注意这里我是直接将集合的大小初始化为了二千万的大小,因此程序在正常启动的时候占用内存是100+MB,正常程序启动仅仅占用30+MB的,因此多出来的60+MB正好是咱们初始化的数组的大小。至于为何要初始化大小的缘由就是为了消除集合在扩容时对咱们观察结果的影响
这里我贴一张,集合未初始化大小和初始化大小内存占用对比图,你们能够看到是有内存上的差别,在ArrayList数组中用于存放数据的是transient Object[] elementData;Object数组,因此它里面存放的是指向对象的指针,一个指针占用4个字节,因此就有两千万个指针,那么就是76M。咱们能够看到差别图和咱们预想的同样。
上面咱们已经算出来了一个Animal对象占用16个字节,因此两千万个占用大概是305MB,和集合加起来就是将近380MB的空间大小,接下来咱们就启动程序来看一下咱们结果是否是对的呢,接下来我用的jconsole工具查看内存占用状况。
咱们能够看到和咱们预算的结果是相吻合的。
那么之后若是有大量的对象须要从数据库中查找出来放入内存的话,那么若是是使用对象来接的话,那么咱们就应该尽可能减小对象中的字段,由于即便你不赋值,其实他也是占用着内存的,咱们接下来再举个例子看一下对个属性值的话占用内存是否是又高了。咱们将Animal对象改造以下
class Animal{ private int age; private int age1; private int age2; private int age3; private int age4; }
此时咱们可以计算获得一个Animal对象占用的内存大小是(对象头12Byte+实例数据20Byte=32Byte)此时32因为是8的倍数因此无需进行填充补齐。那么此时若是仍是二千万条数据的话,此对象占用内存应该是610MB,加上刚才集合中指针的数据76MB,那么加起来将近占用686MB,那么预期结果是否和咱们的同样呢,咱们从新启动程序观察,能够看到下图。能够看到和咱们分析的数据是差很少的。
用Map存储对象
用Map存储对象计算内存大小有些麻烦了,众所周知Map的结构是以下图所示。
它是一个数组加链表(或者红黑树)的结构,而数组中存放的数据是Node对象。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
咱们举例定义下面一个Map对象
Map<Animal,Animal> map
此时咱们能够本身计算一下一个Node对象须要的内存大小对象头12Byte+实例数据16Byte+对其填充4Byte=32Byte,固然这里的key和value的值还须要另算,由于Node对象此时存放的仅仅是他们的引用而已。一个Animal对象所占用内存大小咱们上面也说了是16Byte,因此这里一个Node对象占用的大小为32Byte+16Byte+16Byte=64Byte。
下面咱们用实际例子来验证下咱们的猜测
Map<Animal,Animal> map = new HashMap<>(20000000); for (int i = 0; i < 20000000; i++) { map.put(new Animal(),new Animal()); }
上面的例子在一个Map对象中存放二千万条数据,计算大概在内存中占用多少内存。
数组占用内存大小:咱们先来计算一下数组占了多少,这里有个小知识点,在HashMap中初始化大小是按照2的倍数来的,好比你定义了大小为60,那么系统会给你初始化大小为64。因此咱们定义为二千万,系统实际上是会给咱们初始化为33554432,因此此时仅仅HashMap中数组就占用了将近132MB
数据占用内存大小:咱们上面计算了一个Node节点占用了64Byte,那么两千万条数据就占用了1280MB
两个占用内存大小相加咱们能够知道大概系统中占用了1.4G内存的大小。那么事实是不是咱们想象的呢?咱们运行程序能够看到内存大小如图所示。能够看到结果确实和咱们猜测的同样。
总结回归到上面所说的需求,几百万数据放到内存中会把内存撑爆吗?这时候你能够经过本身的计算获得。最终咱们那个需求通过我算出来其实占用内存量几百兆,对于4个G的堆内存来讲其实远远还没达到撑爆的地步。因此有时候咱们对任何东西都要存在怀疑的态度。你们能够到GitHub中下载代码本身在本地跑一下监测一下,而且能够本身定义几个对象而后计算看是否是和图中的内存大小一致。这样才能记忆更深入。送给你们一句话历来如此,便对吗?。其实我写的文章里面也留了一个小坑,你们能够试着找找,是在对集合进行初始化计算那一块。