最近几天又重新拜读了一下周志明的《深入理解 java 虚拟机》一书,这本书可真的是经典,时隔一年再读又是另外一番感悟。鉴于上次匆匆读完后没做任何笔记导致好像遗忘速度挺快的,所以这次就好好做一下笔记吧。(本书基于 jdk1.7,1.8 后版本变化好像有点大,所以现在就做笔记的过程中也跟 jdk1.8 中 jvm 的做下对比吧,与时俱进...)
Java 内存区域
本书中讲到的内存区域有五块
- 本地方法栈(Native Method Stack - 线程私有)
- 虚拟机栈(VM Stack - 线程私有)
- 方法区(Method Area - 线程共享)
- java 堆(Heap - 线程共享)
- 程序计数器(Program Counter Register - 线程私有)
几个概念问题
- 解释方法区和永久代
方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT 编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。而永久代是 Hotspot 虚拟机特有的概念,是方法区的一种实现,别的 JVM 都没有这个东西。方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。
- 新生代和老年代
HotSpot 虚拟机堆内存被分为新生代和老年代,对堆内存进行分代管理,新生代进一步可以划分为 Eden 空间,From Survivor 空间、To Survivor 空间。
jdk1.8 永久代被元空间所替代了。
如上图所示,jdk1.8 后内存区域只有四块了,去除了方法区,其中的一部分移到堆中(比如常量池),另外的直接移到外部内存中了。
各内存区域介绍
虚拟机栈
Java 虚拟机栈是线程私有的,它的生命周期与线程相同。它描述的是 java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和 returnAddress 类型。注意:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
如果在动态扩展内存的时候无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别:虚拟机栈为虚拟机执行 java 方法服务,本地方法栈则是为虚拟机使用到的 Native 方法服务。
和虚拟机栈出现的异常一样。
方法区(jdk1.8 废弃,被元空间取代)
方法区也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。相对而言,垃圾收集行为在这个区域是比较少出现的,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
java 堆
java 堆是被线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一目的是存放对象实例(当然也不是绝对,还有栈上分配等)。java 堆是垃圾收集器管理的主要区域,由于现在收集器基本都是采用的分代收集算法,所以 java 堆还可以细分为:新生代、老年代。
实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。
如果堆上没有内存进行分配,并无法进行扩展时,将会抛出 OutOfMemoryError 异常。
程序计数器
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。它属于线程私有的内存。如果线程正在执行一个 java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的事 Native 方法,这个计数器值为空。
此内存区域是唯一一个没有规定“内存溢出”情况的区域。
直接内存
直接内存是本地物理机的内存,并不是虚拟机运行时的数据区的一部分。也不是 java 虚拟机规范中定义的内存区域。由于 jdk1.4 中加入了 NIO 类,引入了一种基于通道(Channel)和缓冲区(Buffer)的 I/O 方式,所以这部分内存也被频繁的使用。它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储再 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。因为避免了再 Java 堆和 Native 堆中来复制数据,所以能在一些场景中显著提高性能。
此内存区也可能导致 OutOfMemoryError 异常出现
下面几种生成对象时的内存情况
int i =10
一个方法对应一个栈帧,方法中的基本数据类型变量直接在栈帧中分配。如果是 static、final 类型的基本数据类型则存储在运行时常量池中,和 String 一样。- Object o1 = new Object();,对象引用(Object o1)存储在栈帧中,但是对象数据(new Object())存储在 java 堆中,对象类型数据(Class 等信息)存储在方法区中。
- String s1 = new String("abcd");,使用 new 声明的对象,对象引用(String s1)存储在栈帧中,对象数据(new String(“abcd”))存储在 java 堆中,字符串值(“abcd”)存储在运行时常量池中。
- String s2 = “abc”,对象引用(String s2)存储在栈帧中,字符串值(“abc”)存储在运行时常量池中。
内存溢出异常(OutOfMemoryError)
此异常一般产生的原因:由于 JVM 内存过小,产生了过多的垃圾,并且 gc 回收后还不够用就会产生该异常。
- 内存中加载的数据量是否过于庞大,比如一次从数据库取出大量数据
- gc 是否回收异常。比如集合类有对对象的引用,但是使用完后没有正常的清空,而 gc 无法回收,则内存越来越大。
- 代码中有死循环产生大量对象。
- 参数的内存值设定过小。
增加 JVM 物理内存使用量,VM Args: -Xms256m -Xmx512m
,表示 JVM 分配的堆内存最小为 256MB,最大为 512MB。
栈溢出异常(StackOverflowError)
如果 java 栈的栈深度大于 JVM 允许的深度,就会抛出该错误。如使用递归的方式不同的调用某个方法就会引起该异常(上面所说,每个方法调用执行时就会在调用栈上分配一个栈帧, 这个栈帧包含引用方法的各种信息)。
相关指令:VM Args: -Xss256k
,表示 JVM 分配的栈容量为 256KB。
参考
https://www.cnblogs.com/zhouyuqin/p/5161677.html
https://segmentfault.com/a/1190000016694247
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于