JVM详细学习
一、面试题
1.1、请谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
1.2、什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
1.3、JVM的常用调优参数有哪些?
1.4、内存快照如何抓取,怎么分析Dump文件?知道吗?
1.5、谈谈JVM中,类加载器的认知?
二、参考学习的视频和文章:
本篇主要学自b站狂神,知识浅显,待进一步学习和完善,学无止尽!
2.1视频:
2.2文章:
三、核心知识
3.1、JVM的位置
3.2、JVM的体系结构
3.3、类加载器
- Bootstrap ClassLoader:最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等;
- Extention ClassLoader:扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件;
- Appclass Loader:也称为SystemAppClass,加载当前应用的classpath的所有类;
package com.lxf.demo5;
public class Car {
public static void main(String[] args) {
//类是模板
Car car1=new Car();
Car car2=new Car();
Car car3=new Car();
//打印每个car对象的hashCode,发现不一样
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
//打印每个car对象的class,发现一样
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
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 java程序获取不到(底层c、c++写的) rt.jar
}
}
3.4、双亲委派机制
package java.lang;
public class String {
/*
*JVM加载一个class时先查看是否已经加载过,没有则通过父加载器,然后递归下去,直到BootstrapClassLoader,如果 *BootstrapClassloader找到了,直接返回,如果没有找到,则一级一级返回(查看规定加载路径),最后到达自身去查找这些对象。这 *种机制就叫做双亲委托
*
*作用:
*1.避免重复加载
*2.防止恶意加载
*3.编写恶意类java.lang.Object,自定义加载替换系统原生类;
*/
public String toString(){
return "hello";
}
public static void main(String[] args) {
String s=new String();
s.toString();
}
}
3.5、沙箱安全机制
学习文章:沙箱安全机制
3.6、Native(本地方法)
package com.lxf.demo5;
public class Demo {
public static void main(String[] args) {
new Thread(()->{
},"my thread name").start();
}
//native:凡是带了native 关键字的,说明java的作用域范围抵达不到,会去调用底层c语言的库!
//会进入本地方法栈
//调用本地方法接口 JNI
//JNI作用:扩展java的使用,融合不同的编程语言为java所用! 最初:c,c++
//java诞生的时候:C、C++ 横行,想要立足,必须要有调用C、C++的程序
//它在内存区域中专门开启一块标记区域:Native Method Stack,登记了native方法
//最终执行的时候,通过JNI加载本地方法库中的方法
//例如Java程序驱动打印机,管理系统
private native void start0();
}
3.7、PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区的方法字节码(用来存储指向一条指令的地址,也将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
3.8、方法区
Method Area方法区:
方法区是被所有的线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有 定义的方法的信息都保存在该区域,此区域属于共享空间;
静态变量、常量、类信息(构造函数、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
比如:static、final、Class、常量池(jdk1.7在堆,jdk1.8在元空间)
3.9、栈
存放:八大基本类型、类对象的引用类型
一:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因
在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。
- 当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中
- 当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。
二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量
- 当声明的是基本类型的变量其变量名及其值放在堆内存中的
- 引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
3.10、三种JVM
- Sun公司
HotSpot
(常用) - BEA
JRocjit
- IBM
J9 VM
3.11、堆
一个JVM只有一个堆内存,堆内存的大小是可以调节的
类、方法、常量、变量,保存我们所有的引用类型的真实对象;
堆内存中还要细分为三个区域:
- 新生代
- 伊甸园区
- from区
- to区
- 老年代
- 永久区(jdk1.8后改为元空间)
2.12、新生代
- 类:诞生和成长的地方,甚至死亡
- 伊甸园区:所有的对象都是在伊甸园区创建的
2.13、老年代
当年轻带随着不断地Minor GC ,from survivor中的对象会不断成长,当from survivor中的对象成长大15岁的时候,就会进入老年代,在老年代的就能存储很久了,只有重GC时才会来清理老年代中的垃圾
3.14、永久区(jdk改名元空间,效果也有改变)
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存
当一个启动类加载了大量的第三方jar包。tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载。直到内存满,就会出现OOM;
- jdk1.6之前:永久代,常量池是在方法区
- jdk1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中
- jdk1.8之后:无永久代,常量池在元空间
3.15、堆内存调优
1.设置jvm最大内存和初始化内存大小
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
-Xms:设置初始化内存分配大小(默认1/64)
-Xmx:设置最大分配内存(默认1/4)
PrintGCDetails :打印GC垃圾回收线程清理信息
package com.lxf.demo5;
public class MyTest {
public static void main(String[] args) {
long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
System.out.println("max = " + max/1024/1024);
System.out.println("total = " + total/1024/1024);
}
}
2.内存快照分析:
MAT插件:eclipse集成的
Jprofiler插件
MAT,Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中的数据
- 获得大的对象
- …
下载Jprofiler
- 在Jprofier官网下载安装
- idea下载jprofiler插件
- 注册码:L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
- 配置idea:
测试:
package com.lxf.demo5;
import java.util.ArrayList;
public class Demo2 {
byte[] array=new byte[1*1024*1024];//1m
public static void main(String[] args) {
ArrayList<Demo2> list=new ArrayList<Demo2>();
int count=0;
try {
while(true){
list.add(new Demo2());
count++;
}
} catch (OutOfMemoryError e) {
System.out.println("count = " + count);
e.printStackTrace();
}
}
}
报错:
count = 819
java.lang.OutOfMemoryError: Java heap space//堆内存满了
at com.lxf.demo5.Demo2.<init>(Demo2.java:6)
at com.lxf.demo5.Demo2.main(Demo2.java:13)
打印Dump文件配置:
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
HeapDumpOnOutOfMemoryError:报OOM错时生成Dump文件
再次运行Demo2:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid21752.hprof ...
Heap dump file created [7901177 bytes in 0.036 secs]//生成Dump文件
count = 6
java.lang.OutOfMemoryError: Java heap space
at com.lxf.demo5.Demo2.<init>(Demo2.java:7)
at com.lxf.demo5.Demo2.main(Demo2.java:14)
打开Demo.java文件的位置,向上一直找到src平级,找到java_pid21752.hprof文件,双击打开
3.16、GC(垃圾回收)
概述:
-
常用算法:复制算法、标记整理(压缩)、引用计数器、标记清除法
-
JVM在进行GC时,并不是对这三个区域统一回收。大部分的时候,回收都是新生代
-
GC两种类:轻GC(普通的GC)、重GC(全局GC)
题目:
- JVM的内存模型和分区-详细到每个区
- 堆里面的分区有哪些?Eden,from,to,老年代,说说他们的特点!
- GC的算法有哪些?复制算法、标记整理(压缩)、引用计数器、标记清除法,怎么用?
- 轻GC和重GC分别在什么时候发生?
引用计数法:
复制算法:
解释:年轻代每次GC后,幸存下来的会进入一个幸存区,这个幸存区就叫from,第二次GC后,再将from还在用的和Eden Space幸存下来的交给to区(from中的内容通过复制算法转移),此时to变from,from变to(清空变to)。
-
好处:没有内存的碎片
-
坏处:浪费了内存空间:多了一半空间永远是空to。假设对象100%存活(极端情况),就极大浪费资源
-
复制算法最佳使用场景:对象存活度较低的时候(新生区);
标记清除法:
- 优点:不需要额外的空间!
- 缺点:两次扫描,严重浪费时间,会产生内存碎片
标记压缩(标记清除的优化):
总结:
- 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
- 内存整齐度:复制算法=标记清除算法>标记清除算法
- 内存利用率:标记压缩算法=标记清除算法>复制算法
思考:
- 问题:难道没有最优的算法?
- 答案:没有,没有最好的算法,只有最合适的算法–>GC:分代收集算法
- 年轻代:存活率底–>复制算法
- 老年代:区域大,存活率高–>标记清除+标记压缩混合 实现