再探Java内存分配

简介: 自定义View系列教程00–推翻自己和过往,重学自定义View 自定义View系列教程01–常用工具介绍 自定义View系列教程02–onMeasure源码详尽分析 自定义View系列教程0...

自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理


探索Android软键盘的疑难杂症
深入探讨Android异步精髓Handler
详解Android主流框架不可或缺的基石
站在源码的肩膀上全解Scroller工作机制


Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南


版权声明


引子

这两天有个同事抓耳挠腮地纠结:Java到底是值传递还是引用传递。百思不得其姐,他将这个问题抛给大家一起讨论。于是,有的人说传值,有的人说传引用;不管哪方都觉得自己的理解是正确无误的。我觉得:要回答这个问题不妨先搁置这个问题,先往这个问题的上游走走——Java内存分配。一提到内存分配,我想不少人的脑海里都会浮现一句话:引用放在栈里,对象放在堆里,栈指向堆。嗯哼,这句话听上去没有错;但是我们继续追问一下:这个栈是什么栈?是龙门客栈么?非也!它其实是Java虚拟机栈。呃,到了此处,好学的童鞋忍不住要追问了:啥是Java虚拟机栈呢?不急,我们一起来瞅瞅。


JVM的生命周期

我们知道:每个Java程序都运行于在Java虚拟机上;也就是说:一个运行时的Java虚拟机负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就随之诞生了;当该程序执行完毕后这个虚拟机实例也就随之消亡。例如:在一台计算机上同时运行五个Java程序,那么系统将提供五个Java虚拟机实例;每个Java程序独自运行于它自己所对应的Java虚拟机实例中。

Java虚拟机中有两种线程,即:守护线程与非守护线程。守护线程通常由虚拟机自身使用,比如执行垃圾收集的线程。非守护线程,通常指的是我们自己的线程。当程序中所有的非守护线程都终止时,虚拟机实例将自动退出。


JVM运行时数据区

既然Java虚拟机负责执行Java程序,那我们就先来看看Java虚拟机体系结构,请参见下图:


这里写图片描述

在这里可以看到:class文件由类加载器载入JVM中运行。此处,我们重点关注蓝色线框中JVM的Runtime Data Areas(运行时数据区),它表示JVM在运行期间对内存空间的划分和分配。在该数据区内分为以下几个主要区域:Method Area(方法区),Heap(堆),Java Stacks(Java 栈),Program Counter Register(程序计数器),Native Method Stack(本地方法栈),现对各区域的主要作用及其特点作如下详细介绍。

Method Area(方法区)

Method Area(方法区)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError(OOM)异常。为进一步了解Method Area(方法区),我们来看在该区域内包含了哪些具体组成部分。

(1) 运行时常量池

Class文件中除了有类的版本、字段、方法、接口等描述等与类紧密相关的信息之外,还有一个常量池用于存放编译期生成的各种字面量和符号引用;该常量池将在类加载后被存放到方法区的运行时常量池中。换句话说:在运行时常量池中存放了该类使用的常量的一个有序集合,它在java程序的动态连接中起着非常重要的作用。在该集合中包括直接常量(string,integer和,floating point等)和对其他类型、字段和方法的符号引用。外界可通过索引访问运行时常量池中的数据项,这一点和访问数组非常类似。当然,运行时常量池是方法区的一部分,它也会受到方法区内存的限制,当运行时常量池无法再申请到内存时也会抛出OutOfMemoryError(OOM)异常。

(2) 类型信息

在该部分中包括:

  • 类型的完全限定名
  • 类型的直接超类的全限定名
  • 类型是类类型还是接口类型
  • 类型的访问修饰符(public、abstract、final等)
  • 直接超接口的全限定名的有序列表

(3) 字段信息

字段信息用于描述该类中声明的所有字段(局部变量除外),它包含以下具体信息:

  • 字段名
  • 字段类型
  • 字段的修饰符
  • 字段的顺序

(4) 方法信息

方法信息用于描述该类中声明的所有方法,它包含以下具体信息:

  • 方法名
  • 方法的返回类型
  • 方法输入参数的个数,类型,顺序
  • 方法的修饰符
  • 操作数栈
  • 在帧栈中的局部变量区的大小

(5) 类变量

该部分用于存放类中static修饰的变量。

(6) 指向类加载器的引用

类由类加载器加载,JVM会在方法区保留指向该类加载器的引用。

(7) 指向Class实例的引用

在类被加载器加载的过程中,虚拟机会创建一个代表该类的Class对象,与此同时JVM会在方法区保留指向该Class的引用。

Program Counter Register(程序计数器)

Program Counter Register(程序计数器)在Runtime Data Areas(运行时数据区)只占据非常小的内存空间,它用于存储下一条即将执行的字节码指令的地址。

Java Stacks(Java 栈)

Java Stacks(Java 栈)亦称为虚拟机栈(VM Stack),也就是我们通常说的栈。它用于描述的Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。Java Stacks(Java 栈)的生命周期与线程相同;当一个线程执行完毕那么该栈亦被清空。

Native Method Stack(本地方法栈)

Native Method Stack(本地方法栈)与Java Stacks(Java 栈)非常类似,它用于存储调用本地方法(C/C++)所涉及到的局部变量表、操作栈等信息。

Heap(堆)

Heap(堆)在虚拟机启动时创建,用于存放对象实例,几乎所有的对象实例都在这里分配内存。所以,Heap(堆)是Java 虚拟机所管理的内存中最大的一块,也是垃圾回收器管理的重点区域。

小结

在此对JVM运行时数据区做一个小结:

  • Method Area(方法区)和Heap(堆)是被所有线程共享的内存区域。

  • Java Stacks(Java 栈)和Program Counter Register(程序计数器)以及Native Method Stack(本地方法栈)是各线程私有的内存区域。

  • 创建一个对象,该对象的引用存放于Java Stacks(Java 栈)中,真正的对象实例存放于Heap(堆)中。这也是大家常说的:栈指向堆。

  • 除了刚才提到的JVM运行时数据区所涉及到的内存以外,我们还需要关注直接内存(Direct Memory)。请注意:直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError(OOM)异常出现。比如,在使用NIO时它可以使用Native 函数库直接分配堆外内存,然后通过存储在Java 堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。类似的操作,可避免了在Java 堆和Native 堆中来回复制数据,从而提高性能。


Java调用方法时的参数传递机制

在调用Java方法传递参数的时候,到底是传值还是传引用呢?面对众多的争论,我们还是来瞅瞅代码,毕竟代码不会说谎。我们先来看一个非常简单的例子:交换两个int类型的数据,代码如下:

package cn.com;
/**
 * 原创作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();
        int number1=9527;
        int number2=1314;
        System.out.println("main方法中,数据交换前:number1="+number1+" , number2="+number2);
        testMemory.swapData(number1, number2);
        System.out.println("main方法中,数据交换后:number1="+number1+" , number2="+number2);
    }

    private void swapData(int a,int b) {
        System.out.println("swapData方法中,数据交换前:a="+a+" , b="+b);
        int temp=a;
        a=b;
        b=temp;
        System.out.println("swapData方法中,数据交换后:a="+a+" , b="+b);
    }

}

我们在main方法中声明的两个变量number1=9527 , number2=1314;然后将这两个数作为参数传递给了方法swapData(int a,int b),并在该方法内交换数据。至于代码本身无需再过多的解释了;不过,请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:

main方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换前:a=9527 , b=1314
swapData方法中,数据交换后:a=1314 , b=9527
main方法中,数据交换后:number1=9527 , number2=1314

嗯哼,这和你想的一样么?为什么会是这样呢?还记得刚才讨论Java Stacks(Java 栈)时说的么:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。结合示例的代码:main( )方法在一个栈帧中,swapData( )在另外一个栈帧中;两者彼此独立互不干扰。在main( )中调用swapData( )传入参数时它的本质是:将实际参数值的副本(复制品)传入其它方法内而参数本身不会受到任何影响。也就是说,这number1和number2这两个变量仍然存在于main( )方法所对应的栈帧中,但number1和number2这两个变量的副本(即int a和int b)存在于swapData( )方法所对应的栈帧中。故,在swapData( )中交换数据,对于main( )是没有任何影响的。这就是Java中调用方法时的传值机制——值传递。

嗯哼,刚才这个例子是关于基本类型的参数传递。Java对于引用类型的参数传递一样采用了值传递的方式。我们在刚才的示例中稍加改造。首先,我们创建一个类,该类有两个变量number1和number2,请看代码:

package cn.com;
/**
 * 原创作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class DataObject {

    private int number1;
    private int number2;

    public int getNumber1() {
        return number1;
    }
    public void setNumber1(int number1) {
        this.number1 = number1;
    }
    public int getNumber2() {
        return number2;
    }
    public void setNumber2(int number2) {
        this.number2 = number2;
    }

}

好了,现在我们来测试交换DataObject类对象中的两个数据:

package cn.com;
/**
 * 原创作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();
        DataObject dataObject=new DataObject();
        dataObject.setNumber1(9527);
        dataObject.setNumber2(1314);
        System.out.println("main方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
        testMemory.swapData(dataObject);
        System.out.println("main方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
    }


    private void swapData(DataObject dataObject) {
        System.out.println("swapData方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
        int temp=dataObject.getNumber1();
        dataObject.setNumber1(dataObject.getNumber2());
        dataObject.setNumber2(temp);
        System.out.println("swapData方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());

    }

}

简单地描述一下代码:在main( )中定义一个DataObject类的对象并为其number1和number2赋值;然后调用swapData(DataObject dataObject)方法,在该方法中交换数据。请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:

main方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换后:number1=1314 , number2=9527
main方法中,数据交换后:number1=1314 , number2=9527

嗯哼,为什么是这样呢?我们通过DataObject dataObject=new DataObject();创建一个对象;该对象的引用dataObject存放于栈中,而该对象的真正的实例存放于堆中。在main( )中调用swapData( )方法传入dataObject作为参数时仍然传递的是值,只不过稍微特殊点的是:该值指向了堆中的实例对象。好了,再结合栈帧来梳理一遍:main( )方法存在于与之对应的栈帧中,在该栈帧中有一个变量dataObject它指向了堆内存中的真正的实例对象。swapData( )收到main( )传递过来的变量dataObject时将其存放于其本身对应的栈帧中,但是该变量依然指向堆内存中的真正的实例对象。也就是说:main( )方法中的dataObject和swapData( )方法中的dataObject指向了堆中的同一个实例对象!所以,在swapData( )中交换了数据之后,在main( )会体现交换后的变化。在此,我们可以进一步的验证:在该swapData( )方法的最后一行添加一句代码dataObject=null ;我们发现打印信息并没有任何变化。因为这句代码仅仅使得swapData( )所对应的栈帧中的dataObject不再指向堆内存中的实例对象但不会影响main( )所对应的栈帧中的dataObject依然指向堆内存中的实例对象。

通过这两个示例,我们进一步验证了:Java中调用方法时的传递机制——值传递。当然,有的人说:基础类型传值,对象类型传引用。其实,这也没有什么错,只不过是表述方式不同罢了;只要明白其中的道理就行。如果,有些童鞋非纠缠着个别字眼不放,那我只好说:PHP是世界上最好的语言。


参考资料

相关文章
|
24天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
33 6
|
15天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
1月前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
26天前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
2月前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
231 1
|
17天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
31 8
|
15天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
19天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
46 5
|
17天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
17天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
下一篇
DataWorks