深入学习JVM: (2) Jvm的内存模型

一. 前言

深入学习Jvm的第二篇文章, 依然作为总结, 可能会有点儿乏味, 但却是面试及虚拟机调优的必备知识. 话不多说, 进入正题.

二. Jvm内存模型大致划分

先直接来张图:

Jvm内存区域划分.png

相信这张图很多人都不陌生, 也可能有部分同学只听说过堆和栈的. 不过没关系, 听在下娓娓道来即可, 先简单给大伙儿描述一下上图中每个区域大概是用来干什么的(会刻意省略掉部分复杂的东西, 目的是先有个印象, 便于后面知识的理解):

  1. : 通常用来存放我们的局部变量表, 如果是基础数据类型的变量, 放在栈中. 如果是对象类型的变量, 则实际保存在堆中, 栈中只存放该对象的引用地址.
  1. : 存放对象
  1. 方法栈: 给本地方法用的栈, 也就是native方法, 如果你看过Thread的类的start()方法, 就会发现跟踪到底, 调用了一个带有native修饰符的start0()方法, 这意味着它是跨语言调用, 通常是调用C或C++的函数库.
  1. 方法区: 我们的常量, 静态变量, 以及类元信息都在方法区, 这个区也叫元空间. 什么类元信息? 如果看过在下的上一篇文章, 你可能会有所了解. 这个类元信息就是类加载到jvm内存后所产生的、关于该类的所有信息. 需要注意的是, 这块区域使用的是直接内存, 而不是划分给虚拟机的内存.
  1. 程序计数器: 存放Java字节码执行到哪儿了, 可以粗劣的理解成我们debug时的行号. 这个东西可也是至关重要的, 试想一下多线程的情况你就明白了, 这么多的线程, 涉及到挂起和线程的上下文切换, 每个线程如何知道被唤醒后下一行该执行哪行代码呢?
  1. 类装载子系统: 用来将类加载到Jvm的.
  1. 字节码执行引擎: 顾名思义, 执行字节码的嘛.

相信大家注意到那两个色块儿了, 线程独有和线程共享, 什么意思呢?
线程独有: 会给每个线程单独分配一小块内存空间
线程共享: 所有线程共享一块内存
这样说可能不是很理解, 没关系, 请继续看下文, 因为很多知识需要看完之后加到一块儿才能理解.

三. 细说Jvm栈

先来个简单的代码:

/**
 * 简单的java程序, 用于说明栈的关系
 *
 * @Author: deadline
 * @Date: 2021-02-27 18:33
 */
public class JvmTestForStack {

    public int count() {
        int a = 1;
        int b = 2;
        int c = a + b;
        return c;
    }

    public static void main(String[] args) {
        JvmTestForStack jvmTestForStack = new JvmTestForStack();
        jvmTestForStack.count();
    }
}

再来张图:

栈内存区域.png

代码和图呢, 它们是一伙儿的, 把它们结合起来仔细看看, 我想你应该已经理解的差不多了, 不过我还是打算再多讲讲:

可能你听说过栈这种数据接口, 先入后出嘛, Jvm内存中的栈也是先入后出的. 之前说过栈是每个线程都单独具备的, 意思是, 每个线程都会分配到一小块栈内存空间, 如上图. 说到这里, 又不得不说栈帧, 什么是栈帧呢? 每个方法被调用时, 就会压入一小块儿内存空间到该线程的栈内存中, 这一小块儿内存空间的名字就叫做栈帧. 这个压入的动作就像往弹夹中压入子弹一样, 先压入的最后击发. 根据上述代码, main()方法是最先入栈的, 所以它的栈帧在栈底, 随后是count()方法, 当count()方法执行完毕, 分配给它的栈帧会立马销毁, 这个动作叫做出栈.

结合上述, 图中的大部分内容我相信你已经看懂了, 不过作为总结...这显然不够, 所以我要再写写操作数栈, 以及动态链接

说到这里, 就得提一个java指令: javap, 这是一个用来反编译class字节码的指令, 使用方式: javap -v xxx.class, 下面是使用该指令反编译上述代码的部分代码:

Constant pool:
   #1 = Methodref          #5.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // jvm/JvmTestForStack
   #3 = Methodref          #2.#27         // jvm/JvmTestForStack."<init>":()V
   #4 = Methodref          #2.#29         // jvm/JvmTestForStack.count:()I
   #5 = Class              #30            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Ljvm/JvmTestForStack;
  #13 = Utf8               count
  #14 = Utf8               ()I
  #15 = Utf8               a
  #16 = Utf8               I
  #17 = Utf8               b
  #18 = Utf8               c
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               jvmTestForStack
  #24 = Utf8               MethodParameters
  #25 = Utf8               SourceFile
  #26 = Utf8               JvmTestForStack.java
  #27 = NameAndType        #6:#7          // "<init>":()V
  #28 = Utf8               jvm/JvmTestForStack
  #29 = NameAndType        #13:#14        // count:()I
  #30 = Utf8               java/lang/Object

这个被称之为常量池, 可以看处, 它似乎把我们的java代码分解成了一个一个的符号, 比如#19的main符号, 以及#7的()V符号. 这些符号是存放在方法区中的, 再看下方反编译后的代码:

public int count();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: iload_3
         9: ireturn

这就是count()方法反编译后的代码, 我们从 0: iconst_1 这行代码开始看, 结合上方的栈内存图, 就可以看出count()方法在底层究竟是怎么做的.

有以上铺垫, 现在可以详细说说了:

动态链接: 认真观察一下上面的常量池反编译代码的 #4 = Methodref #2.#29 这一行, 这个#2.#29是什么意思呢? 这个就是符号链接, #2链接着#28, #28 = jvm/JvmTestForStack; #29链接着#13和#14, #13 = count, #14 = ()I; 那么链接起来, 就变成了 jvm/JvmTestForStack.count()I 这行代码, 之前说过, jvm执行到这行代码仍然不知道count()方法具体有哪些字节码呀, 所以底层执行时, 还需要转换一次链接, 也就是把这些个符号链接, 转换为直接链接, 让jvm知道走到这行代码时, 应该去内存中的哪个位置拿到可执行的jvm字节码.

操作数栈: 经过上面说的动态链接, jvm字节码执行引擎找到了count()方法具体的字节码, 这些字节码jvm可以看懂, 并会根据规则再次转成计算机可以看懂的汇编语言, 然后执行. 而我们的计算机它只认识0和1, 所以就有了上面栈内存图中的...将操作数压栈出栈运算等操作.

再次强调: 对于基本数据类型, 也就是int, double, boolean等类型, 它们的值是直接存放在栈中的. 而对于对象, 大多数时候, 栈中存放的仅仅是一个引用, 真正的值存放在堆中.

四. 细说Jvm堆

仍然是先上图:

堆内存区域及gc过程.png

我们的堆内存区域被划分成了两段: 分别是年轻代和老年代, 年轻代占整个堆内存区域的1/3, 老年代占2/3. 年轻代又别划分为Eden区和幸存区(Survivor区), Eden区占年轻代的8/10, 两个Survivor各占1/10;

一般情况下, 新new的对象会被放到Eden区, Eden区放满则执行minor gc, 进行垃圾回收, 那什么样的对象会被视为垃圾对象呢? 其实就是没有任何引用的对象, 无法再通过任何变量访问的对象. 整个过程呢, 图中已经画的很清晰了, 不过多解释了. 需要注意的是, 除了图中提到的对象动态年龄判断机制, 还有很多种情况会将对象移动到老年代, 会在下一篇文章中细说. 当老年代被放满, 则会触发full gc.

full gc为什么那么慢?

在进行full gc的时候, Jvm会执行一个STW的机制, 全称stop the world. 停止所有用户线程, 进行full gc. 为什么要有stw机制? gc的算法有很多种, 但都需要标记出哪些是垃圾对象或非垃圾对象, 所以我的猜测是, 如果一边进行垃圾回收, 一边又有新的垃圾对象产生...就像遍历一个集合时, 又有其它线程不停的在新增或者修改集合中的值, 那么将会产生很严重的后果, 可能永远都遍历不完, 也可能发生线程安全问题等等等等.

五. Jvm内存参数配置

一一一然是先上图:

Jvm内存参数.png

上图即为Jvm中各个区域的内存配置参数, 上图中的值仅是示例值, 具体大小请根据自身系统的业务情况来定, 不知道该配置多大的同学也可以不配置, java默认的大小已经足够大多数系统使用了, 想要了解或学习如何合理的配置Jvm内存各个区域的大小, 请期待在下的下一篇文章.

完整的参数示例: java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar test.jar
此外还有一个需要注意的地方, 相信大家注意到我反复的再提方法区使用的是直接内存. 方法区的默认大小是21M, 当放满之后, 会执行full gc. 然后根据gc的结果, 动态的水平扩缩容该区域的大小, 如果gc释放的量较大, 则缩小该空间; 如果释放的量较小, 则放大该空间(不会大于MaxMetaspaceSize设置的值); 如果没有设置该值, 则没有限制, 且程序启动时, 就会执行好几次full gc

对了, 根据本文所述, 所以模拟栈溢出, 无限递归调用方法即可, 模拟堆溢出, 弄个集合不停new对象即可, 嘿嘿...^^

今天的总结和分享就到这里, 如果有说的不对的地方, 还请大家不吝赐教.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容