详细的图又需要的,评论区留言,后面发给你
上一篇玩完类加载器、双亲委派机制之后,咱们知道了:
- 类加载器就是用来将类整理验证...,然后将字节码文件送到JVM去处理
- (记得咱们的民政&局小故事嘛,说白了类加载器就是一个贴心中间处理机构,怕你送到JVM的东西:
- 有可能太大了
- 有可能携带啥病毒
- 有可能格式不正确,不符合jvm处理规范
- 有可能得先进行初始化赋值等操作处理一下才能继续往下送等等
- ...
- (记得咱们的民政&局小故事嘛,说白了类加载器就是一个贴心中间处理机构,怕你送到JVM的东西:
- 双亲委派机制,就是用来保证安全的,至于具体工作过程以及实际分类等知识,敬请收看上一篇,保证有所收获:玩转~类加载器&双亲委派机制
话说字节码文件进入到java内存(运行时数据区域)中,咱擅闯龙潭是不是得先知道龙潭里面的分布是怎样的吧。
- 哪里有鳄鱼潭?
- 哪里有会把人吸进去的沼泽地?
- 哪里有毒区?
为了保命并能顺利闯关成功,所以咱们得先扒光java内存区域。
- 运行时数据区域:Java虚拟机在执行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域(
jvm将虚拟机分为5大区域),这些区域都有各自的特点、用途以及创建和销毁的时间。
大家都知道,从电脑的角度讲:
- 进程,当咱们用咱们的window10(或其他版本...)启动一个应用程序或者叫应用,就相当于启动了一个进程。一个进程中有好多线程。
- 线程大小可以想象成为迷你版的进程,是由进程中千千万万个线程来执行自己寄宿的进程里面的各个应用程序的任务的。(比如说咱们启动了QQ,那就相当于启动了一个QQ_进程,对吧。然后当咱们在qq中同时挂着qq邮箱,又打开消息和qq好友聊天时,那么qq邮箱和聊天窗口可以说是由两个线程处理的......)。大家就记得最终线程是用来执行具体任务的,是个打手。
- 咱们OS在分配资源时:
- 把(除CPU之外的资源)内存等资源分配给进程
- CPU中有PC、寄存器们等,关于CPU的结构以及具体的工作方式大家可以网上看看,知道CPU是干啥的玩意就行
- OS把CPU分配给线程
- 把(除CPU之外的资源)内存等资源分配给进程
- 咱们OS在分配资源时:
当然,java内存区域(运行时数据区域)里面的打手(线程)也肯定会有自己的一些私人领域区域(线程私有的内存,私有的就说明生命周期和线程相同,线程活你活,线程凉你凉,方法结束或者线程结束时这三个私有空间的内存自然就跟着回收了),别的进入java内存中的东东或者西西是不能随便访问和乱动的,比如:
- 程序计数器:Program Counter Register(相当于打手房间墙上挂的一个屏幕,上面不间断滚动播放着,这个打手明天去干谁、后天去干哪个对手(这里的干指的是线程执行任务哦))
- 程序计数器是一块较小的内存空间,程序计数器可以看作是当前线程所执行的字节码的行号指示器(字节码(就是.class文件)解释器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令,分支循环跳转线程恢复等基础功能都需要依赖这个计数器来完成)
- 咱们OS在分配资源时把CPU分配线程其余的资源分配给进程,为啥要这样分呢,此时程序计数器的作用就凸显出来了
一个进程中生活着很多个线程,线程们是真正占用CPU资源去运行或者说执行进程中的各个任务呢。而CPU一般是使用时间片轮转的方式让进程中的多个线程们轮询占用的(当前线程CPU时间片用完后要让出CPU,等下次轮到自己时再执行),那下次怎么知道之前线程执行到哪里了呢?其实**程序计数器就是来记录该线程让出CPU资源时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定的地址继续执行**(在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场)- 无论采用两种方式中的哪种退出方式,在方法退出之后都需要返回到方法被调用的位置程序才能继续执行。所以方法返回时可能需要在栈帧中保存一些信息用来帮助恢复他的上层方法的执行状态。但是如果是第一种方式也就是正常退出时调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时栈帧中一般不会保存这个信息。
- 咱们OS在分配资源时把CPU分配线程其余的资源分配给进程,为啥要这样分呢,此时程序计数器的作用就凸显出来了
如果线程正在执行的是一个Java方法那么这个计数器记录的是正在执行的虚拟机字节码指令的地址(下一条指令的地址);如果正在执行的是Native方法那么这个计数器值则为空。- 程序计数器这块内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
- 程序计数器是一块较小的内存空间,程序计数器可以看作是当前线程所执行的字节码的行号指示器(字节码(就是.class文件)解释器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令,分支循环跳转线程恢复等基础功能都需要依赖这个计数器来完成)
- java虚拟机栈,(咱们平时最关心的Java内存区域就是堆和栈,而这里的栈就指的是Java虚拟机栈(具体一点说就是虚拟机栈中局部变量表这一部分)),这个相当于打手的一个储物柜(大超市里面那种分好多小柜子那种),里面的每个小柜子就叫做栈帧(Stack Frame)。
- 栈帧在java虚拟机栈中入栈到出栈刚好对应于这个方法从调用到执行完成的过程。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(大体上可以认为是编译器可知的)
- 在类中A a = new A(),这个引用a就是类的属性了,在方法区
- 在方法中A a = new A(),这个引用a就是在虚拟机的栈帧中
- 那咱们成天说调用方法执行方法调用方法执行方法,这调用方法和执行方法到底有什么玄机呢?,这里就可以研究一下,方法调用!=方法执行
- 方法调用阶段
唯一的任务就是确定被调用的方法的版本(也就是调用的哪一个方法),还没涉及到方法内部的具体运行过程。- 一切方法调用在Class文件里面存储的都只是方法调用中的目标方法在常量池中的符号引用而不是方法在实际运行时内存布局中的入口地址(也就是直接引用),虽然听着low并且使得Java方法需要在类加载期间甚至到运行期间才能确定目标方法的直接引用,但是这个性质确实让Java有着很强的动态扩展能力
- 方法调用阶段
- 程序正在运行的方法一定在栈的顶部。一个线程中的方法调用链可能会很长或者说很多方法都有可能同时处于执行状态(所以在众多的活动线程中只有位于栈顶的栈帧才是有效的),也叫做当前栈帧,与这个当前栈帧相关联的方法叫做当前方法。
- 执行引擎运行的所有字节码指令都只针对当前栈帧进行操作
- 在栈的最底部放main()方法
- 对于类里面的方法,都是第一步先执行main()方法(也就是先把main()方法压入栈),之后再执行其他方法(把其他方法压入栈),所以main()方法是最后一个出栈的,也就是main()执行完毕相当于程序执行结束
- 每个java方法在执行时都会为自己创建一个栈帧,用来存储线程私有的局部变量(表)(
局部变量表又包括基本数据类型和对象的引用,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的)、操作数栈、动态链接、方法返回地址以及常量池引用等。(大家可以理解成为打手用来记录自己哪天打哪个对手,要用的不同的拳击手套、穿的衣服、牙套的不同尺寸大小以及存放的位置等等,说不定扔到哪里去了,找不到不就糟了)局部变量表:局部变量表是一组变量值存储空间,里面存放的是方法参数和方法内的局部变量,也就是编译器可知的八种基本数据类型、对象引用(有可能是咱们自己new出来的句柄,也有可能是指向另一个代表一个对象的句柄或者其他与这个对象有关的地址等)以及returnAddress类型(指向了一条字节码指令的地址)- 在Java程序编译为Class文件时就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量
- 局部变量表的容量以变量槽(Variable Slot,也叫Slot)为最小单位,一个Slot可存放一个32位以内的数据类型(八种基本类型中除了Long 、Double其余六种、reference(表示对一个对象实例的引用)、returnAddress(现在已经由异常代替))
- 对于64位的数据类型虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。其中的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占1个局部变量空间。
- 由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作都不会引起数据安全问题
- 如果线程执行的是实例方法(也就是非static方法),那么局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用(在方法中可以通过this来访问到这个隐含的参数),其余参数按照参数表顺序排列
- 为了节省栈帧空间,局部变量表中的Slot是可以重用的
- 局部变量表所需要的内存空间是在编译期间分配完成的。(也就是说当进入一个方法时这个方法需要在栈帧中占用或者说被分配多大的局部变量表空间以及多深的操作数栈是在编译程序代码时已经完全确定的,并且确定之后会写入到方法表的Code属性之中)。方法运行期间不会改变局部变量表的大小。(也就是说一个
**栈帧需要分配多少内存不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现**。)- 局部变量和类变量不一样,局部变量定义后但是没有赋初始值是不能使用的。而类变量有两次赋初始值的过程,一次在准备阶段赋系统初始值一次在初始化阶段赋予程序员定义的初始值~详情加类加载巴拉巴拉
- 对于64位的数据类型虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。其中的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占1个局部变量空间。
操作数栈:也叫操作栈,是一个LIFO栈。- 和局部变量表一样操作数栈的最大深度也在编译时写入到Code属性的max_stacks数据项中。方法执行的任何时候操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。方法开始开始执行时这个方法的操作数栈是空的,在方法的执行过程中会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈和出栈操作
- 操作数栈的每一个元素可以是任意的Java数据类型。32位数据类型占的栈容量为1而64位占两个栈容量。
动态连接:Java虚拟机栈中的每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接- Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化成为直接引用,这种转化也叫做静态解析
- 另外一部分将在每一次运行期间转化成为直接引用,这部分叫做动态连接。
方法返回地址:
- 栈帧在java虚拟机栈中入栈到出栈刚好对应于这个方法从调用到执行完成的过程。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(大体上可以认为是编译器可知的)
- 本地方法栈。这个是打手在自己私有内存区域中专门开辟的,用来保存登记native方法(当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法)(相当于打手虽然肌肉发达,但是人家可是很细心的哟,人家给自己私有领地中开辟了一个小空间,用来放他借用别人的手套呀、衣服呀这些工具(下面也会再将,因为java有时会调用到C、C++等语言写的其他方法,这些不是你java写的(对于我这个java从业者而言哈)方法而是别人写的方法就是本地方法))
- 本地方法栈与Java虚拟机栈一样也会抛出StackOverflowError和OutOfMemoryError异常。具体见下面赠送的图
- 本地方法栈其实和Java虚拟机栈是一个师门的,学的功夫起的作用差不多,只不过Java虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务;而本地方法栈是为虚拟机借用人家外边的Native方法服务
(图片里面的字大家也可以好好看看,都是正经文字) 买一个图赠送一个图 线程私有的Java虚拟机栈有两种异常状况,如下:
- 方法递归调用肯可能会出现该问题StackOverfloError异常----可以调整参数-xss去调整jvm栈的大小
Notes:大家可能也听过这个PC程序计数器,此处可以解释一下为什么将程序计数器设置为私有的。OS将CPU之外的其他资源分配给了进程,而单独把CPU资源分配给了线程(因为线程是来执行任务的打手,进程又不具体执行任务,CPU给你干啥)。线程们是通过时间片轮转的方式来轮流占用并使用CPU的(java虚拟机的多线程也是通过线程轮流切换并分配处理器(对于多核处理器来说一个处理器就是就可以算作一个内核)执行时间或者叫做时间片的方式来实现的)(那CPU分配给线程们你一个他一个时间片让去按照时间片执行任务,但是线程的任务不一定在指定时间内执行完毕,执行到一半有可能就被CPU喊出去了让另一个线程按照自己的时间片接着执行(任何一个特定的时间一个处理器或者说多核处理器中一个内核都只会执行一条线程中的指令),这是不是线程们在不断切换呀)。比如打手在每个关卡指定时间内打倒不同的敌人就可以闯关,但是打到一半时间到了,CPU得找个线程私有的地方(就是程序计数器)来记录某个线程让出CPU时之前的执行地址,待再次该线程被分配到时间片时该线程就可以从自己私有的计数器存的指定地址继续执行,继续打人闯关。
- 至于说为什么要把这个记录上次结束时线程执行到哪里的地方设置为私有的,我个人觉的可能就是方便或者安全,自己心里的小九九嘛,放自己心里合适。如果有其他原因希望大家评论供我学习一下,灰常感谢。(为了线程相互切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,独立就是为了保证各条线程之间计数器互不影响,独立存储)
- 这里执行的任务,就是java代码指令
- 如果正在执行的是本地方法(人家别人写的方法),那么程序计数器则为空,记录undefined
总体而言,人家这个打手自理能力还是比较强的,人家比赛呀、闯关呀完成后,人家会把自己私有的:
- 用来记录自己要闯关或者比赛的对手、地址等信息的大屏幕,人家会每天自己更新一下、维护一下、擦洗一下,不用别人帮我打扫处理
- 自己存货的有很多小柜子的大储物柜,人家会每天自己更新一下、维护一下、擦洗一下,不用别人帮我打扫处理
- 还有自己私有领地中开辟的一个小空间(用来放他借用别人的手套呀、衣服呀这些工具),人家会每天自己更新一下、维护一下、擦洗一下,不用别人帮我打扫处理
当然,说完了打手(线程)的私有领地,得说说打手们(线程们)可以共享的领地了。(可能不全,希望大家评论区指正,共勉)
-
堆(Heap):又叫GC(Garbage Collection Heap)堆
- 在 Java 中,堆被划分成两个不同的区域:
- 新生代 ( Young ):新生代默认占总空间的 1/3。新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
- 新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收
- 老年代 ( Old ):老年代默认占 2/3。
- 老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法
- 新生代 ( Young ):新生代默认占总空间的 1/3。新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
- 这个领地,一个字,大,两个字,真大,三个字,他很大。不错, 堆是进程中或者说Java虚拟机所管理的内存中最大的一块内存(进程是线程寄存的地方,别忘了哦。)。我愿称堆为打手们的拳台,懂我的意思吧,你不大点都站不下打手们咋打。
- 堆这个领地是在进程创建时分配的(虚拟机启动时创建)
- (堆天生的唯一使命或者说存在的唯一目的就是存放对象实例以及数组)所有实例化出来的对象(类中的方法、常量)以及数组都在队内分配内存
- 这个舞台,打手们刚相互打完,也都没力气了,那舞台上舞台下打出来的汗渍、xue迹、用过的脏牙套、脏毛巾、脏衣服、脏水杯......,这个舞台也算是一个主要的垃圾来源,得让别人帮我打扫处理了,来回收一下相关的垃圾。
- Java堆是垃圾收集器管理的主要区域
- Java虚拟机规范中规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。当前的主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)
- 如果在堆中没有内存完成实例分配且堆也无法再扩展时将会抛出OutOfMemoryError异常
- Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
- 指针:指向对象,代表一个对象在内存中的起始地址
- 优势:速度更快,节省了一次指针定位的时间开销。HotSpot 中采用的就是这种方式。
- 句柄:可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
- 优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
买一个图赠送一个图
- 优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
- 指针:指向对象,代表一个对象在内存中的起始地址
- 在 Java 中,堆被划分成两个不同的区域:
-
方法区
- 民政&局办完事的剩下来的材料废物:那些信息呀、证书呀、照片呀、文字呀啥的都放在这块区域里面(这块不太理解可以看看这篇,保证你恍然大悟玩转~类加载器&双亲委派机制)
- 用于存放已被Java虚拟机加载的类的类信息(构造方法、接口定义等)、常量、静态变量(类变量,也就是static修饰的变量)、运行时的常量池、即时编译器编译后的代码等数据。(现在知道咱从民政&局出来后,原来咱们小胡和言小敏这对夫妻以及其他的新人们被人家民政&局是当作一个一个类的呀,新人办理结婚证时在局子里面涉及到的那些信息呀、证书呀、照片呀、文字呀,都跑哪去了,都放到方法去里面来了)
- 在类中A a = new A(),这个引用a就是类的属性了,在方法区
- 在方法中A a = new A(),这个引用a就是在虚拟机的栈帧中
- 刚开始咱们写一个资源类时,这个类中的static、final、Class类模板信息以及常量池等信息就会存放在线程共享的方法区中
- (常量池中存放着这个类中的属性,也就是你初始化时给赋的字符串等一些值,你赋的其他类型的值,比如int等不算,不会放在常量池中)
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是方法区有个别名叫Non-Heap,就是为了与Java堆区分开来。(在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分:加载的类信息(加载的类信息被保存在元数据区中)+运行时常量池(运行时常量池保存在堆中))
- 运行时常量池(Runtime Constant Pool):属于方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool Table,这个常量池和运行时常量池不一样哦,常量池用于存放编译期生成的各种字面量和符号引用,而这些将在类加载后进入方法区的运行时常量池中存放。此外一般也会把翻译出来的直接引用也存储在运行时常量池中)。
- 运行时常量池相对于Class文件常量池的另外一个特征就是具备动态性(一个特征就是对运行时常量池的每一个字节存储哪种数据没有限制),也并非说是只有预先直入Class文件中常量池的内容在类加载后才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,如String类的intern()方法。
- 当常量池无法再申请到内存时将抛出OutOfMemoryError异常
- 运行时常量池(Runtime Constant Pool):属于方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool Table,这个常量池和运行时常量池不一样哦,常量池用于存放编译期生成的各种字面量和符号引用,而这些将在类加载后进入方法区的运行时常量池中存放。此外一般也会把翻译出来的直接引用也存储在运行时常量池中)。
- Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这块区域内存回收的的目标主要是针对常量池的回收和对类型的卸载
- 当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常
-
🕴JVM 中的常量池共有如下四种:
- Class文件常量池:class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
- 运行时常量池:运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
- 全局字符串常量池:字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
- 基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
老规矩,买一赠一
说到这,大家就知道,
- 我打手(线程)私有的三个区域我自己打扫处理好,不用别人帮我打扫。
- 然后拳台(堆)、方法区(民政&局办完事的剩下来的材料废物啥的)需要专人帮我打扫一下,
- 因为呀,新人办理结婚证时在局子里面涉及到的那些信息呀、证书呀、照片呀、文字呀;舞台上舞台下打出来的汗渍、xue迹、用过的脏牙套、脏毛巾、脏衣服、脏水杯......太多了,只能别人帮我打扫,别人比我专业,自己的私人领地扫不好也没啥大关系,拳台以及民政&局办完事的剩下来的材料废物啥的得专人来处理好。
- 当然,很明显,大部分要回收的还是在堆里面(打手的垃圾占整个回收过程的90%左右哟)
- 因为呀,新人办理结婚证时在局子里面涉及到的那些信息呀、证书呀、照片呀、文字呀;舞台上舞台下打出来的汗渍、xue迹、用过的脏牙套、脏毛巾、脏衣服、脏水杯......太多了,只能别人帮我打扫,别人比我专业,自己的私人领地扫不好也没啥大关系,拳台以及民政&局办完事的剩下来的材料废物啥的得专人来处理好。
🕴heap 和stack 有什么区别:
- 内存分配方式
- 堆的物理地址分配对对象
是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩) - 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快
- 堆的物理地址分配对对象
- 申请方式
- stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间
- heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于Java 需要手动 new Object()的形式开辟
- 申请后系统的响应
- stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
- heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
- 申请大小的限制
- stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(默认值也取决于虚拟内存的大小),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
- heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的, 自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见, 堆获得的空间比较灵活,也比较大。堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
- 申请效率的比较
- stack:由系统自动分配,速度较快。但程序员是无法控制的
- heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
- heap和stack中的存储内容
- stack:栈存放的有局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址, 然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行
- heap:堆存放的是对象的实例和数组。因此该区更关注的是数据的存储。一般是在堆的头部用一个字节存放堆的大小。
- 静态变量放在方法区
- 静态的对象还是放在堆。
当然,这是java内存中的剩余的最后一块领地,还没说呢:
- 直接内存(直接上图,大家看看即可)
- 直接内存并不是虚拟机运行时数据区域的一部分也不是Java虚拟机规范中定义的内存区域,但也会被频繁的使用,它避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
- 直接内存也是有可能导致OutOfMemoryError异常的
- JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样因为避免了在Java堆和Native堆中来回复制数据从而提高了性能。
- 直接内存的分配不会受到Java堆大小的限制,但是既然是内存就会受到本机总内存(RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制
ps.上面不是提到了咱们打手会调用到别人C或者C++写的一些拳套呀、牙套呀等工具(本地方法)等等,这些就是这么实现的,一句话:
- 线程私有的本地方法栈里面发起调用,通过本地接口调用本地方法库(库嘛,就是存别人牙套】拳套的库呀)里面的本地方法,让咱们这个打手使用自己原来没有的东西。
大家有兴趣想了解更详细一点,可以看看下面我画好的框图。
大体上了解java内存之后,你光说专人专人来帮我们打扫拳台(垃圾多)和民政&局剩余垃圾(垃圾少)
- 到底啥样的东西才算垃圾呢?
- 到底是谁来帮咱们打扫呀?
- 什么时候打扫?
- 他咋扫呢?我不信我扫不过他!
下集再见,今天写多了,看都不想看了,这么多,谁想往下一直翻......(白眼一个,言小敏最喜欢的那个翻给小胡的那个白眼)