2.1 程序计数器
Java源代码的执行流程:
- JIT Compiler将Java源代码编译为JVM指令,即二进制字节码
- Interpreter将二进制字节码解释为CPU可以执行的机器码
- CPU执行机器码
程序计数器作用:记住下一条jvm指令的执行地址,供Interpreter访问
程序计数器实现:是通过寄存器实现的
特点:
- 程序计数器是线程私有的,每个线程使用一个单独的程序计数器
- 不会出现内存溢出
2.2 虚拟机栈
底层知识:
-
栈:每个线程运行所需要的内存空间
-
栈帧:每个方法运行时需要的内存
-
工作流程:
-
线程中每调用一个方法就会创建一个栈帧,用来记录参数、局部变量、返回地址等信息,并将其压入该线程所操作的栈空间
-
在该方法体内每调用一个方法,线程就会为其创建一个栈帧并压入栈空间,直至栈空间溢出
-
当方法执行完毕后会将其所属的栈帧出栈,释放其所占用的栈空间
-
虚拟机栈:
- 每个线程运行时所需要的内存,称为虚拟机栈(Java Virtual Machine Stacks)
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧(栈顶位置的栈帧),对应当前正在执行的那个方法
相关问题辨析:
-
Q:垃圾回收是否涉及栈内存?
A:不需要,栈帧每次出栈后内存都会自动释放,完全不需要GC去管理
-
Q:栈内存分配越大越好吗?
A:并不是。由于物理内存是固定的,栈内存分配的越多,可同时执行的线程数反而会越少。举个例子,现有500M内存,当栈内存为1M时,可同时执行的线程为500个;而当栈内存为2M时,可同时执行的线程就仅剩250个了。建议使用默认的栈内存大小即可,在有特殊需求时再根据实际应用场景进行适应性调整。
补充知识:栈内存的分配由虚拟机中的参数来指定,其中一些平台的虚拟机提供了默认参数:
- Linux/x64(64-bit):1024KB
- macOS(64-bit):1024KB
- Oracle Solaris/x64(64-bit):1024KB
- Windows平台的栈内存空间的默认参数取决于虚拟内存大小
-
Q:方法内的局部变量是否线程安全?
A:不一定。如果方法内局部变量没有逃离方法的作用范围,则线程安全(局部变量作为其他方法的参数或当前方法的返回值时,都算逃离了当前方法的作用范围)
补充知识:看一个变量是否是线程安全的,主要看它是多个线程共享还是被某个线程所独占。共享则线程不安全,独占才是线程安全的(一般static关键字声明的变量线程都不安全)
栈内存溢出:
- 两种情况导致栈内存溢出
- 栈帧过多导致栈内存溢出(尤其是不合理的递归程序设计)
- 栈帧过大导致栈内存溢出(不常见)
- 报错信息:
java.lang.StackOverflowError
- 解决方法:VM Options里面通过
-Xss
指定一个较大的内存空间
线程运行诊断:
-
案例1 CPU占用过多
- 用top命令定位哪个进程对CPU占用过高
- 使用ps命令定位到具体线程:
ps H -eo pid,tid,%cpu | grep [top命令查到的pid]
- 使用jdk中的工具jstack查看到底哪个线程出问题:
jstack 进程id
- 将查到的线程id转为十六进制后与jstack中的线程id相比对,定位到具体类中那个占用cpu资源的方法
-
案例2 程序运行很长时间没有结果(线程死锁问题)
依旧是使用jstack工具定位问题:
jstack 进程id
2.3 本地方法栈
本地方法栈(Native Method Stacks),也就是用native关键字修饰的方法,使用C/C++实现,方法偏底层,如线程调用、拷贝之类的方法
2.4 堆
注意:
2.4节之前的内容都是线程私有的部分,各个线程之间相互独立,相互影响的部分很少。从2.4节开始,涉及到的内容在多线程的应用场景下都需要考虑线程安全问题
堆(Heap):
通过 new
关键字创建的对象都会使用堆内存
特点:
- 线程共享,一般堆中对象都需要考虑线程安全问题
- 有垃圾回收机制
堆内存溢出:
-
报错信息:
java.lang.OutOfMemoryError: Java heap space
-
解决方法:VM Options里面通过
-Xmx
指定一个较大的内存空间
堆内存诊断:
- jps工具:查看当前系统中有哪些java进程,并显示对应的进程id
- jmap工具:查看某时刻堆内存占用情况
jmap -heap 进程id
- jconsole工具:图形界面,多功能的检测工具,能实现连续检测
- jvisualvm工具:以可视化的形式展示虚拟机,比jconsole工具更高级
2.5 方法区
方法区在哪里:
在《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或进行压缩”,但对于HotSpot虚拟机而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是和堆分开
所以,方法区是一块独立于Java堆的内存空间
方法区的基本理解:
- 方法区(Method Area)与堆(Heap)一样,是各个线程共享的内存区域
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的
- 方法区的大小,跟堆空间一样,可以选择默认大小或者手动扩展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,就会抛出内存溢出错误:Java.lang.OutOfMemoryError:MetaSpace(JDK1.8及以后) 或Java.lang.OutOfMemoryError:PermGen space(JDK1.8以前)
- 关闭虚拟机就会释放整个方法区内存
HotSpot中方法区的演进:
- 在JDK7之前,习惯上把方法区称为永久代(PermGen space),JDK8开始,使用元空间(MetaSpace)替换了永久代
- 本质上,方法区和永久代并不等价,仅是对Hotspot而言
- 到了JDK1.8,完全废弃了永久代的概念,改用为在本地内存中实现的元空间代替
- 元空间的本质和永久代类似,都是对JVM规范中的方法进行实现,不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用了本地内存
- 永久代和元空间二者不仅仅是名字变了,内部结构也调整了
- 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常
方法区空间大小调整:
方法区默认不设置上限,所以无法控制大小。若想控制方法区的大小,可以在VM Option中写上 -XX:MaxMetaspaceSize=8m
(JDK1.8以下要写成-XX:MaxPermSize=8m
),也就是为其指定8M的内存空间
方法区内存溢出实际场景:
- spring使用Cglib动态字节码技术
- mybatis使用Cglib动态字节码技术
都会在运行期间产生大量的类,容易导致方法区内存溢出
方法区常量池(Constant Pool):
方法区中的二进制字节码中包含了以下信息:
- 类基本信息
- 常量池信息(作用:给虚拟机指令提供常量符号,方便解释器找指令)
- 类方法定义
- 虚拟机指令
常量池概念:常量池其实就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池的概念:常量池是*.class文件中的,当该文件被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
-
重要组成部分——StringTable(串池)
介绍:底层数据结构为哈希表(hashTable),排除重复元素,长度固定并且不能扩容
特性:
- 常量池中的字符串仅仅是符号,第一次用到时才会变为对象(字符串延迟加载)
- 利用串池的机制,可以避免重复创建字符串对象
- 字符串变量(String对象)拼接的原理是StringBuilder(jdk1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用
intern()
方法,主动将串池中还没有的字符串对象(String对象,变量,在堆区不在元空间)放入串池,并返回串池中的对象- jdk1.8将这个字符串对象尝试放入串池,如果有则不放入,如果没有则放入串池,返回串池中的对象
- jdk1.6将这个字符串对象尝试放入串池,如果有则不放入,如果没有会把此对象复制一份,放入串池,并返回串池中的对象
性能调优:
由于StringTable底层基于HashTable,也就是哈希表。哈希表长度有限,当常量字符串使用过多时就会由于频繁发生哈希碰撞而导致哈希表性能大打折扣
因此,在处理大批字符串常量的应用场景下,StringTable性能调优就很有必要了
调优方法:
-XX:StringTableSize=桶个数(默认60013)
附加知识:StringTable为啥使用HashTable的底层实现,为啥不直接存?
- 如果字符串常量中有很多重复的值时,使用HashTable可以节省大量内存空间
2.6 直接内存
直接内存(Direct Memory):
常见于NIO操作时,用于数据缓冲区;分配回收成本高,但读写性能高;不受JVM内存回收管理
直接内存的分配与释放原理:
- 通过JVM内部的
Unsafe
(通过反射创建对象)对象进行分配与释放,Unsafe内部使用Native方法实现 - 通过
allocateMemory()
分配一块指定大小的内存空间,并通过setMemory()
设置初值 - 通过
freeMemory()
释放分配的内存
NIO中如何分配与释放直接内存:
- ByteBuffer的实现类内部使用了Cleaner(虚引用)来监测ByteBuffer对象
- 一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的
clean()
方法调用freeMemory()
来释放直接内存