摘抄:jvm的内存介绍和举例

4.2 容易被搞晕的--堆和栈

由于"堆"和"栈"这两个概念是看不见摸不着的东西,让很多程序员都整不明白是怎么回事,其实这两个概念也没有什么好研究的,因为堆和栈程序员根本没有办法控制其具体内容。

我们只需要了解一点,栈与堆都是Java用来在内存中存放数据的地方就行了。然后再弄清楚这两个概念分别对应这程序开发的什么操作,以及堆和栈的区别即可。

4.2.1 堆--用new建立,垃圾自动回收负责回收

1、堆是一个"运行时"数据区,类实例化的对象就是从堆上去分配空间的;

2、在堆上分配空间是通过"new"等指令建立的;

3、Java针对堆的操作和C++的区别就是,Java不需要在空间不用的时候来显式的释放;

4、Java的堆是由Java的垃圾回收机制来负责处理的,堆是动态分配内存大小,垃圾收集器可以自动回收不再使用的内存空间。

5、但缺点是,因为在运行时动态分配内存,所以内存的存取速度较慢。

例如:

String str = new String("abc");

就是在堆上开辟的空间来存放String的对象。

4.2.2 栈--存放基本数据类型,速度快

1、栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄;

2、栈的存取速度比堆要快;

3、栈数据可以共享;

4、栈的数据大小与生存期必须是确定的,缺乏灵活性。

例如:

int a = 3;


就是在堆上开辟的空间来存放String的对象。

4.2.3 何谓栈的"数据共享"

栈其中一个特性就是"数据共享",那么什么是"数据共享"呢?

我们这里面所说的数据共享,并不是由程序员来控制的,而是JVM来控制的,指的是是系统自动处理的方式。

比如定义两个变量:

int a = 5;int b = 5;

这两个变量所指向的栈上的空间地址是同一个,这就是所谓的"数据共享"。

它的工作方式是这样的:

JVM处理int a = 5,首先在栈上创建一个变量为a的引用,然后去查找栈上是否还有5这个值,如果没有找到,那么就将5存放进来,然后将a指向5。

接着处理int b = 5,在创建完b的引用后,因为在栈中已经有5这个值,便将b直接指向5。

于是,就出现了a与b同时指向5的内存地址的情况。

4.2.4 实例化对象的两种方法

对于String这个类来说它可以用两种方法进行建立:

String s = new String("asdf");



String s = "asdf";

用这两个形式创建的对象是不同的,第一种是用new()来创建对象的,它是在堆上开辟空间,每调用一次都会在堆上创建一个新的对象。

而第二种的创建方法则是先在栈上创建一个String类的对象引用,然后再去查找栈中有没有存放"asdf",如果没有,则将"asdf"存放进栈,并让str指向"asdf",如果已经有"asdf" 则直接把str指向"abc"。

我们在比较两个String是否相等时,一定是用"equals()"方法,而当测试两个包装类的引用是否指向同一个对象时,我们应该用"= ="。

因此,我们可以通过"= ="判断是否相等来验证栈上面的数据共享的问题。

例1:

String s1 = "asdf"; String s2 = "asdf"; System.out.println(s1==s2);

该程序的运行结果是,"true",那么这说明"s1"和"s2"都是指向同一个对象的。

例2:

String s1 =new String ("asdf"); String s2 =new String ("asdf"); System.out.println(s1==s2);

该程序的运行结果是,"false",这说明用new的方式是生成的对象,每个对象都指向不同的地方。

4.3 内存控制心中有数

如果想对内存控制做到十拿九稳,就必须要做到"明明白白"以及"心中有数"。

4.3.1 两个读取内存信息函数

其实,Java给我们提供了读取内存信息的函数,这两个函数分别是:

1、Runtime.getRuntime().maxMemory()

得到虚拟机可以控制的最大内存数量。

2、Runtime.getRuntime().totalMemory()

得到虚拟机当前已经使用的内存数量。


-Xms<size> set initial Java heap size设置JVM初始化堆内存大小
-Xmx<size> set maximum Java heap size设置JVM最大的堆内存大小
-Xss<size> set java thread stack size设置JVM栈内存大小

4.4 内存控制效率优化的启示

内存控制效率优化说起来简单做起来难,真正能做到优化,必须从点滴做起,并利用有效的手段加以应用,现在就看看对于控制内存方面都有哪些启示。

4.4.1 启示1:String和StringBuffer的不同之处

相信大家都知道String和StringBuffer之间是有区别的,但究竟它们之间到底区别在哪里?我们就再本小节中一探究竟,看看能给我们些什么启示。还是刚才那个程序,我们把它改一改,将本程序中的String进行无限次的累加,看看什么时候抛出内存超限的异常,程序如下所示:

public class MemoryTest{public static void main(String args[]){String s="abcdefghijklmnop";System.out.print("当前虚拟机最大可用内存为:");System.out.println(Runtime.getRuntime().maxMemory()/1024/1024+"M");System.out.print("循环前,虚拟机已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/1024/1024+"M");int count = 0;while(true){try{s+=s;count++;}catch(Error o){System.out.println("循环次数:"+count);System.out.println("String实际字节数:"+s.length()/1024/1024+"M");System.out.print("循环后,已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/1024/1024+"M");System.out.println("Catch到的错误:"+o);break;}}}}
程序运行后,果然不一会儿的功夫就报出了异常,

我们注意到,在String的实际字节数只有8M的情况下,循环后已占内存数竟然已经达到了63.56M。这说明,String这个对象的实际占用内存数量与其自身的字节数不相符。于是,在循环19次的时候就已经报"OutOfMemoryError"的错误了。

因此,应该少用String这东西,特别是 String的"+="操作,不仅原来的String对象不能继续使用,而且又要产生多个新对象,因此会较高的占用内存。

所以必须要改用StringBuffer来实现相应目的,下面是改用StringBuffer来做一下测试:

public class MemoryTest{public static void main(String args[]){StringBuffer s=new StringBuffer("abcdefghijklmnop");System.out.print("当前虚拟机最大可用内存为:");System.out.println(Runtime.getRuntime().maxMemory()/1024/1024+"M");System.out.print("循环前,虚拟机已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/1024/1024+"M");int count = 0;while(true){try{s.append(s);count++;}catch(Error o){System.out.println("循环次数:"+count);System.out.println("String实际字节数:"+s.length()/1024/1024+"M");System.out.println("循环后,已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/1024/1024+"M");System.out.println("Catch到的错误:"+o);break;}}}}
我们将String改为StringBuffer以后,在运行时得到了如下结果,
这次我们发现,当StringBuffer所占用的实际字节数为"16M"的时候才产生溢出,整整比上一个程序的String实际字节数"8M"多了一倍。


4.4.2 启示2:用"-Xmx"参数来提高内存可控制量

前面我们介绍过"-Xmx"这个参数的用法,如果我们还是处理刚才的那个用StringBuffer的Java程序,我们用"-Xmx1024m"来启动它,看看它的循环次数有什么变化。

输入如下指令:

java -mx1024m MemoryTest


4.4.3 启示3:二维数组比一维数组占用更多内存空间

对于内存占用的问题还有一个地方值得我们注意,就是二维数组的内存占用问题。

有时候我们一厢情愿的认为:

二维数组的占用内存空间多无非就是二维数组的实际数组元素数比一维数组多而已,那么二维数组的所占空间,一定是实际申请的元素数而已。

但是,事实上并不是这样的,对于一个二维数组而言,它所占用的内存空间要远远大于它开辟的数组元素数。

4.4.4 启示4:用HashMap提高内存查询速度

田富鹏主编的《大学计算机应用基础》中是这样描述内存的:

……

DRAM:即内存条。常说的内存并不是内部存储器,而是DRAM。

……CPU的运行速度很快,而外部存储器的读取速度相对来说就很慢,如果CPU需要用到的数据总是从外部存储器中读取,由于外部设备很慢,……,CPU可能用到的数据预先读到DRAM中,CPU产生的临时数据也暂时存放在DRAM中,这样的结果是大大的提高了CPU的利用率和计算机运行速度。

……

这是一个典型计算机基础教材针对内存的描述,也许作为计算机专业的程序员对这段描述并不陌生。但也因为这段描述,而对内存的处理速度有神话的理解,认为内存中的处理速度是非常快的。

以使持有这种观点的程序员遇到一个巨型的内存查询循环的较长时间时,而束手无策了。

请看一下如下程序:

public class MemFor{public static void main (String[] args) {long start=System.currentTimeMillis(); //取得当前时间int len=1024*1024*3; //设定循环次数int [][] abc=new int[len][2];for (int i=0;i<len;i++){abc[i][0]=i;abc[i][1]=(i+1);}long get=System.currentTimeMillis(); //取得当前时间//循环将想要的数值取出来,本程序取数组的最后一个值for (int i=0;i<len;i++){if ((int)abc[i][0]==(1024*1024*3-1)){System.out.println("取值结果:"+abc[i][1]);}}long end=System.currentTimeMillis(); //取得当前时间//输出测试结果System.out.println("赋值循环时间:"+(get-start)+"ms");System.out.println("获取循环时间:"+(end-get)+"ms");System.out.print("Java可控内存:");System.out.println(Runtime.getRuntime().maxMemory()/1024/1024+"M");System.out.print("已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/1024/1024+"M");}}
运行这个程序: java -Xmx1024m MemFor


程序的运行结果如下:

取值结果:3145728

赋值循环时间:2464ms

获取循环时间:70ms

Java可控内存:1016M

已占用内存:128M

我们发现,这个程序循环了3145728次获得想要的结果,循环获取数值的时间用了70毫秒。

你觉得快吗?

是啊,70毫秒虽然小于1秒钟,但是如果你不得不在这个循环外面再套一个循环,即使外层嵌套的循环只有100次,那么,想想看是多少毫秒呢?

回答:70毫秒*100=7000毫秒=7秒

如果,循环1000次呢?

70秒!

70秒的运行时间对于这个程序来说就是灾难了。

面对这个程序的运行时间很多程序员已经束手无策了,其实,Java给程序员们提供了一个较快的查询方法--哈希表查询。

我们将这个程序用"HashMap"来改造一下,再看看运行结果:

import java.util.*;public class HashMapTest{public static void main (String[] args) {HashMap has=new HashMap();int len=1024*1024*3;long start=System.currentTimeMillis();for (int i=0;i<len;i++){has.put(""+i,""+i);}long end=System.currentTimeMillis();System.out.println("取值结果:"+has.get(""+(1024*1024*3-1)));long end2=System.currentTimeMillis();System.out.println("赋值循环时间:"+(end-start)+"ms");System.out.println("获取循环时间:"+(end2-end)+"ms");System.out.print("Java可控内存:");System.out.println(Runtime.getRuntime().maxMemory()/1024/1024+"M");System.out.print("已占用内存:");System.out.println(Runtime.getRuntime().totalMemory()/1024/1024+"M");}}
运行这个程序:
java -Xmx1024m HashMapTest

程序的运行结果如下:

取之结果:3145727

赋值循环时间:16454ms

获取循环时间:0ms

Java可控内存:1016M

已占用内存:566M

那么现在用HashMap来取值的时间竟然不到1ms,这时我们的程序的效率明显提高了,看来用哈希表进行内存中的数据搜索速度确实很快。

在提高数据搜索速度的同时也要注意到,赋值时间的差异和内存占用的差异。

赋值循环时间:

HashMap:16454ms

普通数组:2464ms

占用内存:

HashMap:566M

普通数组:128M

因此,可以看出HashMap在初始化以及内存占用方面都要高于普通数组,如果仅仅是为了数据存储,用普通数组是比较适合的,但是,如果为了频繁查询的目的,HashMap是必然的选择。

4.5 内存垃圾回收问题

那本谭浩强主编的Java入门教材说:

……

1、简单性

设计Java语言的出发点就是容易编程,不需要深奥的知识。Java语言的风格十分接近C++语言,但要比C++简单得多。Java舍弃了一些不常用的、难以理解的、容易混淆的成分,如运算符重载、多继承等。增加了自动垃圾搜集功能,用于回收不再使用的内存区域。这不但使程序易于编写,而且大大减少了由于内存分配而引发的问题。

……

这样类似的描述出现在众多的Java入门级教材中,非常容易让人们忽略了内存垃圾回收的问题,其实Java的垃圾回收问还是需要关注一下的。这个问题在招聘单位的笔试题中出现的频率也比较高,我们需要好好的研究一下Java的垃圾回收机制。

4.5.1 什么是内存垃圾,哪些内存符合垃圾的标准

我们在前面讲过了,堆是一个"运行时"数据区,是通过"new"等指令建立的,Java的堆是由Java的垃圾回收机制来负责处理的,堆是动态分配内存大小,垃圾收集器可以自动回收不再使用的内存空间。

也就是说,所谓的"内存垃圾"是指在堆上开辟的内存空间在不用的时候就变成了"垃圾"。

C++或其他程序设计语言中,必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至死机。但手工回收内存往往是一项复杂而艰巨的工作。因为要预先确定占用的内存空间是否应该被回收是非常困难的!如果一段程序不能回收内存空间,而且在程序运行时系统中又没有了可以分配的内存空间时,这段程序就只能崩溃。

Java和C++相比的优势在于,这部分"垃圾"可以被Java 虚拟机(JVM)中的一个程序发现并自动清除掉,而不用程序员自己想着"delete"了。

Java语言提供了一个系统级的线程,即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当JVM处于空闲循环时,自动回收每一块可以回收的内存。

4.5.1.1 垃圾回收工作机制

垃圾收集器线程它是一种低优先级的线程,它必须在一个Java程序的运行过程中出现内存空闲的时候才去进行回收处理。

垃圾收集器系统有其判断内存块是否需要回收的判断标准的。垃圾收集器完全是自动被执行的,它不能被强制执行,即使程序员能明确地判断出某一块内存应该被回收了,也不能强制执行垃圾回收程序进行垃圾回收。

程序员可以做的只有调用"System.gc()"来"建议"执行垃圾收集器程序,但是这个垃圾收集程序什么时候被执行以及是否被执行了,都是不不能控制的。但是虽然垃圾收集器是低优先级的线程,却在系统内存可用量过低时,它仍然可能会突发地执行来挽救系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值