1.运行时数据区包含什么
-
程序计数器(program counter register)
-
本地方法栈
-
虚拟机栈
-
Java堆
-
方法区
-
直接内存
1.1 Java运行时数据区和Java内存模型的区别
Java内存区域/结构也称之为Java运行时数据区。
Java内存结构和Java内存模型是不
一样的东西。可能有些人在面试的时候会把Java内存模型理解为Java内存结构,然后答到堆,栈,GC垃圾回收,最后和面试官想问的问题相差甚远。实际上一般问到Java内存模型都是想问多线程,Java并发相关的问题。
-
Java内存区域是指Java运行是将数据分区域存储,强调对内存空间的划分。
-
Java内存模型(Java memory model 简称JMM)是定义了线程和主内存之间的抽象关系,即JMM定义了JVM在计算机内存(RAM)中的工作方式。如果要想深入了解Java并发编程,就要先理解好Java内存模型。JMM
2.程序计数器
2.1定义
程序计数器(program counter register)是一块较小的内存空间,并且是线程私有的。他的核心作用:用于存储下一个所要执行的JVM指令的内存地址
,也可以看做是当前线程所执行的JVM字节码的行号指示器。
-
唯一一个无OOM的区域(out of memory)内存溢出异常(OutOfMemoryError)
-
每一个线程都有一个独立的程序计数器
-
如果是native方法,则为空(Ubdifined)????1:如何确保native执行后的位置 2:native方法}
JVM规范中的定义:程序计数器存放的是Java字节码的地址,而native方法的方法体是非Java的,所以程序计数器的值才未定义
Java代码通过JVM类加载
后成为二进制字节码JVM指令。
Java指令执行流程:
-
每一条二进制字节码(JVM指令)通过解释器程序转换为机器码,然后就可以被CPU执行了!
-
当解释器将一条JVM指令转换成机器码后,会想程序计数器递交下一条JVM指令的执行地址。
-
程序计数器在硬件层面是通过寄存器实现的
2.1.1 线程私有
既不会出现并发安全问题。JVM运行数据区中程序计数器,虚拟机栈,本地方法栈是线程私有的,Java堆和方法去市线程共享的。
线程私有数据区域的生命周期与线程的生命周期相同,依赖用户线程的启动、结束 而 创建、销毁。
因为JVM里多线程运行时,(最少也会有main,gc),是通过轮流切换并分配处理器执行时间的方式运行的,在确定的某一时刻,一个CPU(或内核)只有一个线程运行(一个线程中的程序计数器改变),需要保证线程切换后能恢复到正确的执行位置 -- 每条线程一个程序计数器,彼此独立,互不干扰。
2.1.2 JVM、编译器、解释器的含义
Java编译器:
-
将Java源文件(.java文件结尾)编译成字节码文件(.class结尾的2进制文件,是特殊的二进制文件,二进制字节码文件)。这种字节码就是JVM的“机器语言”(JVM能够识别)。javac.exe可以简单看成是Java编译器。
Java解释器:
-
是JVM的一部分,Java解释器用来解释执行Java编译器编译后的程序(.class文件),解释成为具体平台的机器码的程序。java.exe可以简单看成是Java解释器。
-
(英语:interpreter),又译为直译器。是一种电脑程序,能够把高级编程语言一行一会直接转移运行。解释器不会一次把整个程序转译出来,只像一个“中间人”,每次运行程序时都要先转成另一种语言在作运行,因此解释器的程序运行速度比较缓慢。他每转译一行程序叙述就立刻运行,然后再转移下一行,再运行,如此不停的进行下去。
编译成功的程序,可以直接用java.exe随处解释执行了。
当程序需要首次启动和执行的时候,解释器可以首先发挥作用,一行一行直接转译运行,但效率低下。
当多次调用方法或循环体时JIT编译器可以发挥作用,把越来越多的代码编译成本地机器码,之后可以获得更高的效率(占内存)。
JVM:Java virtual machine(Java虚拟机)的缩写。JVM是一种用于计算设备的规范,他是虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
2.2唯一不会出现OOM的区域
在编译时已经确定该线程代码的偏移量最大值,根据该最大偏移量可以分配内存空间:A字节;
或者JVM已确定程序计数器的大小:X字节。
不管是A字节还是X字节,都已经保证程序计数器的最大偏移量≤空间最大值。
另外,在线程运行时,只改变程序计数器的值,不涉及空间的扩展,所以不会存在空间不够用的情况。
2.3 存储的值
对于Java方法是字节码偏移量
对于native方法是undefined
3.虚拟机栈
3.1 定义
-
每个线程运行需要的内存空间,称为虚拟机栈
-
每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
-
每个线程只有一个活动的栈帧,对应着正在运行的方法
栈。每一个方法是一个栈帧。栈帧里面包含,变量,参数,返回值。会出现栈内存溢出。
与程序计数器一样,虚拟机栈也是线程私有,虚拟机栈和线程的声明周期相同。
Java方法执行的内存模型,每个方法在执行的同事都会创建一个栈帧(stackframe),栈帧里面包含本地变量表(局部变量表Local variable),操作数栈,对运行时常量池的引用(runtime constant pool reference),动态链接,方法出口等信息。
每一个方法从调用直至执行完成的过程就对应这样一个栈帧在虚拟机中入栈和出栈的过程。当且仅有一个栈帧在执行。
栈帧用于存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(dynamic liking)、方法返回值和异常分派(dispatch exception)。
栈帧随着方法调用而创建,随着方法执行结束而销毁--无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
异常:
-
线程请求的栈深度大于JVM所允许的深度stackoverflowerror,(方法递归)
-
若JVM允许动态扩展,若无法申请到足够内存outofmemoryerror。(第三方工具,json转化,对象无线递归)
3.2演示
代码:
public class TestStackFrame { // 程序主线程 public static void main(String[] args) { method1(); } // 定义一个执行方法1 private static void method1() { int i = method2(); System.out.println("i = " + i); } // 定义一个执行方法2 private static int method2() { int i = 10; i++; return i; } }
在控制台debug模式上可以看出,主类中的方法在进入虚拟机栈的时候,符合虚拟机栈的特点
问题辨析
-
垃圾回收是否涉及栈内存
-
不需要。因为一个线程一个虚拟机栈是有多个栈帧(方法嵌套顺序)组成的,在方法执行完成之后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制回收内存。
-
-
栈内存的分配越大越好吗?
-
不是。因为电脑的物理内存是固定的,如果增大了虚拟机栈的内存,虽然当前线程可以支持更多的(递归)方法,但是整体上可执行的线程个数据就减少了。
-
-
方法中的局部变量是线程安全的吗?
-
如果局部变量没有脱离方法的作用范围,就是线程安全的变量
-
如果局部变量引用了对象,并且逃离了方法的作用范围,则需要考虑线程安全问题
-
3.3模拟栈内存
-
模拟栈帧过多,方法递归 java.lang.StackOverflowError
-
每个栈帧所占用过大
栈内存比如100K,栈内存溢出的原因:1.栈帧(方法)递归占内存多;2.栈帧(方法)局部变量占内存
代码:
static Integer i = 0; public static void main(String[] args) { try { method1(); } catch (Throwable e) { System.out.println(i); e.printStackTrace(); } } private static void method1() { i++; //long a, z, d, c, e, w, q, r, t, y, u, f, i, h, g,l = 100L; method1(); }
设置虚拟机栈内存参数:-Xss10k
运行结果:java.lang.StackOverflowError
3.4线程运行诊断
如何找到那个线程那个方法占用CPU过高(CPU占用过高诊断)
模拟CPU占用过高,linux诊断是那一个线程?
代码:
//Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程 public class TestThreadCpu { public static void main(String[] args) { for (; ; ) { } } }
-
在linux上运行该程序,java -jar xxx.jar
-
top命令查看那个Java的PID占用CPU过高
-
通过ps命令获取到所有线程id占用CPU的情况:
ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号
-
使用Java里面的
jstack + 进程ID
命令查看运行情况及进程中的线程的nid。刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换,如xxx。在jstack打印日志中找到xxx,就能得到具体是哪一行!
4.本地方法栈
一些代用native
关键字的方法就是需要JAVA调用本地的C或者C++方法。
因为Java有时候没法直接和操作系统底层交互,所有需要用到本地方法。
在执行本地方法的时候,会给其分配一片内存空间。
5.堆
5.1定义
通过new的对象和数组
都会存放在堆内存区域中【几乎所有的对象实例都在这里分配内存】。
Java堆是被所有线程共享的一块内存区域。
Java堆是Java虚拟机所管理的内存中最大的一块。
5.2特点
-
所有线程共享,堆内存中的对象都需要考虑线程安全问题
-
有垃圾回收机制,Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为
GC堆
(garbage) -
Java堆内存可分配;
-Xmx -Xms:
JVM初始分配的堆内存由-Xms
指定,应用程序(不是JVM)最大的堆内存由-Xms
指定。64位的操作系统上,初始化堆大小默认是物理内存的1/64
,16G内存主机也就是256M
。
Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xms -Xms
设定)。
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError heap
异常。
JVM中堆默认初始值和最大值?
在window上查看xmx和xms默认值:
命令:java -XX:+PrintFlagsFinal -version | findstr /i "HeapSize"
在Linux上查看xmx和xms默认值:
命令:java -XX:+PrintFlagsFinal -version | grep -iE "HeapSize"
MaxHeapSize:最大堆大小,就是Xmx的默认值
InitialHeapSize:初始化最小堆大小,就是Xms的默认值
默认值和Java的版本有关。
这是Java8的文档中关于Default Heap Size的描述:点击这里
hotspot虚拟机的默认堆大小如果未指定,他们是根据服务器物理内存计算而来的
client模式下,JVM初始和最大堆大小为:在物理内存达到192MB之前,JVM最大堆大小为物理内存的一半,否则,在物理内存大于192MB,在到达1GB之前,JVM最大堆大小为物理内存的1/4,大于1GB的物理内存也按1GB计算,举个例子,如果你的电脑内存是128MB,那么最大堆大小就是64MB,如果你的物理内存大于或等于1GB,那么最大堆大小为256MB。Java初始堆大小是物理内存的1/64,但最小是8MB。
server模式下:与client模式类似,区别就是默认值可以更大,比如在32位JVM下,如果物理内存在4G或更高,最大堆大小可以提升至1GB,,如果是在64位JVM下,如果物理内存在128GB或更高,最大堆大小可以提升至32GB。
在linux中查看运行中Java程序的Xmx和Xms的值:
-
首先查看Java进程的PID
命令:ps -ef | grep java
-
根据PID查看当前设置值
命令:sudo jcmd PID VM.flags
5.3堆内存溢出
代码
public class TestHeapOutMemoryError { byte[] b = new byte[1024 * 1024 * 10]; public static void main(String[] args) { List<TestHeapOutMemoryError> list = new ArrayList<>(); try { for (; ; ) { list.add(new TestHeapOutMemoryError()); } } catch (Throwable e) { System.out.println("list.size() = " + list.size()); e.printStackTrace(); } } }
运行结果:
list.size() = 315
java.lang.OutOfMemoryError: Java heap space
堆内存诊断:
jps
jmap
jconsole
jvisualvm
5.其他
所有new的实例对象都是在堆内存吗?
对象不一定在堆上分配内存,这是不对的。
正常情况下,对象是要在堆上进行内存分配的,但是随之编译器的优化技术的成熟,虽然虚拟机规范是这样要求的,但是具体实现上会有些差别的。
如HotSpot虚拟机引入了JIT优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就有可能通过标量替换来实现栈上分配,而避免堆上分配内存。浅谈JVM中的逃逸分析
6.方法区
6.1定义
JVM1.6的方法区的内容:
JVM1.8+的方法区调整为元空间,内部的常量池,class,classloader存储在直接内存中。stringtable移到堆内存区域中。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
它用于存储JVM加载的类信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
永久代:
JDK8以前,通常吧方法区称呼为永久代(Permanent Generation)
,或将俩者混为一谈。
本质上这俩这并不是等价的,因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得虚拟机的垃圾收集器能够像管理Java堆一样管理这部分内存,省的专门为方法区编写内存管理代码的工作。
6.2内存溢出
-
1.8以前是
永久代
内存溢出异常 -
1.8以后是
元空间
内存溢出异常
模拟方法区内存溢出:
import jdk.internal.org.objectweb.asm.ClassWriter; import jdk.internal.org.objectweb.asm.Opcodes; import java.util.ArrayList; import java.util.List; /** * 模拟方法区 内存溢出 ~ * -XX:MaxMetaspaceSize=10m8+:设置元空间大小 * idea 设置 VM 参数,切勿设置成args的参数 * * @author <发哥讲Java-694204477@qq.com> * @version 1.0 * @date 2022-03-08 17:08 */ public class TestMethodAreaError extends ClassLoader { public static void main(String[] args) throws Exception { int j = 0; try { TestMethodAreaError test = new TestMethodAreaError(); for (int i = 0; i < 1000000; i++, j++) { // ClassWriter 作用是生成类的二进制字节码 ClassWriter cw = new ClassWriter(0); // 版本号, public, 类名, 包名, 父类, 接口 cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 返回 byte[] byte[] code = cw.toByteArray(); // 执行了类的加载 test.defineClass("Class" + i, code, 0, code.length);// Class 对象 } } finally { System.out.println(j); } } }
运行结果:8531;Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
idae调试记录
大坑,调试了一下午加半晚上。
刚开始一直设置到了args的参数里面,发现-XX:MaxMetaspaceSize=10m设置错了。
回到家里之后,设置为了中文,然后才发现参数设置错了,应该是设置在VM里面。
7.直接内存
-
属于操作系统,常见于NIO操作是,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受JVM内存回收管理
基本阻塞式文件读写流程:
使用DirectBuffer直接内存缓存区:
直接内存是操作系统和Java代码都可以访问的一块区域
,无需将代码从系统内存复制到Java堆内存,从而提高了效率。
Java中直接内存的使用和释放
Unsafe类
-
Java中用unsafe调用本地allocateMemory()方法获取直接内存
-
ava中用unsafe调用本地freeMemory()方法释放内存
ByteBuffer->DirectByteBuffer类
-
ByteBuffer使用allocateDirect(内部初始化DirectByteBuffer对象)方法分配一个新的直接内存缓存区。
-
ByteBuffer的内部实现使用了Cleaner(需饮用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存。
//ByteBuffer类: //通过ByteBuffer申请1M的直接内存 // ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M); // allocateDirect 实现: public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } //DirectByteBuffer类: DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); //申请内存 } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象 att = null; }
这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用
的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存
public void clean() { if (remove(this)) { try { this.thunk.run(); //调用run方法 } catch (final Throwable var2) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (System.err != null) { (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); } System.exit(1); return null; } }); }
对应对象的run方法
public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); //释放直接内存中占用的内存 address = 0; Bits.unreserveMemory(size, capacity); }
笔记参考文档
(https://nyimac.gitee.io/2020/07/03/JVM学习/)
笔记参考文档-路飞(https://csp1999.blog.csdn.net/article/details/119856919)
JVM内存区域_廖志伟
(https://blog.csdn.net/java_wxid/article/details/121624260)