Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解。可以说,关于JVM的相关知识,基本是每个Java开发者必学的知识点,也是面试的时候必考的知识点。

在JVM的内存结构中,比较常见的两个区域就是堆内存和栈内存(如无特指,本文提到的栈均指的是虚拟机栈),关于堆和栈的区别,很多开发者也是如数家珍,有很多书籍,或者网上的文章大概都是这样介绍的:

1、堆是线程共享的内存区域,栈是线程独享的内存区域。

2、堆中主要存放对象实例,栈中主要存放各种基本数据类型、对象的引用。

但是,作者可以很负责任的告诉大家,以上两个结论均不是完全正确的。

对象内存分配

在《Java虚拟机规范》中,关于堆有这样的描述:

在Java虚拟机中,堆是可供各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。

一个Java对象在堆上分配的时候,主要是在Eden区上,如果启动了TLAB的话会优先在TLAB上分配,少数情况下也可能会直接分配在老年代中,分配规则并不是百分之百固定的,这取决于当前使用的是哪一种垃圾收集器,还有虚拟机中与内存有关的参数的设置。

但是一般情况下是遵循以下原则的:

  • 对象优先在Eden区分配
    • 优先在Eden分配,如果Eden没有足够空间,会触发一次Monitor GC
  • 大对象直接进入老年代
    • 需要大量连续内存空间的Java对象,当对象需要的内存大于-XX:PretenureSizeThreshold参数的值时,对象会直接在老年代分配内存。

但是,虽然虚拟机规范中是有着这样的要求,但是各个虚拟机厂商在实现虚拟机的时候,可能会针对对象的内存分配做一些优化。这其中最典型的就是HotSpot虚拟机中的JIT技术的成熟,使得对象在堆上分配内存并不是一定的。

其实在《深入理解Java虚拟机》中,作者也提出过类似的观点,因为JIT技术的成熟使得"对象在堆上分配内存"就不是那么绝对的了。但是书中并没有展开介绍到底什么是JIT,也没有介绍JIT优化到底做了什么。那么接下来我们就来深入了解一下:

JIT 技术

我们大家都知道,通过 javac 将可以将Java程序源代码编译,转换成 java 字节码,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。这就是传统的JVM的解释器(Interpreter)的功能。很显然,Java编译器经过解释执行,其执行速度必然会比直接执行可执行的二进制字节码慢很多。为了解决这种效率问题,引入了 JIT(Just In Time ,即时编译) 技术。

有了JIT技术之后,Java程序还是通过解释器进行解释执行,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。

热点检测

上面我们说过,要想触发JIT,首先需要识别出热点代码。目前主要的热点代码识别方式是热点探测(Hot Spot Detection),HotSpot虚拟机中采用的主要是基于计数器的热点探测

基于计数器的热点探测(Counter Based Hot Spot Detection)。采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,某个方法超过阀值就认为是热点方法,触发JIT编译。

编译优化

JIT在做了热点检测识别出热点代码后,除了会对其字节码进行缓存,还会对代码做各种优化。这些优化中,比较重要的几个有:逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除等。

而这些优化中的逃逸分析就和本文要介绍的内容有关了。

逃逸分析

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

例如:

sb是一个方法内部变量,上述代码中并没有将他直接返回,这样这个StringBuffer又不会被其他方法所改变,这样它的作用域就只是在方法内部。我们就可以说这个变量并没有逃逸到方法外部。

有了逃逸分析,我们可以判断出一个方法中的变量是否有可能被其他线程所访问或者改变,那么基于这个特性,JIT就可以做一些优化:

  • 同步省略
  • 标量替换
  • 栈上分配

标量替换、栈上分配

我们说,JIT经过逃逸分析之后,如果发现某个对象并没有逃逸到方法体之外的话,就可能对其进行优化,而这一优化最大的结果就是可能改变Java对象都是在堆上分配内存的这一原则。

对象要分配在堆上其实有很多原因,但是有一点比较关键的和本文有关的,那就是因为堆内存在访问上是线程共享的,这样一个线程创建出来的对象,其他线程也能访问到。

那么,试想下,如果我们在某一个方法体内部创建了一个对象,并且对象并没有逃逸到方法外的话,那还有必要一定要把对象分配到堆上吗?

其实就没有必要了,因为这个对象并不会被其他线程所访问到,生命周期也只是在一个方法内部,也就不用大费周折的在堆上分配内存,也减少了内存回收的必要。

那么,有了逃逸分析之后,发现一个对象并没有逃逸到放法外的话,通过什么办法可以进行优化,减少对象在堆上分配可能呢?

这就是栈上分配。在HotSopt中,栈上分配并没有正在的进行实现,而是通过标量替换来实现的。

所以我们重点介绍下,什么是标量替换,如何通过标量替换实现栈上分配。

标量替换

标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

以上代码中,point对象并没有逃逸出alloc方法,并且point对象是可以拆解成标量的。那么,JIT就会不会直接创建Point对象,而是直接使用两个标量int x ,int y来替代Point对象。

可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。

通过标量替换,原本的一个对象,被替换成了多个成员变量。而原本需要在堆上分配的内存,也就不再需要了,完全可以在本地方法栈中完成对成员变量的内存分配。

实验证明

Talk Is Cheap, Show Me The Code

No Data, No BB;

接下来我们就来通过一个实验,来看一下逃逸分析是否可以生效,生效后是否真的会发生栈上分配,而栈上分配又有什么好处呢?

我们来看以下代码:

publicstaticvoidmain(String[] args){

long a1 = System.currentTimeMillis();

for (int i = 0; i < 1000000; i++) {

alloc();

}

// 查看执行时间

long a2 = System.currentTimeMillis();

System.out.println("cost " + (a2 - a1) + " ms");

// 为了方便查看堆内存中对象个数,线程sleep

try {

Thread.sleep(100000);

} catch (InterruptedException e1) {

e1.printStackTrace();

}

}

privatestaticvoidalloc(){

User user = new User();

}

static class User {

}

其实代码内容很简单,就是使用for循环,在代码中创建100万个User对象。

我们在alloc方法中定义了User对象,但是并没有在方法外部引用他。也就是说,这个对象并不会逃逸到alloc外部。经过JIT的逃逸分析之后,就可以对其内存分配进行优化。

我们指定以下JVM参数并运行:

-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 

其中-XX:-DoEscapeAnalysis表示关闭逃逸分析。

在程序打印出 cost XX ms 后,代码运行结束之前,我们使用jmap命令,来查看下当前堆内存中有多少个User对象:

从上面的jmap执行结果中我们可以看到,堆中共创建了100万个StackAllocTest

在关闭逃逸分析的情况下(-XX:-DoEscapeAnalysis),虽然在alloc方法中创建的User对象并没有逃逸到方法外部,但是还是被分配在堆内存中。也就说,如果没有JIT编译器优化,没有逃逸分析技术,正常情况下就应该是这样的。即所有对象都分配到堆内存中。

接下来,我们开启逃逸分析,再来执行下以上代码。

-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 

在程序打印出 cost XX ms 后,代码运行结束之前,我们使用jmap命令,来查看下当前堆内存中有多少个User对象:

从以上打印结果中可以发现,开启了逃逸分析之后(−XX:+DoEscapeAnalysis),在堆内存中只有8万多个StackAllocTestUser对象。也就是说在经过JIT优化之后,堆内存中分配的对象数量,从100万降到了8万。

除了以上通过jmap验证对象个数的方法以外,读者还可以尝试将堆内存调小,然后执行以上代码,根据GC的次数来分析,也能发现,开启了逃逸分析之后,在运行期间,GC次数会明显减少。正是因为很多堆上分配被优化成了栈上分配,所以GC次数有了明显的减少。

逃逸分析并不成熟

前面的例子中,开启逃逸分析之后,对象数目从100万变成了8万,但是并不是0,说明JIT优化并不会完完全全的所有情况都进行优化。

关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

虽然这项技术并不十分成熟,但是他也是即时编译器优化技术中一个十分重要的手段。

总结

正常情况下,对象是要在堆上进行内存分配的,但是随着编译器优化技术的成熟,虽然虚拟机规范是这样要求的,但是具体实现上还是有些差别的。

如HotSpot虚拟机引入了JIT优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。

所以,对象一定在堆上分配内存,这是不对的。

别再说Java对象都是在堆内存上分配空间的了!的更多相关文章

  1. JVM知识(一) 求你了,别再说Java对象都是在堆内存上分配空间的了!

    求你了,别再说Java对象都是在堆内存上分配空间的了! https://baijiahao.baidu.com/s?id=1661296872935371634&wfr=spider& ...

  2. 求你了,别再说Java对象都是在堆内存上分配空间的了!

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解.可以说,关于JVM的相关知识,基本是每个Java开发者 ...

  3. 【性能优化】面试官:Java中的对象都是在堆上分配的吗?

    写在前面 从开始学习Java的时候,我们就接触了这样一种观点:Java中的对象是在堆上创建的,对象的引用是放在栈里的,那这个观点就真的是正确的吗?如果是正确的,那么,面试官为啥会问:"Jav ...

  4. 面试官:Java中对象都存放在堆中吗?你知道逃逸分析?

    面试官:Java虚拟机的内存分为哪几个区域? 我(微笑着):程序计数器.虚拟机栈.本地方法栈.堆.方法区 面试官:对象一般存放在哪个区域? 我:堆. 面试官:对象都存放在堆中吗? 我:是的. 面试官: ...

  5. python中一切皆是对象,对象都是在堆上存放的,一切都是指针

    1 由于对象都是在堆上存放的,所以,返回值可以任意返回. 这样看来,闭包里面的外部函数的内部变量也是对象,所以,当返回的内部函数被调用时,这个外部函数的变量就没有被释放. 这样看来,返回时,不需要考虑 ...

  6. Java 字符串常量存放在堆内存还是JAVA方法区?

    JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池. JDK1.8开始,取消了Java方法区,取而代之的是位于直接内 ...

  7. 【转载】java项目中经常碰到的内存溢出问题: java.lang.OutOfMemoryError: PermGen space, 堆内存和非堆内存,写的很好,理解很方便

    Tomcat Xms Xmx PermSize MaxPermSize 区别 及 java.lang.OutOfMemoryError: PermGen space 解决 解决方案 在 catalin ...

  8. 一个Java对象到底占用多大内存?

    最近在读<深入理解Java虚拟机>,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存? 在网上搜到了一篇博客讲的非常好 ...

  9. 一个Java对象到底占用多大内存

    在网上搜到了一篇博客讲的非常好,里面提供的这个类也非常实用: import java.lang.instrument.Instrumentation; import java.lang.reflect ...

随机推荐

  1. ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(系统保障篇)

    快速上手多人游戏服务器开发.后续会基于 Google Agones,更新相关 K8S 运维.大规模快速扩展专用游戏服务器的文章.拥抱️原生 Cloud-Native! 系列 ColyseusJS 轻量 ...

  2. @JsonFormat 格式化时间 时出现时间不准确问题

    今天突然报个问题,简单来说说就是数据库某一字段的记录时间为 14点,然而展示到前台却是 6点 我腚眼一看,postman测试的数据也是6点 然而idea查出来的并不是6点 再仔细一瞅idea实体类的时 ...

  3. 前端实操案例丨如何实现JS向Vue传值

    摘要:项目开发过程中,组件通过render()函数渲染生成,并在组件内部定义了自定义拖拽指令.自定义拖拽指令规定了根据用户可以进行元素拖拽.缩放等一系列逻辑处理的动作. 本文分享自华为云社区<[ ...

  4. C++中使用sort对常见容器排序

    本文主要解决以下问题 STL中sort的使用方法 使用sort对vector的排序 使用sort对map排序 使用sort对list排序 STL中sort的使用方法 C++ STL 标准库中的 sor ...

  5. [Django框架之路由层匹配、有名 无名分组、反向解析、路由分发、名称空间、伪静态、本地虚拟环境、django版本区别]

    [Django框架之路由层匹配.有名 无名分组.反向解析.路由分发.名称空间.伪静态.本地虚拟环境.django版本区别] 路由层 路由即请求地址与视图函数的映射关系,如果把网站比喻成一本书,那路由就 ...

  6. FastDFS依赖无法导入

    FastDFS依赖无法导入 fastdfs-client-java 导入爆红 <!-- FastDFS--> <dependency> <groupId>org.c ...

  7. Flutter 2.2 更新详解

    Flutter 2.2 版已正式发布!要获取新版本,您只需切换到 stable 渠道并更新目前安装的 Flutter,或前往 flutter.cn/docs/get-started 从头开始安装. 虽 ...

  8. [Java] SpringBoot

    背景 简化SSM(H)中大量的配置工作,开发人员只关心提供业务功能 可以看成简化了的.按照约定开发的SSM(H) 概念 JavaBean:满足规范的Java类(属性private+默认构造方法+get ...

  9. [Web] 计算机网络课程(一)

    局域网 覆盖范围小,自己花钱买设备,自己单位维护 线长不超过100米,带宽固定(10M 100M 1000M) 星形结构,上层交换机口少,但每个口带宽高 广域网 距离远 如在家通过ADSL拨号上网,或 ...

  10. named piped tcp proxy 下载

    named piped tcp proxy 在某DN上面下载很麻烦,还要登录什么的,分享出来!希望大家支持 链接:https://pan.baidu.com/s/1fdJD6O0qb8_BkkrnMy ...