JVM架构图



Java运行时数据区域
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同
线程共享
堆内存
堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存)
JDK7版本及以前,堆内存通常被分为下面三部分- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永生代(Permanent Generation)
JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),
取而代之是元空间,元空间使用的是直接内存。方法区
HotSpot虚拟机的永久代
方法区主要用于存放已被加载的类信息(构造方法,接口定义)、常量、静态变量、
即是编译器编译后的代码等数据。简而言之,static,final,Class类,运行时常量池都存在方法区中。
方法区还有一块区域叫:运行时常量池
运行时常量池- JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区
的实现为永久代 - JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池
被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
字符串常量池和静态变量在1.7迁出了方法区 - JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆,
运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
- JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区
直接内存(非运行时数据区的一部分)
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,
但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及
处理器寻址空间的限制。个人理解:JVM进程向操作系统申请的一块内存,一部分作为JVM运行时的内存使用,另一部分就是
未被使用的内存,这就是直接内存。
线程私有
虚拟机栈
每个线程都有各自的虚拟机栈,虚拟机栈被分为一个个大小相等的栈帧.
栈帧组成部分如下:
- 局部变量表
局部变量表最重要,局部变量表主要存放了编译期可知的各种基本数据类型
(boolean、byte、char、short、int、float、long、double)、
对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,
也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 - 操作数栈
- 动态链接
- 方法返回值信息
栈帧具体解释:Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,
每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,
都会有一个栈帧被弹出。对象的两种访问定位方式:
- 直接指针,即栈帧中的一个指针直接指向堆中对象
- 句柄访问,即栈帧中的一个指针指向堆中句柄池中的一个句柄,该句柄再指向堆中的对象
- 局部变量表
程序计数器(PC寄存器)
程序计数器是记录下一条指令的地址,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
程序计数器的作用字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,
如:顺序执行、选择、循环、异常处理。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,
只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
本地方法栈
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、
操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间不同的虚拟机,栈空间不一样,hotspot虚拟机中(默认使用的虚拟机),虚拟机栈和本地方法栈合而为一,
统一都叫虚拟机栈。
每一个线程都有各自的虚拟机栈,程序计数器,本地方法栈,多个线程共享堆内存和方法区
了解一下类加载器
类加载的过程
类加载器的种类
- 虚拟机自带的加载器
- 启动类(根)加载器(BootStrapClassLoader),rt.jar中
- 扩展类加载器,jdk/jre/lib/ext目录
- 应用程序(系统类,AppClassLoader)加载器
双亲委派机制
保证安全的机制,向上委派,向下加载
类字节码的加载顺序:将字节码文件交给最顶层的加载器优先加载
具体过程- 类加载器收到类加载的请求
- 将这个请求向上委托给父加载器去完成,一直向上委托,直到启动类加载器
- 启动类加载器检查是否能加载这个类,能加载就结束,使用当前的加载器,否则,抛出异常
通知子加载器进行加载 - 重复步骤3
public class Car { public static void main(String[] args) { Car car = new Car(); Class<? extends Car> aClass = car.getClass(); ClassLoader classLoader = aClass.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); System.out.println(classLoader.getParent().getParent()); } } //输出结果 jdk.internal.loader.ClassLoaders$AppClassLoader@2f0e140b jdk.internal.loader.ClassLoaders$PlatformClassLoader@16b98e56 null //扩展类加载器往上就是null,获取不到加载器,因为上面根加载器是用c++语言写的 //注意,jdk1.9之前,扩展类加载器是EXTClassLoader,1.9版本之后改成了PlatformClassLoader
了解一下Native关键字
- 应用场景
在Thread类中start方法调用过被native修饰的start0方法。 - 底层原理
- 被native修饰的方法会进入线程的本地方法栈,这个方法会去调用底层C语言的库。
- 首先这个方法会去调用JNI,java native interface,本地方法接口。
JNI就会去调用本地方法库,例如C语言库,Python库,可以达到扩展java类的使用。
融合不同的编程语言为java所用。