JVM学习笔记(一)——基础

介绍

在这里插入图片描述
由上图可以看出,JVM分为五个大块,分别是方法区,jvm运行方法栈,本地方法栈,程序计数器以及最大块的堆。

程序计数器

程序计数器,人如其名,程序计数器就是一个记录数字的地方,那他记录的是什么数字呢?一个程序运行需要CPU,CPU有内核,一个内核只能执行一条线程的指令,我们所说的多线程,很多情况下是一个内核实现。因为一个内核只能执行一条线程的指令,那么我这么多线程该如何跑起来还能让用户感觉不到停顿呢,这是因为一个内核在轮流切换并分配处理时间来实现多线程工作,因此我线程A跑着跑着就要停一会,内核这个时候要处理线程B的指令,然后过了一会内核又要执行线程A了,这个时候内核是不是要知道之前线程A在哪停顿的?所以程序计数器里就需要记录线程A和线程B执行的到哪个位置了,并且线程A和线程B都有一个独立的计数器,每个线程的计数器相互独立互不影响,因此程序计数器是线程私有的。
JVM中的程序计数器,如果执行的是java类方法,那这个计数器就是记录的虚拟机字节码指令地址;如果执行的是本地方法(native 方法—非java语言的方法),那么计数器记录的值就是null。
程序计数器有个特点,就是这块区域是JVM中唯一一个不会抛出OutOfMemoryError异常的地方。这里解释一下,为什么程序计数器不会有内存泄漏的情况:

OutOfMemoryError,内存泄漏,为什么会内存泄漏?因为内存不够用,在栈和堆中,如果没有指定大小,那么栈和堆的内存大小会随着程序的运行自动变化,也就是常说的伸缩扩容。在程序运行中,如何栈和堆内存不够,开始向空余内存扩张的时候,发现没有空余内存了,那么就会抛出OutOfMemoryError异常。.

而程序计数器就是记录的下一条执行命令的地址,虽然它一开始分配的内存空间很小,但是他够用,哪怕程序有问题,也会是堆栈先出问题,轮不到它。

本地方法栈

详情请见JVM虚拟机栈,这里说下区别,区别就是JVM虚拟机栈执行的是java方法,本地方法栈执行的是非java方法,因为在jdk中也有很多其他语言的方法,想一下计算机只认识什么语言?你知道的。

方法区

从图上列举的信息来看,方法区里存放的是常量,类型信息,静态变量。
这个有几个知识点:

(一) 老版的JDK,也就是JDK1.8之前,方法区也被称为永久代,1.7就已经开始逐步去除永久代,直到1.8完全摒弃。
(二) 方法区其实和堆的作用差不多,只是存的东西不一样。
(三) 我们常说的常量池就是方法区的一部分。

Java虚拟机栈

在介绍本地方法栈的时候,说过本地栈执行的是非java语言方法,那么java虚拟机栈就是执行java方法。虚拟机栈中被一个个线程开辟了一个个私有空间,每个线程窜着一个个栈帧:每个方法被执行的时候,java虚拟机就会同步创建一个栈帧,当当前线程中每个方法执行完毕的过程,就是栈帧在虚拟机栈中从入栈到出栈的过程,遵循先进后出。可以看出java虚拟机栈是线程私有的,他的生命周期和线程相同。
在这里插入图片描述

栈帧:
虚拟机栈中,最重要的组成部分就是栈帧,栈帧也是方法运行时的最小数据结构。那么栈帧里到底存了什么?在《深入理解JVM虚拟机》中是这样描述的:栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息。这里我们需要了解也就是局部变量表。
栈帧中的局部变量表中存储了三个信息——基本数据类型、对象引用和returnAddres类型:
基本数据类型:也就是java里的八个基本数据类型;
returnAddres类型:指向了一条字节码指令的地址。
对象引用:栈帧中存了堆中对象的地址,也就是reference,关于reference的访问方式,这里介绍下2种。句柄和直接指针,我引用书种的两张图来说明下:

句柄
在这里插入图片描述

直接指针
在这里插入图片描述

堆,是整个虚拟机中占用内存最大的地方,但是他的用途很简单,就是存放对象实例的地方,所有线程都可以来堆里面拿到对象实例,因此堆是线程共享的。

在java堆中,每个线程都开辟一个内存空间,称本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在对应的缓冲区分配,用完再分配新的缓冲区。
为什么会出现TLAB分配方法,试想一下,把堆内存分成一块一块,每一块用来存储对象,在多线程环境下,大家抢着来堆里面占地方,这样堆在分配内存的时候可能会出现指针碰撞,影响分配效率,所以会采用TLAB。TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。
命令:-XX:+/-UseTLAB 开启关闭、-XX:TLABSize 指定缓冲区大小。
说到TLAB,再聊一下对象分配流程:首先如果开启栈上分配,JVM会先进行栈上分配,如果没有开启栈上分配或则不符合条件的则会进行TLAB分配,如果TLAB分配不成功,再尝试在eden区分配,如果对象满足了直接进入老年代的条件,那就直接分配在老年代。后面我会将详细的流程画出来,这里先作简单介绍。

对象包括三个部分:对象头、实例数据和对齐填充:

  • 对象头中包含了两类信息:存储对象自身的运行时数据和类型指针。
  • 对象自身的运行时数据包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、>偏向线程ID、偏向时间戳等
  • 类型指针:就是确定改对象是属于哪个类的实例。

对象中的实例数据简单点说就是我们编程时候定义的各类型字段内容。这里联想一下,栈帧中包含了一个局部变量表,其中含有基本数据类型等信息,所以我这边是这么来理解的,栈帧是方法的内存,堆是对象的内存,栈帧中的局部变量表可以理解为局部数据信息,堆中的实例数据可以理解为全局数据信息,这里的数据信息主要是指基本数据类型和对象指针。区别在于栈帧的局部变量表还有个returnAddress类型。
对齐填充没有实际意义,HotSpot内存管理规定,对象的大小一定是8字节的倍数,其中对象头的大小正好是8字节的倍数,但是实例数据却没有固定的大小,因此对齐填充主要是用来补全实例数据不够8字节倍数的部分。
要了解堆,那就不得不了解垃圾回收——Garbage Collected,下面我将以hotSpot虚拟机的垃圾回收器来介绍java发展历程中的各个垃圾回收机制。

对象分配

  1. 虚拟机遇到一条new指令时,先执行相应的类加载过程,接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
    分配对象的方法有两种:

    指针碰撞

    如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。

    空闲列表

    如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。

    选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 思考这样一个问题:在堆中创建对象是十分平常的,而堆是线程共享的,所以分配对象并不是线程安全的行为,如果正在给对象A分配内存,指针还没来得及修改,对象B又同时被分配到这块内存,那这该如何解决?
    第一种就是上面提到的TLAB;第二种就是CAS,也就是如果B发现这块内存被使用了,重新分配。

  2. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  3. 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

  4. 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值