`

摘抄: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()"来"建议"执行垃圾收集器程序,但是这个垃圾收集程序什么时候被执行以及是否被执行了,都是不不能控制的。但是虽然垃圾收集器是低优先级的线程,却在系统内存可用量过低时,它仍然可能会突发地执行来挽救系统。

分享到:
评论

相关推荐

    JVM面试资料:JVM结构、JVM调优、四大垃圾回收算法、七大垃圾回收器

    JVM结构:类加载器,执行引擎,本地方法接口,本地内存结构; 四大垃圾回收算法:复制算法、标记-清除算法、标记-整理算法、分代收集算法 七大垃圾回收器:Serial、Serial Old、ParNew、CMS、Parallel、Parallel Old...

    揭秘Java虚拟机-JVM设计原理与实现

    5. **内存管理**:JVM自动进行垃圾收集,管理堆内存。它采用分代收集算法,包括新生代、老年代和永久代,以及各种GC策略如Stop-the-world、并发标记等。 6. **异常处理**:JVM支持异常处理框架,通过异常表来确定...

    java.net.BindException: Address already in use: JVM_Bind :8088(端口冲突)

    在myeclipse中将html文件改成jsp文件时myeclipse卡住;将之前的任务关掉;再打开时多次部署项目的时候报错

    JVM入门实战/arthas实战/垃圾回收算法/垃圾回收器/jvm内存模型分析

    第一节:学习JVM的意义和目标 1.1 意义: 1.2 目标: 第二节:JVM内存模型 1.1 概念 1.2 JVM内存模型 1.3 Heap堆内存模型 第三节:定位垃圾对象的依据 1.1 引用计数法 1.2 可达性算法 第四节:垃圾回收算法 ...

    java技术面试必问:JVM-内存模型讲解.docx

    本文将详细介绍Java技术面试中必问的JVM内存模型讲解。Java程序的执行过程包括java文件编译、类加载、字节码执行等步骤。在整个加载过程中,JVM使用一段空间来存储程序执行期间需要的数据和相关信息,这个空间就叫做...

    JVM内存溢出问题解析

    JVM 内存区域组成包括栈内存和堆内存。栈内存用于存放基本类型变量和对象的引用变量,而堆内存用于存放由 new 创建的对象和数组。堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,但缺点是要在运行时...

    jvm内存监控工具使用

    通过 jstat,你可以监控 JVM 各个区域的内存使用率,包括年轻代、老年代和永久代,这对于分析内存泄漏和优化 GC 参数至关重要。 #### jmap:内存映射和对象转储 jmap 工具用于打印 Java 进程的堆内存布局,包括...

    idea插件JVM内存工具JProfiler11

    首先,JProfiler11是一款专业级别的Java性能分析工具,它能够对JVM进行深入的内存和CPU分析。通过集成到Idea中,开发者可以直接在IDE内部进行性能检测,无需离开熟悉的开发环境。只需下载JProfiler11的安装包,并...

    新手必须学习资料:JVM详解

    JVM 的内存管理机制包括内存分配和垃圾回收两个方面。 * 内存分配:JVM 会根据不同的对象分配不同的内存空间,包括堆和栈两种方式。 * 垃圾回收:JVM 的垃圾回收机制负责回收垃圾对象的内存空间,避免内存泄露。 ...

    JVM内存空间分配笔记

    ### JVM内存空间分配详解 #### 一、JVM内存模型概览 JVM(Java虚拟机)内存模型主要由以下几个部分组成:程序计数器、Java虚拟机栈、本地方法栈、Java堆以及方法区(在JDK 8之后称为元空间)。下面将对这几个部分...

    Java虚拟机:JVM高级特性与最佳实践(第二版)

    - **类装载器**:JVM通过类装载器将类文件加载到内存中,分为启动类装载器、扩展类装载器和应用程序类装载器,以及用户自定义的类装载器。 - **运行数据区**:包括方法区、堆、虚拟机栈、本地方法栈和程序计数器。...

    深入详解JVM内存模型与JVM参数详细配置.pdf

    本资源详细介绍了JVM内存模型的结构和组成部分,包括堆内存、方法区、栈内存、程序计数器等。同时,还详细介绍了JVM参数的配置和调整方法,以提高JVM的性能和效率。 knowledge point 1: JVM内存模型结构 JVM内存...

    推荐一些JVM原理,JVM调优,JVM内存模型,JAVA并发 电子书1

    Java内存模型(JMM)规定了线程对共享变量的可见性和有序性,它通过主内存和工作内存的概念来实现多线程之间的协作。 3. JVM调优:JVM调优通常指对JVM进行配置,优化性能以应对特定的应用需求。常见的调优手段包括...

    JVM内存配置优化

    可以通过调整JVM参数`-Xms`和`-Xmx`来增加堆内存的初始值和最大值。例如,设置`-Xms2048m -Xmx2048m`可以将堆内存的最小和最大值都设置为2GB。 - **JVM默认堆内存配置**:JVM默认情况下,最小堆内存为物理内存的1...

    [] - 2022-08-12 图文并茂:JVM内存布局详解.pdf

    互联网资讯,技术简介,IT、AI技术,人工智能

    JVM内存管理白皮书

    在这份由Sun Microsystems公司出版的《JVM内存管理白皮书》中,我们可以找到关于Java虚拟机(JVM)内存管理的详细介绍和深入分析。这份文档对于想要深入了解JVM工作原理的读者来说是一份宝贵的学习资料。在这份...

    jvm 内存分析文档

    理解JVM内存结构和内存分配机制对于避免内存溢出(OutOfMemoryError)、提升程序性能、减少垃圾回收开销至关重要。开发者应关注内存配置、对象生命周期管理以及适当的垃圾回收策略,以优化应用程序的性能和稳定性。

    JVM内存参数详解以及配置调优

    内存管理是 JVM 中最重要的组件之一,它负责为对象分配内存和释放内存。垃圾回收(Garbage Collector,GC)是内存管理的主要组件,它负责回收堆中不再使用的对象,以释放内存。垃圾回收有两种方式:Minor 收集和 ...

    java获得jvm内存大小

    在启动Java应用程序时,可以通过命令行参数来设置JVM的初始堆内存和最大堆内存。常用的参数有: - `-Xms&lt;size&gt;`:设置初始堆内存大小。例如,`-Xms256M`表示初始堆内存为256MB。 - `-Xmx&lt;size&gt;`:设置最大堆内存...

Global site tag (gtag.js) - Google Analytics