传送门 【JVM】一、JVM体系结构 【JVM】二、JVM垃圾收集器 【JVM】三、JVM内存溢出问题分析查看 【JVM】四、JVM优化-GC调优
一、What is JVM?
Java Virtual Machine(Java虚拟机),是虚拟出来的一个“操作系统”,在这个“操作系统”里面运行java程序的class字节码文件; JVM启动之后,在我们的电脑上或者服务器上表现出来的就是一个java进程,这个进程里面运行的是我们的Java程序; 这个JVM本身是C语言开发的,并且不同的电脑操作系统是不同的版本,比如针对windows有windows的jvm,针对Linux有Linux的jvm;
1、 JDK、JRE、JVM 之间的关系
来自官方:https://docs.oracle.com/javase/8/ 我们安装的JDK中已经包含了JVM,在我们安装目录下: C:\Program Files\Java\jdk1.8.0_101\bin\java.exe /usr/local/jdk1.8.0_101/bin/javajava.exe 和 java ,我们认为就是jvm,因为用这个命令工具启动运行就得到一个运行的jvm虚拟机; JDK安装好之后,在电脑上只是一堆静态的文件,当运行java.exe 或 java后就在当前电脑上启动了一个jvm虚拟机,该jvm虚拟机里面运行的是我们的程序;
- 我们的Test.java 文件是按照Java编程语言的语法规则编写的源码文件,是高级语言,人类可以阅读;
- javac --> Test.class 文件是Java编译器编译后的字节码文件,该文件不便于人类阅读,但是JVM虚拟机能识别和执行,该.class文件有它自己的格式,该格式是虚拟机规范所定义的;
- JVM的价值所在就是将Test.class 文件的字节码翻译为具体操作系统及硬件的指令,以便于执行;
2、 其他运行在jvm的语言
-
Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,运行在Java虚拟机上; -
Scala是一门多范式编程语言,运行在Java虚拟机上; -
Kotlin是一种在Java虚拟机上运行的静态类型编程语言; 这些能在JVM上运行的编程语言都是把编写好的源代码编译成JVM能识别的.class字节码文件,然后再JVM上运行; -
那么这些语言需要做什么事情呢? 需要实现一个编译器,通过该编译器把源代码文件编译成JVM能识别的字节码文件即可;
二、JVM体系结构
1、类加载子系统
在Java虚拟机中,负责查找并装载类的部分称为类加载子系统,类加载子系统用于定位和加载编译后的class文件;(jar包、某个磁盘目录下,甚至在网络上)
(1)类加载的步骤
(2)类加载的时机
Java虚拟机规范并没有强制约束什么时候加载类,是交给具体的虚拟机实现自己去把握的,虚拟机有一些实现,比如Hotspot虚拟机是我们常用的java虚拟机,还有JRockit、J9、TaobaoVM,一般情况下类的加载是按需加载的,需要使用的时候才加载;
(3)类加载器
通过一个类的全限定名来获取描述此类的二进制字节流; 类加载器主要实现类的加载;
(4)双亲委派模型
- Bootstrap ClassLoader启动类加载器(C++ 实现,是虚拟机的一部分);
- Extension ClassLoader 扩展类加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
- Application ClassLoader 应用程序加载器(Java实现)
- Custom ClassLoader 自定义类加载器(java实现,用户自己定义的)
除了顶层的启动类加载器之外,其他类加载器都有自己的父类加载器;
(5)类加载器工作过程
如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器,只有父类无法完成加载时子类加载器才会尝试加载;
2、JVM内存部分(运行时数据区)
(1)、数据区域划分
JVM在运行时会把它所管理的内存划分为若干不同的数据区域,总共是5个区域,宏观上划分为线程私有数据区、线程公有数据区 两 大块:
(1.1)线程私有数据区;
1.1.1 程序计数器
- 1、记录程序执行位置、行号;
- 2、一块很小的区域;
- 3、线程私有;
- 4、不存在OutOfMemoryError;
- 5、无GC回收;
1.1.2 虚拟机栈
虚拟机栈是采用了一种栈的数据结构,入口和出口只有一个,分为入栈和出栈,先进后出;虚拟机栈主要是执行方法; A方法调用B方法,B方法调用C方法,(A–> B–> C) 方法执行就是压栈,方法执行结束就出栈
局部变量表: 是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量; 操作数栈: 也叫操作栈,它是一个先进后出的栈 (FILO),当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是入栈和出栈操作,一个完整的方法执行期间往往包含多个这样入栈/出栈的过程; 动态链接: 一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池,所以需要在运行时动态将这些符号引用转化为直接引用; 返回地址: 方法不管是正常执行结束还是异常退出,需要返回方法被调用的位置; 1、线程私有; 2、方法执行会创建栈帧,存储局部变量表等信息; 3、方法执行入虚拟机栈,方法执行完出虚拟机栈; 4、栈深度大于虚拟机所允许StackOverflowError; 5、栈需扩展而无法申请空间OutOfMemoryError(比较少见); 6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的; 7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M; 8、该区域不会有GC回收; JVM参数配置参考: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
1.1.3 本地方法栈
- 1、与虚拟机栈基本类似;
- 2、区别在于本地方法栈为Native方法服务;
- 3、Sun HotSpot将虚拟机栈和本地方法栈合并;
- 4、有StackOverflowError和OutOfMemoryError(比较少见);
- 5、GC不会回收该区域;
- 栈的OutOfMemoryError溢出一般是在多线程条件下可能会产生,建立过多的线程,每个线程的运行时间又比较长,可能产生栈的OutOfMemoryError溢出;
在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配时,虚拟机抛出StackOverFlowError的错误异常;
1.1.4 线程私有部分的整体特征:
是线程私有的,随着线程执行结束而结束(JVM就销毁了虚拟机栈里面的栈帧),是比较有规律的,问题会少一些,出问题比较多的是线程共享的部分,也就是堆和方法区(元空间)
(1.2)线程公有数据区;
1.2.1 方法区(元空间)
方法区(jdk 1.7后合并到了堆) 方法区在JDK1.8称为元空间(Metaspace),元空间与堆不相连,但与堆共享物理内存; 方法区(元空间)的特点
- 1、线程共享;
- 2、存储 类信息、常量、运行时常量池、静态变量、即时编译器编译后的代码等数据;
- 3、HotSpot虚拟机上将方法区叫永久代;(1.7及之前的版本)
- 4、垃圾收集很少光顾该区域(无GC回收);
- 5、方法区通过-XX:MaxPermSize设置最大值;(1.7及之前的版本)
(元空间:1.8是-XX:MaxMetaspaceSize=48m) - 6、空间不够分配时OutOfMemoryError;
测试元空间的不过偶分配的异常参数
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseCompressedClassPointers -XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m
第一个参数用于打印GC日志; 第二个参数用于打印对应的时间戳; 第三个参数-XX:-UseCompressedClassPointer表示在Metaspace中不要开辟出一块新的空间(Compressed Class Space),如果开辟这块空间的话,该空间默认大小是1G,所以我们关闭该功能,此时再设置Metaspace的大小; 方法区/元空间溢出 1、方法区也称永久代(1.7及之前的版本); 2、方法区存放class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,比如通过反射大量生成动态类填充该区域即会发生内存溢出; JDK1.6及之前:有永久代,常量池在方法区; JDK1.7:从某个版本开始已去除永久代,常量池1.7放入堆中; JDK1.8及之后:无永久代,常量池1.8在元空间; 在jdk1.7及jdk1.8中不会报OutOfMemoryError:PermGen space; 1.8元空间设置大小: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
1.2.2 堆
- 1、线程共享;
- 2、内存中最大的区域;
- 3、虚拟机启动时创建;
- 4、存放所有实例对象或数组;
- 5、GC垃圾收集器的主要管理区域;
- 6、可分为新生代、老年代;
- 7、新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor = 8:1:1
- 8、可通过-Xmx、-Xms调节堆大小;
- 9、无法再扩展OutOfMemoryError
java.lang.OutOfMemoryError: Java heap space
堆分代思想
- 新生代
Eden空间 From Survivor空间 To Survivor空间 - 老年代
不再划分 - 方法区(元空间)
永久代 (jdk<1.8) 元空间 (jdk>=1.8)
Java堆溢出
不断创建对象又不释放,当对象到达一定数量,无堆空间将产生堆内存溢出; 内存泄漏: GC Roots到对象之间有可达路径而无法收集; 内存溢出: GC Roots到对象之间无可达路径,可以被收集,但对象还需要存活着,此时可根据物理机内存适当调大虚拟机堆参数-Xms、-Xmx,分析代码是否对象生命周期是否过长、对象是否持有状态时间过长;
堆中对象的创建过程
-
当通过 new 创建对象时,首先检查这个new指令的参数是否能在元空间中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,执行相应的类加载; 类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确定),在堆的空闲内存中划分一块区域(‘指针碰撞《内存规整》’或‘空闲列表《内存交错》’的分配方式); -
由于堆中分配内存非常频繁,为了避免多个线程同时分配堆内存时的冲突,虚拟机采用CAS和失败重试方式保证操作的线程安全,同时虚拟机还有另一套设计就是把每个线程分配堆内存的动作隔离开,即每个线程预先在堆中分配一块内存,称为线程分配缓冲(TLAB->Thread Local Allocation Buffer),线程先在自己的TLAB上分配,分配完了再CAS同步,这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全; -
内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。 执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成;
对象的内存布局
在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)
-
对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’;第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以; -
实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的); -
对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍;
对象的访问
使用对象时,我们是通过栈上的 reference 引用来操作堆上的具体对象; Sun Hotspot虚拟机使用直接指针访问具体对象;
|