前言

在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. css多行省略和单行省略

    实现文本省略: <!-- html代码 --> <p class="single">该文的主题思想即对自由境界的向往.朱自清当时虽置身在污浊黑暗的旧中国,但 ...

  2. IE8/IE9/IE10打开网页后,自动设置浏览器模式为“IE8/IE9/IE10兼容性视图”

    http://blog.snsgou.com/post-16.html 作者:SNSGOU 发布于:2013-07-17 22:23:05  分类:CSS/HTML     评论(0)  浏览(132 ...

  3. 设置键盘return键样式

    textField.returnKeyType = UIReturnKeySend; typedef NS_ENUM(NSInteger, UIReturnKeyType) { UIReturnKey ...

  4. SpringBoot2.x【一】从零开始环境搭建

    SpringBoot2.x[一]从零开始环境搭建 对于之前的Spring框架的使用,各种配置文件XML.properties一旦出错之后错误难寻,这也是为什么SpringBoot被推上主流的原因,Sp ...

  5. docker安装和基本使用

    1.安装环境 此处在Centos7进行安装,可以使用以下命令查看CentOS版本 lsb_release -a 在 CentOS 7安装docker要求系统为64位.系统内核版本为 3.10 以上,可 ...

  6. Java:成员变量、局部变量和静态变量

    梳理一下: 根据定义变量位置的不同,可以将变量分成两大类:成员变量和局部变量. 成员变量(俗称全局变量):在类里定义的变量.又分为实例变量和类变量(也成为静态变量). 实例变量:不以static修饰, ...

  7. 【C++】常见易犯错误之数值类型取值溢出与截断(1)

    1. 数据类型数值范围溢出 如标题所述,该错误出现的原因是由于变量的值超出该数据类型取值范围而导致的错误. 例题如下: (IDE环境:C-Free,编译器为mingw5,如下图) # include ...

  8. SSIS 数据类型 第二篇:变量的数据类型

    变量(Variable)用于存储在Package运行时用到的值,集成服务支持两种类型的变量:用户自定义的变量和系统变量,自定义的变量由用户来定义,系统变量由集成服务来定义. 变量的用途十分广泛,用于容 ...

  9. raw_input和input

    昨天在OJ上做CTF的题目,发现有道python的题目很有意思,让我知道了raw_input和input的区别,并且能干一些别的事情. 官方文档上说,input()相当于eval(raw_input( ...

  10. Vulnerability of SSL to Chosen-Plaintext Attack 读书报告

    这篇文章讲述了在SSL上的选择明文攻击.我想分四个部分讲讲我对这篇文章的理解. 1.CPA的定义: 2.文章讲述SSL相关基本概念漏洞: 3.对SSL的CPA攻击的过程: 4.实现这种攻击的可能性以及 ...