Java中对象都是分配在堆上吗?你错了!

前言

我们在学习使用Java的过程中,一般认为new出来的对象都是被分配在堆上,但是这个结论不是那么的绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致Java中new出来的对象并不一定分别在所认为的堆上。这两个点分别是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)。本文首先对这两者进行介绍,而后对Java对象分配过程进行介绍。

1. 逃逸分析

1.1 逃逸分析的定义

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

在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。

Java在Java SE 6u23以及以后的版本中支持并默认开启了逃逸分析的选项。Java的 HotSpot JIT编译器,能够在方法重载或者动态加载代码的时候对代码进行逃逸分析,同时Java对象在堆上分配和内置线程的特点使得逃逸分析成Java的重要功能。

1.2 逃逸分析的方法

Java Hotspot编译器使用的是

[plain] view plain copy Choi J D, Gupta M, Serrano M, et al. Escape analysis for Java[J]. Acm Sigplan Notices, 1999, 34(10): 1-19.

Jong-Deok Choi, Manish Gupta, Mauricio Seffano,Vugranam C. Sreedhar, Sam Midkiff等在论文《Escape Analysis for Java》中描述的算法进行逃逸分析的。该算法引入了连通图,用连通图来构建对象和对象引用之间的可达性关系,并在次基础上,提出一种组合数据流分析法。由于算法是上下文相关和流敏感的,并且模拟了对象任意层次的嵌套关系,所以分析精度较高,只是运行时间和内存消耗相对较大。

绝大多数逃逸分析的实现都基于一个所谓“封闭世界(closed world)”的前提:所有可能被执行的,方法在做逃逸分析前都已经得知,并且,程序的实际运行不会改变它们之间的调用关系 。但当真实的 Java 程序运行时,这样的假设并不成立。Java 程序拥有的许多特性,例如动态类加载、调用本地函数以及反射程序调用等等,都将打破所谓“封闭世界”的约定。

不管是在“封闭世界”还是在“开放世界”,逃逸分析,作为一种算法而非编程语言的存在,吸引了国内外大量的学者对其进行研究。

1.3 逃逸分析后的处理

经过逃逸分析之后,可以得到三种对象的逃逸状态。

GlobalEscape(全局逃逸), 即一个对象的引用逃出了方法或者线程。例如,一个对象的引用是复制给了一个类变量,或者存储在在一个已经逃逸的对象当中,或者这个对象的引用作为方法的返回值返回给了调用方法。

ArgEscape(参数级逃逸),即在方法调用过程当中传递对象的应用给一个方法。这种状态可以通过分析被调方法的二进制代码确定。

NoEscape(没有逃逸),一个可以进行标量替换的对象。可以不将这种对象分配在传统的堆上。

编译器可以使用逃逸分析的结果,对程序进行一下优化。

堆分配对象变成栈分配对象。一个方法当中的对象,对象的引用没有发生逃逸,那么这个方法可能会被分配在栈内存上而非常见的堆内存上。

消除同步。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。

矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在CPU寄存器内,这样能大大提高访问速度。

下面,我们看一下逃逸分析的例子。

class Main {
public static void main(String[] args) {
example();
}
public static void example() {
Foo foo = new Foo(); //alloc
Bar bar = new Bar(); //alloc
bar.setFoo(foo);
}
}
class Foo {}
class Bar {
private Foo foo;
public void setFoo(Foo foo) {
this.foo = foo;
}
} 

在这个例子当中,我们创建了两个对象,Foo对象和Bar对象,同时我们把Foo对象的应用赋值给了Bar对象的方法。此时,如果Bar对在堆上就会引起Foo对象的逃逸,但是,在本例当中,编译器通过逃逸分析,可以知道Bar对象没有逃出example()方法,因此这也意味着Foo也没有逃出example方法。因此,编译器可以将这两个对象分配到栈上。

1.4 编译器经过逃逸分析的效果

测试代码:

package com.yang.test2;
/**
* Created by yangzl2008 on 2015/1/29.
*/
class EscapeAnalysis {
private static class Foo {
private int x;
private static int counter;
public Foo() {
x = (++counter);
}
}
public static void main(String[] args) {
long start = System.nanoTime();
for (int i = 0; i < 1000 * 1000 * 10; ++i) {
Foo foo = new Foo();
}
long end = System.nanoTime();
System.out.println("Time cost is " + (end - start));
}
} 

设置JVM运行参数:

未开启逃逸分析设置为:

-server -verbose:gc

开启逃逸分析设置为:

-server -verbose:gc -XX:+DoEscapeAnalysis

在未开启逃逸分析的状况下运行情况如下:

[GC 5376K->427K(63872K), 0.0006051 secs]
[GC 5803K->427K(63872K), 0.0003928 secs]
[GC 5803K->427K(63872K), 0.0003639 secs]
[GC 5803K->427K(69248K), 0.0003770 secs]
[GC 11179K->427K(69248K), 0.0003987 secs]
[GC 11179K->427K(79552K), 0.0003817 secs]
[GC 21931K->399K(79552K), 0.0004342 secs]
[GC 21903K->399K(101120K), 0.0002175 secs]
[GC 43343K->399K(101184K), 0.0001421 secs]
Time cost is 58514571 

开启逃逸分析的状况下,运行情况如下:

Time cost is 10031306

未开启逃逸分析时,运行上诉代码,JVM执行了GC操作,而在开启逃逸分析情况下,JVM并没有执行GC操作。同时,操作时间上,开启逃逸分析的程序运行时间是未开启逃逸分析时间的1/5。

2. TLAB

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。

也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。

关于对象分配的JDK源码可以参见JVM 之 Java对象创建[初始化]中对OpenJDK源码的分析。

3. Java对象分配的过程

编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2.

如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3.

重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.

在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5.

执行一次Young GC(minor collection)。

经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。

对象不在堆上分配主要的原因还是堆是共享的,在堆上分配有锁的开销。无论是TLAB还是栈都是线程私有的,私有即避免了竞争(当然也可能产生额外的问题例如可见性问题),这是典型的用空间换效率的做法。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 深入了解java内存分配和回收策略

    一.导论 java技术体系中所提到的内存自动化管理归根结底就是内存的分配与回收两个问题,之前已经和大家谈过java回收的相关知识,今天来和大家聊聊java对象的在内存中的分配.通俗的讲,对象的内存分配就是在堆上的分配,对象主要分配在新生代的Eden上(关于对象在内存上的分代在垃圾回收中会补上,想了解的也可以参考<深入理解java虚拟机>),如果启动了本地线程分配缓冲,讲按线程优先在TLAB上分配.少数情况下也是直接在老年代中分配. 二.经典的分配策略 1.对象优先在Eden上分配 一般情况下对

  • Java 内存分配深入理解

    Java 内存分配深入理解 本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java.这类文章网上有很多,但大多比较零碎.本文从认知过程角度出发,将带给读者一个系统的介绍. 进入正题前首先要知道的是Java程序运行在JVM(Java  Virtual Machine,Java虚拟机)上,可以把JVM理解成Java程序和操作系统之间的桥梁,JVM实现了Java的平台无关性,由此可见JVM的重要性.所以在学习Java内存分配原理的时候一定要牢记这一切都是在JVM中进行的,JVM是

  • 关于Java变量的声明、内存分配及初始化详解

    实例如下: class Person { String name; int age; void talk() { System.out.println("我是: "+name+", 今年: "+age+"岁"); } } public class TestJava2_1 { public static void main(String args[]) { Person p; if (p == null) { p = new Person(); }

  • 浅谈Java继承中的转型及其内存分配

    看书的时候被一段代码能凌乱啦,代码是这样的: package 继承; abstract class People { public String tag = "疯狂Java讲义"; //① public String name = "Parent"; String getName(){ return name; } } class Student extends People { //定义一个私有的tag实例变量来隐藏父类的tag实例变量 String tag =

  • 浅谈JAVA内存分配与参数传递

    JAVA中方法的参数传递方式只有一种:值传递. JAVA内存分配: 1.栈:存放 基本类型的数据.对象的引用(类似于C语言中的指针) 2.堆:存放用new产生的数据 3.静态域:存放在对象中用static定义的静态成员 4.常量池:存放常量 5.寄存器 6.非RAM存储 class BirthDate{ private int day; private int month; private int year; public BirthDate(int d,int m,int y){ day=d;

  • Java GC 机制与内存分配策略详解

    Java GC 机制与内存分配策略详解 收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现 自动内存管理解决的是:给对象分配内存 以及 回收分配给对象的内存 为什么我们要了解学习 GC 与内存分配呢? 在 JVM 自动内存管理机制的帮助下,不再需要为每一个new操作写配对的delete/free代码.但出现内存泄漏和溢出的问题时,如果不了解虚拟机是怎样使用内存的,那么排查错误将是一项非常艰难的工作. GC(垃圾收集器)在对堆进行回收前,会先确定哪些对象"存活",哪些已经&quo

  • Java中对象都是分配在堆上吗?你错了!

    前言 我们在学习使用Java的过程中,一般认为new出来的对象都是被分配在堆上,但是这个结论不是那么的绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致Java中new出来的对象并不一定分别在所认为的堆上.这两个点分别是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer).本文首先对这两者进行介绍,而后对Java对象分配过程进行介绍. 1. 逃逸分析 1.1 逃逸分析的定义 逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压

  • Java中对象与C++中对象的放置安排的对比

    Java中对象与C++中对象的放置安排的对比 概要: Java中,所有的对象都存放在堆(Heap,一种通用的内存池)中:而对象的引用是存放在堆栈(Stack)中的. 我们可以通过String直接声明的字符串与new String声明出来的字符串使用equals()和"=="进行的比较,从而理解对象和引用的关系及它们的存储位置. 堆栈是一种快速有效的分配存储方法,仅次于寄存器.创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动堆栈指针. 堆不同于堆栈的好处是:

  • 了解java中对象基础Object类

    目录 一.Object简述 1.显式扩展 2.引用与对象 二.基础方法 1.getClass 2.toString 3.equals与hashCode 4.thread相关 5.clone 6.finalize 三.生命周期 1.作用域 2.垃圾回收机制 四.源代码地址 一.Object简述 源码注释:Object类是所有类层级关系的Root节点,作为所有类的超类,包括数组也实现了该类的方法,注意这里说的很明确,指类层面. 所以在Java中有一句常说的话,一切皆对象,这话并不离谱. 1.显式扩展

  • 你真的知道Java中对象的销毁吗

    在日常的开发中.我们都知道,Java的内存清理是通过垃圾回收器进行的,那么其是如何将没用的对象被被清理掉的呢? Java 语言的内存自动回收称为垃圾回收(Garbage Collection)机制,简称 GC.垃圾回收机制是指 JVM 用于释放那些不再使用的对象所占用的内存. Java对象在使用后需要清理. 对象清理是释放该对象所占用的内存. 在创建对象时,用户必须使用new操作符为对象分配内存. 清除对象后,系统会自动回收内存,不需要用户进行额外的处理. 这也是Java语言的一个特性,它使程序

  • 谈谈Java中对象,类和this,super,static关键字的使用

    目录 Java对象究竟是什么 创建对象的过程 创建多个对象时,内存的变化 无处不在的this和super关键字 static关键字 为何如此特殊 Java对象究竟是什么 对象:对象是类的一个实例,有状态和行为. 类:类是一个模板,它描述一类对象的行为和状态.例如 人 是一个类 其状态有:姓名.性别.身高.体重等 其行为:吃饭.睡觉.聊天.运动等     public class Person {         /**          * 状态 or 属性          */       

  • Java中对象的深复制(深克隆)和浅复制(浅克隆)介绍

    1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象. ⑵深复制(深克隆) 被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量.那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象.换言之,深复制把要复制的对象所引用的对象都复制了一遍. 2.Java的clone()方法 ⑴clone方法将对象复制了一份并返回

  • Java中对象序列化与反序列化详解

    本文实例讲述了Java中对象序列化与反序列化.分享给大家供大家参考.具体如下: 一.简介 对象序列化(Serializable)是指将对象转换为字节序列的过程,而反序列化则是根据字节序列恢复对象的过程. 序列化一般用于以下场景: 1.永久性保存对象,保存对象的字节序列到本地文件中: 2.通过序列化对象在网络中传递对象: 3.通过序列化在进程间传递对象. 对象所属的类必须实现Serializable或是Externalizable接口才能被序列化.对实现了Serializable接口的类,其序列化

  • 详解Java中对象序列化与反序列化

    序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程.一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等.在网络传输过程中,可以是字节或是XML等格式.而字节的或XML编码格式可以还原完全相等的对象.这个相反的过程又称为反序列化. Java对象的序列化与反序列化 在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象.但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的.只有JVM处于运行状态的时候,这些对

  • 浅析Java中对象的创建与对象的数据类型转换

    Java:对象创建和初始化过程 1.Java中的数据类型     Java中有3个数据类型:基本数据类型(在Java中,boolean.byte.short.int.long.char.float.double这八种是基本数据类型).引用类型和null类型.其中,引用类型包括类类型(含数组).接口类型.     下列语句声明了一些变量: int k ; A a; //a是A数据类型的对象变量名. B b1,b2,-,b10000;// 假定B是抽象类或接口. String s; 注意:从数据类型

  • Java中对象的销毁方法分析

    本文较为详细的分析了Java中对象的销毁方法.分享给大家供大家参考.具体分析如下: Java中的基本数据类型变量和对象的名称引用变量如定义在方法中,都为局部变量.但对象本身不一定是局部生命周期.如函数外存在其他对该对象的引用变量,则该对象的生命周期延伸至该其他引用变量所在的块. 如从被调用函数参数引用传值或返回值到主调用函数所在的对象类型变量中,则该对象都仍存在(但被调用函数的该对象的引用变量生命周期结束,因此引用变量是局部变量),此时对象突破了局部变量的局部生命期. Java对象销毁 Java

随机推荐