JVM探究
-
请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
-
什么是OOM,什么是栈溢出StackOverFlowError? 怎么分析?
-
JVM的常用调优参数有哪些?
-
内存快照如何抓取,怎么分析Dump文件?知道吗?
-
谈谈JVM中,类加载器你的认识?
1. JVM位置
2. JVM的体系结构
3. 类加载
3.1 类加载过程
类加载的过程分为三个阶段:
1.加载
- 将类的字节码载入方法区,并创建类.class对象(存在堆中);
- 如果此类的父类没有加载,先加载父类或先加载接口;
- 加载是懒惰执行。(用到这个类时才会把类加载到方法区中)
2.链接
- 验证 -验证类是否符合Class规范,合法性、安全性检查;
- 准备 -为static变量分配空间,设置默认值;
- 解析 -将常量池的符号引用解析为直接引用。
3.初始化
- 执行静态代码块与非final静态变量的赋值;
- 初始化是懒惰执行。
3.2 类加载器
作用:加载class文件
-
虚拟机自带的加载器
-
启动类(根)加载器(用c写的)
-
拓展类加载器
-
应用程序加载器
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader); //AppClassLoader 应用程序加载器
System.out.println(classLoader.getParent());//ExtClassLoader 拓展类加载器 在\jre\lib\ext
System.out.println(classLoader.getParent().getParent()); //null 启动类(根)加载器 在External Libraries下的 rt.jar
}
}
3.2 双亲委派机制
public class String {
public String toString(){
return "1111";
}
public static void main(String[] args) {
String s = new String();
s.toString();
}
}
结论:
- 类加载器收到类加载的请求;
- 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类(根)加载器;
- 启动加载器检查是否能够加载这个类,能加载就结束(使用当前加载器),否则,通知子加载器进行加载;
- 重复步骤3;
- 如果都找不到报Class Not Found。
运行一个类之前,会先到启动类(根)加载器中找有没有,有则执行,没有就去拓展类加载器找,再没有才去应用程序加载器。
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Class<? extends Car> aClass1 = car1.getClass();
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println("自己写的Car类的加载器为:"+classLoader);
String s = new String();
Class<? extends String> aClass = s.getClass();
ClassLoader classLoader1 = aClass.getClassLoader();
System.out.println("String类的加载器为:"+classLoader1);
}
}
为null的原因:启动类(根)加载器是用C语言写的,java调用不到。
双亲委派机制存在的意义:
-
通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
-
通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。那么,就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。
-
双亲委派机制是在classLoader里的loadclass方法里实现的,
/** *加载具有指定二进制名称的类。该方法的默认实现按以下顺序搜索类: 调用findloaddclass (String)检查类是否已经加载。 在父类装入器上调用loadClass方法。如果父类为空,则使用虚拟机内置的类装入器。 调用findClass(String)方法来查找类。 如果使用上述步骤找到该类,并且resolve标志为true,则该方法将在结果class对象上调用resolveClass(class)方法。 ClassLoader的子类被鼓励重写findClass(String),而不是这个方法。 除非重写,否则该方法在整个类加载过程中同步getClassLoadingLock方法的结果。 参数: name -类的二进制名称 resolve -如果为真,则解析类 返回: 产生的Class对象 抛出: ClassNotFoundException—如果找不到类 **/ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
沙箱安全(了解)
4. Native、本地方法栈
native关键字:
-
凡是带了native关键字的方法,说明java的作用范围达不到了,要去调用C语言的库(本地方法库中)。
-
会进入本地方法栈,调用本地方法接口,本地方法库
-
JNI(本地方法接口)的作用: 拓展Java的使用,融合不同编程语言为Java所用
-
它在内存区域中专门开辟了一标记区域(本地方法栈),登记native方法,
在最终执行的时候,加载本地方法库中的方法通过本地方法接口(JNI)
使用其它接口(其它语言写的)的方法:
http,socket,webservice调用其它语言写的接口
5. 程序计数器(PC寄存器)
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
6. 方法区
static,final,Class文件信息,已被虚拟机加载的信息(即编译器编译后的代码缓存等数据);
Class文件信息:包含的信息有版本、访问标志、常量池、当前类、超级类、接口、字段、方法、属性等。
7. 栈
8大基本类型、对象引用、实例的方法
值传递:
引用传递
(18条消息) 值传递与引用传递的区别_梦樊哥哥的博客-CSDN博客_值传递和引用传递的区别是什么
8. HotSpot
三种JVM:(了解)
- Sun公司:HotSpot
- BEA公司:JRockit
- IBM公司:J9
9. 堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把类,方法,常量,变量放到堆中,还有我们所有引用类型的实例对象,数组
堆内存分为三个区域:
- 新生区(伊甸园)
- 养老区
- 永久区 Perm
OOM(OutOfMemoryError):堆内存不够
在Jdk1.8后,永久代变为元空间
9.1 新生区
概述:类 诞生 和 成长,甚至死亡的地方
- 伊甸园:所有对象都是在伊甸园中new出来的
- 幸存者区(0,1):当伊甸园存满后,会触发轻GC垃圾回收,没有被回收掉的进入幸存区
解释:
假设伊甸园可以存十个,当伊甸园存满后,会触发轻GC垃圾回收,没有被回收掉的进入幸存区,
当伊甸园和幸存区都存满了,尝试进入老年代,老年代放不下会触发重GC清理空间,
清理完后依然放不下会OOM了,但99%的对象都是临时对象。能进老年区的都很少。
9.2 老年代
9.3 永久区(元空间)
这个区域常驻内存,用来存放jdk自身携带的Class对象,Interface元数据,存储的java运行时一些环境,不存在垃圾信息
关闭虚拟机就会释放这个区域的内存
jdk1.6之前: 永久代,常量池在方法区
jdk1.7: 永久代,常量池在堆中,在虚拟机中
jdk1.8: 无永久代,常量池在元空间,元空间在本地内存中
逻辑上存在:
元空间的内存释放比较苛刻,先回收实例引用a、b、c,然后等所有类都被回收了,先把类加载器回收了,最后才会把元空间中占用的内存才会释放掉。
结论:类内存释放需要等到类的实例不再引用,它的类加载器被回收。
9.4 堆内存调优
public class demo {
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory();
//返回jvm的初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("max="+maxMemory+"字节\t"+(maxMemory/(double)1024/1024)+"MB");
System.out.println("total="+totalMemory+"字节\t"+(totalMemory/(double)1024/1024)+"MB");
}
}
默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存:1/64
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
- -Xms:设置初始化总内存
- -Xmx:设置最大内存
- -XX:打印堆信息
其它结论:
新生代大小 + 老年代大小 = 堆内存大小
所以元空间逻辑上存在堆,物理上在本地内存
处理OOM:
- 尝试扩大堆内存看结果
- 扩大堆内存无效,分析内存(使用专业工具)
10. 使用JPofiler工具分析OOM原因
OOM处理过程:
- 扩大堆内存
- 内存快照分析工具:JPofiler
JPofiler作用:
- 分析Dump内存文件,快速定位内存泄露;
- 获得堆中的数据
- 获得大的对象
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
11.GC回收
GC: 垃圾回收,只存在堆(包括方法区)中,大部分发生在新生区
- 伊甸园
- 幸存区(form、to)
- 老年区
GC目的:实现无用对象内存自动释放,减少内存碎片、加快分配速度。
GC要点:
- 回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存;
- 判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象;
- GC具体的实现称为垃圾回收器;
- GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略;
- 根据GC的规模可以分成Minor GC,MixedGC,FullGC;
GC规模分类:
- 轻GC(普通GC):主要发生在新生区
- 重GC(全局GC):主要发生在老年区,清理全局
11.1 分代回收与GC规模
11.1.1 分代回收
- 伊甸园eden,最初对象都分配到这里,与幸存区合称新生代;
- 幸存区survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成from和to,采用标记复制算法;
- 老年代old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升);
11.1.2 GC规模
- MinorGC:发生在新生代的垃圾回收,暂停时间短;
- MixedGC:新生代+老年代部分区域的垃圾回收,G1收集器特有 ;
- FullGC:新生代+老年代完整垃圾回收,暂停时间长,应尽力避免。
11.2 GC回收算法
GC算法:
- 标记清除法
- 标记整理
- 标记复制
- 引用计数器
- 三色标记法
根据如何判定对象是垃圾,垃圾回收算法分为两类:
1、引用计数式垃圾收集(判定垃圾是通过引用计数器)别名:直接垃圾收集
- 引用计数法
2、追踪式垃圾收集(判定垃圾是通过GC Roots)别名:间接垃圾收集
- 可达性分析算法(根据GC Roots即一定不会被垃圾回收的对象,通过它的引用链确定是否为无用对象)
- 三色标记法:用于标记对象 JVM垃圾回收——三色标记法_jvm三色标记法_熟透的蜗牛的博客-CSDN博客
可达性算法用于追踪,三色标记法用于标记
11.2.1 引用计数法(基本不用)
每个对象都有一个计数器,每引用一次,计数器就加一,没引用就GC掉
很low,用的少
11.2.2 标记清除(很少用)
“标记清除”算法首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
标记是通过GC Root根对象的引用链查看该对象是否被引用,从而决定是否被标记,
也可以反过来,标记要存活的对象,统一回收所有未标记的对象。
缺点:造成内存碎片,虽然清除后释放了很大的内存,但它们都不连续。如果我需要一段连续的内存(数组),将会不够用。
所以很少用这个算法。
11.2.3 标记复制(新生代主要算法)
注意:to和from区会轮流交换,不是确定的
谁空谁是to
1.每次GC都会把伊甸园中活的对象移到幸存区,一旦伊甸园被GC后就会是空的;
2.假设to区和from区都只有一个对象时,会把其中一个区的对象复制到另一个区,然后变成to区
3.但一个对象经历15次(默认)GC还存活,会进入养老区,可以通过-XX:MaxTenuringThreshold=5设置对象进入老年代经历GC的次数
好处:没有内存的碎片
坏处: 浪费了内存空间,多了半空间
复制算法最佳使用场景:
对象存活度较低的区域(新生区)
11.2.4 标记整理(老年代主要算法)
把标记后的对象往一端靠拢,防止内存碎片问题的产生。
缺点:效率比较低
11.2.5 三色标记法
1. 概念
用三种颜色记录对象的标记状态
- 黑色:已标记,
- 灰色:标记中
- 白色:未标记
2. 并发漏标问题
产生原因:有两类线程:垃圾回收线程(守护线程)和用户线程;
以前的垃圾回收器是不可以让两个线程并发运行的,现在某些垃圾回收器可以让两个线程并发运行,并发运行就可能产生漏标问题。
解决方案:记录标记过程变化
1.Incrementtal Update(增量更新)
- 只要复制发生,被赋值的对象就会被记录。
2.Snapshot At The Beginning,SATB(原始快照)
- 新加对象就会被记录;
- 被删除引用关系的对象也被记录。
11.3 垃圾回收器
应用程序可分为两种:
1.注重响应时间
2.注重吞吐量:注重很多计算
JVM基础 -> 什么是STW?_jvm stw_欧皇小德子的博客-CSDN博客
11.3.1 Paraller GC
- eden内存不足发生Minor GC,标记复制 STW;
- old内存不足发生Full GC,标记整理 STW;
- 注重吞吐量。
11.3.2 ConcurrentMarkSweepGC
- old并发标记,重新标记时需要STW==(防止漏标问题)==,并发清除;----->标记清除算法
- Failback Full GC;
- 注重响应时间。
因为用了标记清除法,会有内存碎片,所有会有更好的垃圾回收器代替它。
11.3.3 G1 GC
从jdk9开始已经作为默认垃圾回收器
- 响应时间与吞吐量兼顾;
- 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous;
- 新生代回收:eden内存不足,标记复制STW;
- 并发标记:old并发标记,重新标记时需要STW;
- 混合收集:并发标记完成,开始混合收集,参与复制的有eden、survivor、old,其中old会根据暂停时间目标,选择部分回收价值高(存活对象少)的区域,复制时STW;
- Failback Full GC:当收集的速度小于需要分配新对象的速度,出现并发失败的情况,触发Full GC。
第一阶段:新生代回收
第二个阶段:并发标记
触发条件:当老年代的内存越来越多,达到堆内存的45%以上,才会触发并发标记;
并发标记:就是在老年代找到那些存活对象,给它们加上标记,这个标记过程是并发执行的,而为了解决漏标问题,采用了原始快照法,重新标记时,需要处理漏标对象,也需要STW。
第三个阶段:混合收集
会先根据存活时间目标,选择部分回收价值高(存活对象少)的老年代区域以及eden,幸存区做一次垃圾回收。
完成后释放内存
混合收集会进行多次,结束又循环回到新生代回收。
12.执行引擎
12.1 Interpreter解释器
1.把java的字节码转换成适用各个平台的机器码;
2.会多次解释同一段代码。
12.2 JIT Compiler 即使编译器
如果某一段代码调用次数非常地频繁,这段代码会被标记成热点代码,热点代码不适合再用解释器进行解释,
所以要用JIT Complier把热点代码解释成机器码(会对代码进行优化),并缓存起来(缓存在代码缓存区),下次再调用这段代码时,会调用缓存起来的代码。
(33条消息) JVM系列之:关于即时编译器的其他一些优化手段_hresh的博客-CSDN博客
12.3 GC垃圾回收
GC垃圾回收为执行引擎的一种
13.JVM内存参数
-Xmx10240m:虚拟机最大内存,m为单位(兆);
-Xms10240m:虚拟机最小内存;
-Xmn5120m:新生代内存大小,剩下的内存为老年代;
-Xss:每个线程占用的内存,如果不设置这个参数,与操作系统有关,虚拟机栈的内存与这个参数有关;
-XX:SurvivorRatio=3: eden区:from区=3:1,默认比例为 8:1
-XX:NewRatio=2: 老年代内存:新生代内存=2:1
除了按比例设置,还可以直接设置大小:
建议在生产环境把-Xmx和-Xms设置成一样的。
元空间的位置在本地内存,所以它的大小取决于物理内存
代码缓存区:用于保存即时编译器对热点代码的缓存(机器码)
14.内存溢出
不会出现内存溢出的区域-程序计数器
出现OutOfMemorvError的情况:
- 堆内存耗尽-对象越来越多,又一直在使用,不能被垃圾回收
- 方法区内存耗尽-加载的类越来越多,很多框架都会在运行期间动态产生新的类
- 虚拟机栈累积-每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁时
出现StackOverflowError的区域:
- 虚拟机栈内部-方法调用次数过多
项目中什么情况下会出现内存溢出,怎么解决?
-
误用线程池导致的内存溢出
例子一:
public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); while (true){ executorService.submit(()->{ try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
使用Executors.newFixedThreadPool(2);创建的线程池的队列无限制,没有拒绝策略,会导致任务过多,造成内存溢出;
所以推荐使用ThreadPoolExecutor创建自定义创建线程池。
例子二:
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); while (true){ executorService.submit(()->{ try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
用的是有界队列,但它可创建的线程数为Interget.MAX_Value(2147483647),如果任务很多,会创建大量的线程造成内存溢出。
-
查询数据量太大导致的内存溢出
查询出来的记录数据太多导致创建的对象过多导致内存溢出。
-
动态生成类导致的内存溢出
生成的类太多且一直在引用不能被回收
使用Executors.newFixedThreadPool(2);创建的线程池的队列无限制,没有拒绝策略,会导致任务过多,造成内存溢出;
所以推荐使用ThreadPoolExecutor创建自定义创建线程池。
例子二:
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); while (true){ executorService.submit(()->{ try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
用的是有界队列,但它可创建的线程数为Interget.MAX_Value(2147483647),如果任务很多,会创建大量的线程造成内存溢出。
-
查询数据量太大导致的内存溢出
查询出来的记录数据太多导致创建的对象过多导致内存溢出。
-
动态生成类导致的内存溢出
生成的类太多且一直在引用不能被回收