JVM探究
一个进程对应一个JVM实例
面试常见:
●请你谈谈你对JVM的理解? java8虚拟机和之前的变化更新?
●什么是OOM(内存溢出),什么是栈溢出StackOverFlowError? 怎么分析?
●JVM的常用调优参数有哪些?
●内存快照如何抓取,怎么分析Dump文件?
●谈谈JVM中,类加载器你的认识
JRE:java开发环境,包含了JVM
2.JVM的体系结构
一个Java程序运行起来就是一个进程,一个进程对应一个JVM实例,一个JVM实例就有一个运行时数据区,一个进程只有一个方法区和堆,进程里的多个线程共享这两个区,每个线程有自己的程序计数器、本地方法栈,虚拟机栈。
百分之99的JVM调优都是在方法区和堆(99%是堆)中调优,Java栈、本地方法栈、程序计数器是不会有垃圾存在的。
3. 类加载器
类是模板,是抽象的,类实例化得到的对象是具体的。所有的对象反射回去得到的是同一个类模板。
JVM中提供了三层的ClassLoader:
Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
AppClassLoader:主要负责加载应用程序的主函数类
4.双亲委派机制
看这一篇
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了?
类加载器收到类加载的请求 Application 将这个请求向上委托给父类加载器(ExtClassLoader)去完成,一 直向上委托,直到启动类加载器 Boot
启动加载器检查是否能够加载当前这个类,能加载就结束, 使用当前的加载器,否则, 抛出异常,通知子加载器进行加载
重复步骤3
举例:程序员自己写了一个String类,类加载器
Class Not Found异常就是这么来的
Null:Java调用不到。Java早期的名字:C+± - Java = C++:去掉繁琐的东西,指针,内存管理~
Java语言保留了C的接口,这些方法就是用native(本地)修饰的,java通过native方法调用操作系统的方法
6.native
native:
- 凡是带了native关键字的,说明java的作用范围达不到了(会去调用底层c语言的库)
- 会进入本地方法栈
- 调用本地方法本地接口 JNI (Java Native Interface)
- JNI作用:开拓Java的使用,融合不同的编程语言为Java所用,最初: C、C++
- Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
- 它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法
- 在最终执行的时候,加载本地方法库中的方法通过JNI
- 例如:Java程序驱动打印机,管理系统,掌握即可,在企业级应用比较少
private native void start0();
调用其他接口:Socket… WebService … http~
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
Native Method Stack
它的具体做法是Native Method Stack 中登记native方法,在 ( Execution Engine ) 执行引擎执行的时候加载Native Libraies。【本地库】
7.PC寄存器
程序计数器: Program Counter Register
- 每个线程都有一个程序计数器,是线程私有的,生命周期与线程的生命周期保持一致,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计
- 没有垃圾回收,不会有oom
8.方法区 Method Area
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
- 方法区是不存在堆中的
方法区主要存放的是 Class,而堆中主要存放的是实例化的对象
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
也就是:
static、final、Class、常量池
-
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
-
多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次。
-
方法区在JVM启动的时候被创建,并且它的实际物理内存空间和Java堆区一样都可以是不连续的。
-
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
-
方法区是接口,元空间或者永久代是方法区的实现
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:
- java.lang.OutofMemoryError:PermGen space(JDK7之前)
或者 - java.lang.OutOfMemoryError:Metaspace(JDK8之后)
举例说明方法区 OOM
- java.lang.OutofMemoryError:PermGen space(JDK7之前)
-
加载大量的第三方的jar包
Tomcat部署的工程过多(30~50个)
大量动态的生成反射类
关闭JVM就会释放这个区域的内存。
9.栈
栈:数据结构
程序 = 数据结构+算法︰持续学习~
程序 = 框架+业务逻辑︰吃饭~
栈:先进后出、后进先出,桶
队列:先进先出( FIFO : First Input First Output )
为什么main()先执行,最后结束~
栈:栈内存
栈:栈内存,主管程序的运行,生命周期和线程同步
栈存放的内容:8大基本数据类型+ 对象引用+实例的方法
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)为基本单位存储的
- 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
一个方法的执行对应一个栈帧的入栈,一个方法的执行结束对应一个栈帧的出栈 - 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
为什么main方法先执行,最后结束
线程结束,栈内存也就释放了,对于栈来说,不存在垃圾
StackOverFlowError:
栈、堆、方法区的交互关系
- Person 类的 .class 信息存放在方法区中
- person 变量存放在 Java 栈的局部变量表中
- 真正的 person 对象存放在 Java 堆中
10、三种JVM
- Sun公司HotSpot java Hotspot™64-Bit server vw (build 25.181-b13,mixed mode)
- BEA JRockit
- IBM 39 VM
我们学习都是:Hotspot
11.堆Heap
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
主要用于存放Java类的实例对象
堆内存分区(重要)
堆内存中还要细分为三个区域:
- 新生区(伊甸园区) 又被划分为Eden区和Survivor区
- 养老区 Old/Tenure generation space
- 永久区 Permanent Space
为什么要把Java堆分代?不分代就不能正常工作了吗?
- 经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
- 新生代:有Eden、两块大小相同的Survivor(又称为from/to,s0/s1)构成,to总为空。
- 老年代:存放新生代中经历多次GC之后仍然存活的对象。
- 其实不分代完全可以,分代的唯一理由就是优化GC性能。
- 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。
- 而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
GC:Garbage recycling
轻GC:轻量级垃圾回收,主要是在新生区
重GC(Full GC):重量级垃圾回收,主要是养老区,重GC就说明内存都要爆了
GC垃圾回收,主要在伊甸园区,和养老区
如果内存满了,OOM,堆内存不够了,
**元空间:逻辑上存在,物理上不存在 (**因为存储在本地磁盘内) 而并不算在JVM虚拟机内存中,也就是并没有占堆内存。
12.堆内存调优
//-Xms8m -Xmx8m -XX:+PrintGCDetails
public class Hello{
public static void main( String[ ] args) i
string str = "kuangshensayjava";
while (true){
str += str + new Random( ) .nextInt( bound: 888888888)+new Random( ) .nextInt( bound: 99999999);
}
}
可以通过调整这个参数(Edit Configuration—>VM options)控制Java虚拟机初始内存和分配的总内存的大小。
默认情况下:分配的总内存是电脑内存的 1/4,而初始化的内存: 1/64
OOM:
1、尝试扩大堆内存看结果
2、分析内存,看一下哪个地方出现了问题(专业工具)
//-Xms1024m -Xmx1024m -XX :+PrintGCDetails
当新生代、老年代、元空间内存都满了之后才会报OOM
在一个项目中,突然出现了OOM故障,那么该如何排除,研究为什么出错:
- 能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
- Dubug,一行行分析代码!
MAT,Jprofiler作用
- 分析Dump内存文件,快速定位内存泄露;
- 获得堆中的数据
- 获得大的对象
……
MAT最早集成于Eclipse中,IDEA中可以使用Jprofiles插件,在Settings—>Plugins中搜索Jprofiles,安装改插件即可使用
13.GC
JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代~
Minor GC
年轻代 GC(Minor GC)触发机制
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor区满不会触发- GC。(每次Minor GC会清理年轻代的内存)
- 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
- Minor GC会引发STW,即会暂停其它用户的线程,等待垃圾回收线程结束,用户线程才恢复运行,GC调优就是希望GC能少一些。
Major GC
老年代 GC(MajorGC/Full GC)触发机制
- 指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了
- 出现了MajorGc,经常会伴随至少一次的Minor GC
但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程- 也就是在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC
- Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
- 如果Major GC后,内存还不足,就报OOM了
Full GC
Full GC 触发机制(后面细讲)
触发Full GC执行的情况有如下五种:(归根结底就是老年代空间不足)
- 调用System.gc( )时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小 大于 老年代的可用内存
- 由Eden区、survivor space0(From Space)区 向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存 小于 该对象大小
说明:Full GC 是开发或调优中尽量要避免的。这样STW时间会短一些
内存分配策略
内存分配策略或对象提升(Promotion)规则
- 如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
- 对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代
- 对象晋升老年代的年龄阀值,可以通过选项**-XX:MaxTenuringThreshold**来设置
针对不同年龄段的对象分配原则如下所示:
14判断对象可以回收的方法
JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。
14.1引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
它的优点是简单、高效,
但是缺点也是异常明显:1.这个方法无法解决对象循环引用的问题2.需要单独的字段存储计数器,增加存储空间开销3.每次赋值都需要更新计数器,增加时间开销。
== Java垃圾回收器中没有使用这类算法==。
程序启动后,objectA和objectB两个对象被创建并在堆中分配内存,它们都相互持有对方的引用,但是除了它们相互持有的引用之外,再无别的引用。而实际上,引用已经被置空,这两个对象不可能再被访问了,但是因为它们相互引用着对方,导致它们的引用计数都不为0,因此引用计数算法无法通知GC回收它们,造成了内存的浪费。如下图:对象之间的引用形成一个有环图。
14.2可达分析算法
基于引用计数法无法回收循环应用,我们就有了一种新的方法。
可达分析算法,或叫根搜索算法,在主流的JVM中,都是使用的这种方法来判断对象是否存活的。
这个算法的思路很简单,它把内存中的每一个对象都看作一个结点,然后定义了一些可以作为根结点的对象,我们称之为GC Roots。如果一个对象中有另一个对象的引用,那么就认这个对象有一条指向另一个对象的边。
像上面这张图,JVM会起一个线程从所有的GC Roots开始往下遍历,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被回收。
14.3 什么对象可以当作GC Roots?
共有四种对象可以作为GC Roots
-
虚拟机栈中的引用对象
我们在程序中正常创建一个对象时,对象会在堆上开辟一块内存空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种对象可以作为GC Roots。 -
全局的静态的对象
也就是使用了static关键字定义了的对象,这种对象的引用保存在共有的方法区中,因为虚拟机栈是线程私有的,如果保存在栈里,就不叫全局了,很显然,这种对象是要作为GC Roots的。 -
常量引用
就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也作为GC Roots。 -
本地方法栈中JNI引用的对象
有时候单纯的java代码不能满足我们的需求,就可能需要调用C或C++代码(java本身就是用C和C++写的嘛),因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。
15.GC常用算法
15.1 标记清除算法
- 标记-清除算法 采用从根集合进行扫描,对被引用的(也就是不是垃圾的对象)对象进行标记,标记完毕后,再对堆内存从头到尾进行线性遍历,未被标记的对象进行直接回收,如上图。
- 标记-清除算法优缺点:
不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片。
15.2复制算法 :
新生区主要是用的复制算法
- 复制算法将内存分为两个空间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空闲区间)则是空闲的。
- 复制算法采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。
- 复制算法优缺点
15.3标记压缩算法
-
复制算法不适用于老年代的垃圾回收,因为老年代的大部分对象都是存活对象,复制成本高,使用复制算法效率低。
-
标记清除算法可以用于老年代,但是不仅执行效率低,而且执行完之后会产生内存碎片。
-
标记-压缩算法采用和 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。
-
标记-清除 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
15.4 三种GC算法对比
15.5 分代回收算法.
年轻代复制算法的具体应用:
生成空间好比就是eden区,survivor分别是From幸存区、To幸存区,eden区会标记一些存活的对象拷贝到From区,然后清空eden区。
如果From区和eden区都有垃圾,就把eden区和From区都存活的对象全部拷贝到To区,然后清空eden区和From区
如果To区和eden区都有垃圾,就把eden区和To区都存活的对象全部拷贝到From区,然后清空eden区和To区
反复循环,默认来回循环15次,如果活动的对象还是没有被垃圾回收器回收了,就存放到老年代