前言

在JAVA虚拟机内存管理中,堆、栈、方法区、常量池等概念经常被提到,对理论知识的理解也常常停留在字面意思上,比如说堆内存中存放对象,栈内存中存放局部变量,常量池中存放字符串常量表等,本篇文章通过一个有趣的例子,尽量将这些理论概念通过程序样例及图解的方式表达清楚,让我们更能深入底层知识。

例子

源码
  • StringJvm类中定义了一个CONST_STRING字符串常量,并赋值"Hello World";
  • main方法定义String类型的str1,str2变量,两个变量都赋值"Hello World";
  • 比较一下str1和str2的地址值是否相等 ;
  • new 一个String类型的str3,并通过构造函数初始化值为"Hello World";
  • 比较一下str3和str3的地址值是否相等。
/**
* 理解JVM--堆内存中的对象
* @author zhuhuix
* @date 2020-05-19
*
*/
public class StringJvm {
public static final String CONST_STRING="Hello World"; public static void main(String[] args) { String str1 =CONST_STRING;
String str2 =CONST_STRING;
System.out.println("str1==str2: "+ (str1 == str2));
System.out.println("str1==CONST_STRING: "+ (str1 == CONST_STRING)); String str3 =new String(CONST_STRING);
System.out.println("str3==str1: "+(str3 == str1));
}
}
输出

先说两个比较结果:

  1. str1的地址值等于str2的地址值
  2. str1的地址值等于CONST_STRING的地址值
  3. str3的地址值不等于str1地址值,即也不等于str2值

图解

有些同学有可能会有疑问,明明四个字符串的值都是“Hello World”,地址值却有相等 ,也有不相等的,这里就会引入JVM的堆内存、栈内存,常量池的一些基本概念,我们直接上图再讲解说明:

  1. 定义静态常量CONST_STRING放入方法区;赋值时,JVM会在字符串常量池中放入"Hello World"字符串供共享使用,并将内存地址赋给静态常量。
  2. 定义str1变量并给该字符串赋值时,JVM首先会在字符串常量池中寻找字符串值相同的内存地址,并将该内存地址赋值str1变量。所以在程序中打印输出str1变量的地址值是否等 于静态常量CONST_STRING的地址值时,得到的结果是True.
  3. 定义str2变量时按以上2过程同样处理,也即str2的地址值等于CONST_STRING和str1的地址值。
  4. 在定义str3变量时,采用了强制new一个对象及构造方法传值方式处理,我们知道new一个对象,一定会在堆中分配内存给该对象,也即str3的地址值引用在堆内存中,所以str3的地址值肯定不等于str1的地址值。
深入分析

以上的图解只是JVM的理论上解释了为什么有些地址值相等,有些地址值不等,接下来我们创建一个获取地址值的工具类,来验证以上的理论知识。

首先我们利用Java中的Unsafe类创建一个工具类及静态方法,模仿C++手动管理内存的能力来获取对象的实际地址值。


/**
* 获取JVM object的内存地址
* @author zhuhuix
* @date 2020-05-19
*
*/
public class ObjectAddress {
private static Unsafe unsafe; static
{
try
{
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
}
catch (Exception e)
{
e.printStackTrace();
}
} public static long addressOf(Object o) {
Object[] array = new Object[] {o}; long baseOffset = unsafe.arrayBaseOffset(Object[].class);
int addressSize = unsafe.addressSize();
long objectAddress;
switch (addressSize)
{
case 4:
objectAddress = unsafe.getInt(array, baseOffset);
break;
case 8:
objectAddress = unsafe.getLong(array, baseOffset);
break;
default:
throw new Error("unsupported address size: " + addressSize);
} return(objectAddress);
}

接下来改造一下原来的程序:

/**
* 理解JVM--堆内存中的对象
* @author zhuhuix
* @date 2020-05-19
*
*/
public class StringJvm {
public static final String CONST_STRING="Hello World"; public static void main(String[] args) { System.out.println( "CONST_STRING的地址值:"+ObjectAddress.addressOf(CONST_STRING)); String str1 =CONST_STRING;
String str2 =CONST_STRING;
System.out.println( "str1的地址值 :"+ObjectAddress.addressOf(str1));
System.out.println("str1==str2: "+ (str1 == str2));
System.out.println("str1==CONST_STRING: "+ (str1 == CONST_STRING)); String str3 =new String(CONST_STRING);
System.out.println("str3的地址值 :"+ ObjectAddress.addressOf(str3));
System.out.println("str3==str1: "+(str3 == str1)); }
}

输出结果如下:

这里我们实际看到了对象的实际地址值,静态常量CONST_STRING的地址值等于str1的地址值;str3的地址值不等于str1的地址值。

学以致用

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

我们看下上面这段话,JVM告诉我们常量池有动态的特性,利用String类的intern()方法能够将当前String对象与常量池动态绑定起来。为了更好理解,我们再改造一下例子:

/**
* 理解JVM--堆内存中的对象
* @author zhuhuix
* @date 2020-05-19
*
*/
public class StringJvm {
public static final String CONST_STRING="Hello World"; public static void main(String[] args) { System.out.println( "CONST_STRING的地址值:"+ObjectAddress.addressOf(CONST_STRING)); String str1 =CONST_STRING;
String str2 =CONST_STRING;
System.out.println( "str1的地址值 :"+ObjectAddress.addressOf(str1));
System.out.println("str1==str2: "+ (str1 == str2));
System.out.println("str1==CONST_STRING: "+ (str1 == CONST_STRING)); String str3 =new String(CONST_STRING);
System.out.println("str3 new String的地址值 :"+ ObjectAddress.addressOf(str3));
System.out.println("str3==str1: "+(str3 == str1));
//通过intern方法在程序运行运程中与常量池进行动态绑定
str3=str3.intern();
System.out.println("str3.intern()的地址值 :"+ ObjectAddress.addressOf(str3)); System.out.println("str3.intern()后==str1: "+(str3 == str1));
}
}

看下输出结果:我们发现利用此方法str3的地址值与str1地址一致了,也就是说str3也指向了常量池中初始的内存分配地址。



把以上改造后的代码,再用这张图说明就一清二楚了。

写在最后

JVM的架构设计是非常精妙的,需要深入底层进行剖析;在对系统架构的学习过程中,结合原理动手写样例代码,画原型图是我们学习路上必不可少的一步。

举一个有趣的例子,让你轻松搞懂JVM内存管理的更多相关文章

  1. java虚拟机——轻松搞懂jvm

    一.JVM体系结构概述 JVM位置 JVM体系结构 1.1 类加载器 ClassLoader   类加载器(ClassLoader)负责加载class文件,class文件在文件开头有特定的文件标示,并 ...

  2. 轻松搞懂Java中的自旋锁

    前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...

  3. Ruby中,类方法和实例方法的一个有趣的例子

    最初的代码如下: class Object def abc p "instance abc" end def self.abc p "class abc" en ...

  4. 轻松搞懂Python递归函数的原理与应用

    递归: 在函数的定义中,函数内部的语句调用函数本身. 1.递归的原理 学习任何计算机语言过程中,“递归”一直是所有人心中的疼.不知你是否听过这个冷笑话:“一个面包,走着走着饿了,于是就把自己吃了”. ...

  5. 轻松搞懂WebService工作原理

    用更简单的方式给大家谈谈WebService,让你更快更容易理解,希望对初学者有所帮助. WebService是基于网络的.分布式的模块化组件. 我们直接来看WebService的一个简易工作流程: ...

  6. 轻松搞懂elasticsearch概念

      本文主要介绍elasticsearch6.0的一些基本概念,有助于深入理解.研究elasticsearch和elk系统 一图胜千言 elasticsearch与mysql参照来看 添加一条数据 紫 ...

  7. 一文轻松搞懂redis集群原理及搭建与使用

    今天早上由于zookeeper和redis集群不在同一虚拟机导致出了点很小错误(人为),所以这里总结一下redis集群的搭建以便日后所需同时也希望能对你有所帮助. 笔主这里使用的是Centos7.如果 ...

  8. 【转载】轻松搞懂WebService工作原理

    用更简单的方式给大家谈谈WebService,让你更快更容易理解,希望对初学者有所帮助. WebService是基于网络的.分布式的模块化组件. 我们直接来看WebService的一个简易工作流程: ...

  9. 一文轻松搞懂Vuex

    概念: Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式(官网地址:https://vuex.vuejs.org/zh/).它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状 ...

随机推荐

  1. 浅析常见的 Web 安全预防

    1. CSRF 跨站请求伪造,英文名:Cross-site request forgery CSRF 攻击流程 结合下面这张图说明 CSRF 的攻击流程. 李四在网站 A 注册了用户名,并且登录到了网 ...

  2. keepalived的一些。。

    继续采坑..有些坑,踩了才知道. 1.文件位置不能错. 首先是安装, 源码编译,--prefix=/usr/local/keepalive 然后用 sbin/keepalive -f  ...conf ...

  3. 【Python Deap库】遗传算法/遗传编程 进化算法基于python DEAP库深度解析讲解

    目录 前言 概述 启发式的理解(重点) 优化问题的定义 个体编码 初始族群的创建 评价 配种选择 锦标赛 轮盘赌选择 随机普遍抽样选择 变异 单点交叉 两点交叉 均匀交叉 部分匹配交叉 突变 高斯突变 ...

  4. 11.3 Go 开发博客

    11.2 Go 开发博客 1.1. MVC模式 MVC分层简化了分组开发.不同的开发人员可同时开发视图,控制器逻辑和业务逻辑. 耦合性低:视图层和业务逻辑层分离.相互独立,不受影响 重用性高:业务逻辑 ...

  5. Djano之ORM多表查询操作

    # 把 model 转化为 迭代器去循环 MODEL.objects.all().iterator() # 等同于 values, values_list, 但是 only 这种方式 获取字段属性依旧 ...

  6. 如何在本地调试你的 Spark Job

    生产环境的 Spark Job 都是跑在集群上的,毕竟 Spark 为大数据而生,海量的数据处理必须依靠集群.但是在开发Spark的的时候,不可避免我们要在本地进行一些开发和测试工作,所以如何在本地用 ...

  7. jsp 循环数字

    <c:forEach var ="i" begin="1" end="${homeexamque.optionNum}" step=& ...

  8. Java开发架构篇:领域驱动设计架构基于SpringCloud搭建微服务

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言介绍 微服务不是泥球小单体,而是具备更加清晰职责边界的完整一体的业务功能服务.领域驱动 ...

  9. Hyperledger Fabric——balance transfer(三)创建和加入Channel

    详细解析blance transfer示例的创建通道(Channel)和加入节点到通道的过程. 创建Channel 1.首先看app.js的路由函数 var createChannel = requi ...

  10. HDU6040 Hints of sd0061

    题目链接:https://vjudge.net/problem/HDU-6040 题目大意: 给出 \(n\) 个数,有 \(m\) 次询问,每次询问这 \(n\) 个数中第 \(k+1\) 大的数是 ...