再探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是世界上最好的语言。


参考资料

相关文章
|
10天前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
84 3
|
2月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
29天前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
191 17
|
5月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
162 0
|
2月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
133 1
|
2月前
|
存储 监控 算法
Java垃圾回收机制(GC)与内存模型
本文主要讲述JVM的内存模型和基本调优机制。
|
4月前
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
127 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
|
2月前
|
边缘计算 算法 Java
Java 绿色计算与性能优化:从内存管理到能耗降低的全方位优化策略与实践技巧
本文探讨了Java绿色计算与性能优化的技术方案和应用实例。文章从JVM调优(包括垃圾回收器选择、内存管理和并发优化)、代码优化(数据结构选择、对象创建和I/O操作优化)等方面提出优化策略,并结合电商平台、社交平台和智能工厂的实际案例,展示了通过Java新特性提升性能、降低能耗的显著效果。最终指出,综合运用这些优化方法不仅能提高系统性能,还能实现绿色计算目标,为企业节省成本并符合环保要求。
88 0
|
4月前
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
173 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化