Java对象都是在堆上分配空间吗?答案竟然是...

作者 l Hollis

来源 l Hollis(ID:hollischuang)

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编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

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

例如:

public static String craeteStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();}

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

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

  • 同步省略

  • 标量替换

  • 栈上分配

本文主要来分析下标量替换和栈上分配。

标量替换、栈上分配

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

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

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

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

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

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

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

标量替换

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

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

public static void main(String[] args) {alloc();}private static void alloc() {Point point = new Point(1,2);System.out.println("point.x="+point.x+"; point.y="+point.y);}class Point{private int x;private int y;}

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

private static void alloc() {int x = 1;int y = 2;System.out.println("point.x="+x+"; point.y="+y);}

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

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

实验证明

Talk Is Cheap, Show Me The Code

No Data, No BB;

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

我们来看以下代码:

public static void main(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");// 为了方便查看堆内存中对象个数,线程sleeptry {Thread.sleep(100000);} catch (InterruptedException e1) {e1.printStackTrace();}}private static void alloc() {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 -histo 2809num     #instances         #bytes  class name
----------------------------------------------1:           524       87282184  [I2:       1000000       16000000  StackAllocTest$User3:          6806        2093136  [B4:          8006        1320872  [C5:          4188         100512  java.lang.String6:           581          66304  java.lang.Class

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

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

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

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

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

➜  ~ jmap -histo 2859num     #instances         #bytes  class name
----------------------------------------------1:           524      101944280  [I2:          6806        2093136  [B3:         83619        1337904  StackAllocTest$User4:          8006        1320872  [C5:          4188         100512  java.lang.String6:           581          66304  java.lang.Class

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

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

逃逸分析并不成熟

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

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

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

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

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

总结

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

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

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

最后,我们留一个思考题,我们之前讨论过了TLAB,今天又介绍了栈上分配。大家觉得这两个优化有什么相同点和不同点吗?

关于作者Hollis,一个对Coding有着独特追求的人,现任阿里巴巴技术专家,个人技术博主,技术文章全网阅读量数千万,《程序员的三门课》联合作者。

END

想读Spring源码?先从这篇「 极简教程」开始

震惊!这样终止线程,竟然会导致服务宕机?

99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546276.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

服务器运行容器工具大盘点!

服务器到底是什么&#xff1f;服务器的硬件好理解&#xff0c;其实就是一台性能、稳定性、扩展性等等比我们普通个人PC强的一台机器而已&#xff0c;它也需要搭载操作系统&#xff0c;比如有专门的Windows Server或者各种Linux发行版系统。只不过咱这里很多小伙伴可能还是处于学…

想读Spring源码?先从这篇「 极简教程」开始吧...

为什么要阅读源码&#xff1f;这是一个有趣的问题&#xff0c;类似的问题还有&#xff0c;为什么要看书&#xff1f;为什么要爬山&#xff1f; 这也是一个哲学问题&#xff0c;我想每个人都有不同的答案&#xff0c;下面我是对阅读源码好处的一些思考。 &#xff08;PS&#x…

ALP规则的验证

实验1&#xff1a;ALP规则的验证实验目标创建本地用户jack、tom、mike,创建本地组group1&#xff0c;并把所建的用户加入到group1中&#xff0c;通过ALP规则实现以上用户对e:\share\1.txt 文件内容读取和写入权限实验环境略实验步骤一、 创建本地用户jack、tom、mike右击计算机…

6大分布式定时任务对比

作者 | sharedCode来源 | blog.csdn.net/u012394095/article/details/79470904分布式定时任务简介 把分散的&#xff0c;可靠性差的计划任务纳入统一的平台&#xff0c;并实现集群管理调度和分布式部署的一种定时任务的管理方式&#xff0c;叫做分布式定时任务。常见开源方案 e…

Python利用pandas获取每行最大值和最小值

知识点 1.找出每一行最大值和对应的列索引&#xff1a; #找出每行最大值对应的索引 df[max_idx]df.idxmax(axis1) #取出该最大值 df[max_val]df.max(axis1)2.找出每一行最小值和对应的列索引&#xff1a; # 找出每行最小值对应的索引 df[min_idx]df.idxmin(axis1) # 取出该最…

一文搞懂 ThreadLocal 原理

当多线程访问共享可变数据时&#xff0c;涉及到线程间同步的问题&#xff0c;并不是所有时候&#xff0c;都要用到共享数据&#xff0c;所以就需要线程封闭出场了。数据都被封闭在各自的线程之中&#xff0c;就不需要同步&#xff0c;这种通过将数据封闭在线程中而避免使用同步…

单域MPLS ***数据转发实验分析

MPLS 数据详细转发流程示意图&#xff1a;配置思路&#xff1a;在自治系统AS100中配置IGP&#xff0c;使得两台PE路由器的lo 0接口路由可达&#xff1b;两台PE路由器路由可达后&#xff0c;在两台PE路由器之间建立MP-iBGP邻居关系&#xff0c;用来传输V4路由&#xff1b;AS100中…

IDEA 终于支持中文版和 JDK 直接下载了(太方便了)附新版介绍视频

这是我的第 48 篇原创文章。IDEA 2020.1 经过了漫长的打磨终于发布正式版了&#xff0c;而这次的版本不止直接支持 Java 14&#xff0c;还带来了两个重量级的功能&#xff1a;官方中文版支持和 JDK 直接下载。在之前的开发中&#xff0c;当我们需要下载 JDK 时&#xff0c;通常…

【BO】WEBI文件打开时提示Illegal access错误

在infoview中打开WEBI文件时&#xff0c;提示如下错误。 通过查看SCN&#xff0c;找到错误原因是CMC中有一个服务没有启动。 启动这个服务即可&#xff1a; WebIntelligenceProcessingServer转载于:https://www.cnblogs.com/uzipi/p/3905513.html

华为交换机链路聚合使用ENSP模拟器进行实验

一、简介 链路聚合(Eth-Trunk)是将多个物理接口捆绑为一个逻辑接口,实现增加链路带宽,提高可靠性,提供负载分担的目的。 二、华为ENSP模拟器仿真图 华为交换机LSW1和LSW2端口G0/0/2、G0/0/3实现链路聚合,这2个交换机通过静态路由实现2个不同网段V10、V20互通。 三、华…

为什么工作很卖力,最后还晋升不了?

作者 | 军哥来源 | 军哥手记&#xff08;公众号ID&#xff1a;zxhy_cj&#xff09;最近写文章多了一些思考&#xff0c;要写对大家有价值的、有启发、有思考的东西&#xff0c;希望读者朋友们enjoy&#xff01;为什么自我感觉良好&#xff0c;可是晋升没我&#xff1f;感觉老板…

基于Java的数据采集(终结篇)

关于写过关于JAVA采集入库的三篇文章&#xff1a; 基于Java数据采集入库&#xff08;一&#xff09;&#xff1a;http://www.cnblogs.com/lichenwei/p/3904715.html 基于Java数据采集入库&#xff08;二&#xff09;&#xff1a;http://www.cnblogs.com/lichenwei/p/3905370.ht…

华为交换机、路由器配置单臂路由实现不同网段通信

一、eNSP模拟器仿真图 二、华为交换机配置 //批量创建VLAN [Huawei]vlan batch 10 20 //PC1电脑与交换机连接端口配置 [Huawei]interface GigabitEthernet0/0/2 [Huawei-GigabitEthernet0/0/2]port link-type access [Huawei-GigabitEthernet0/0/2]port default vlan 10 [Huaw…

IDEA 正式版终于支持中文版和 JDK 直接下载了(太方便了)附介绍视频

IDEA 2020.1 经过了漫长的打磨终于发布正式版了&#xff0c;而这次的版本不止直接支持 Java 14&#xff0c;还带来了两个重量级的功能&#xff0c;官方中文版支持和 JDK 直接下载。 在之前的开发中&#xff0c;当我们需要下载 JDK 时&#xff0c;通常的步骤是这样的&#xff1…

HoughLine变换

对于HoughLine变换&#xff0c;有两种方法&#xff0c;标准霍夫变换&#xff08;SHT&#xff09;用的矩阵是CV_32FC2&#xff0c;用极坐标法记录直线,而累积概率霍夫变换&#xff08;PPHT&#xff09;用的是CV_32FC核心函数&#xff1a;cvCvtColor&#xff0c;cvHoughLines2&am…

华为交换机、路由器配置静态路由实现不同网段通信

一、eNSP模拟器仿真图 二、目标要求 在主核心交换机上配置vlanif10 192.168.10.254/24,vlanif20 192.168.20.254/24,vlanif100 192.168.100.1/24,PC1:192.168.10.1能ping通PC2:192.168.20.1,也能ping通PC3:192.168.30.1,交换机和路由器使用静态路由。 三、华为交换机配置…

答读者问:学历不高,要如何破局?

今天读者群在激烈讨论学历是否重要&#xff0c;有的朋友说非常重要&#xff0c;也有人说并没有那么重要。有读者问&#xff1a;“我是专科毕业&#xff0c;我需要读在职本科或者研究生吗”&#xff0c;也有读者问&#xff1a;“洋哥&#xff0c;三本毕业几年&#xff0c;想辞职…

华为路由器配置OSPF实现不同网段通信

一、简介 PC1、PC2、LSW1和AR1配置单臂路由,实现PC1能够ping通PC2,具体实现请参考:华为交换机、路由器配置单臂路由实现不同网段通信。下面实现AR1和AR2通过配置OSPF,实现PC1能ping通PC3。 二、华为路由器配置 AR1新增配置: [Huawei]int g0/0/1 [Huawei-GigabitEthernet…

史上最全的 SQL 优化方案!建议收藏

在进行MySQL的优化之前&#xff0c;必须要了解的就是MySQL的查询过程&#xff0c;很多查询优化工作实际上就是遵循一些原则&#xff0c;让MySQL的优化器能够按照预想的合理方式运行已。图-MySQL查询过程1优化的哲学 注&#xff1a;优化有风险&#xff0c;涉足需谨慎a优化可能带…

Linux Podman安装MySQL数据库

1.拉取MySQL镜像 这里拉取官方最新版本镜像 podman pull mysql2.查看本地镜像 podman images可以看到&#xff0c;我们已安装最新版本的mysql镜像 3.运行容器 可以使用以下命令来运行 mysql 容器 podman run -d --name mysql-test -p 4000:3306 -e MYSQL_ROOT_PASSWORD12…