JVM
一.JVM初识
jvm简介
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
Java类加载器(ClassLoader)及双亲委派
类加载器
classloader顾名思义,即是类加载。虚拟机把描述类的数据从class字节码文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
执行流程
启动类加载器(Bootstrap
Classloader)负责将<JAVA_HOME>/lib目录下并且被虚拟机识别的类库加载到虚拟机内存中。我们常用基础库,例如java.util.,java.io.,java.lang.**等等都是由根加载器加载。
扩展类加载器(Extention
Classloader)负责加载JVM扩展类,比如swing系列、内置的js引擎、xml解析器等,这些类库以javax开头,它们的jar包位于<JAVA_HOME>/lib/ext目录中。
应用程序加载器(Application
Classloader)也叫系统类加载器,它负责加载用户路径(ClassPath)上所指定的类库。我们自己编写的代码以及使用的第三方的jar包都是由它来加载的。
自定义加载器(Custom
Classloader)通常是我们为了某些特殊目的实现的自定义加载器,后面我们得会详细介绍到它的作用以及使用场景。
public class Test {
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.getClass());
Class<? extends Test> testClass = test.getClass();
System.out.println(testClass.getClassLoader()+" 系统类加载器 (AppClassLoader)");
System.out.println(testClass.getClassLoader().getParent()+" 扩展类加载器 (Extension ClassLoader)");
System.out.println(testClass.getClassLoader().getParent().getParent()+" 启动类加载器 (Bootstrap Classloader)");
}
}
执行结果
classloader双亲委托机制
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父加载器,递归的交由父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
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.
c = findClass(name);
}
}
return c;
}
执行流程
当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
双亲委派的好处
果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap
classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
堆
堆栈的区别
- 物理地址: 堆的物理地址分配对对象是不连续的。因此性能慢些。 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
- 内存分别: 堆因为是不连续的,所以分配的内存是在 运行期 确认的,因此大小不固定。一般堆大小远远大于栈。栈是连续的,所以分配的内存大小要在 编译期 就确认,大小是固定的。
- 存放的内容: 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 - 程序的可见度 堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。 - GC机制:针对的是堆
栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就over
堆的分区
- Eden区:新对象都是在此new出来的
- 老年代区:经过GC机制未清理的对象(长期存活的对象或者新生代无法容纳的大对象)进入老年代
- 元空间:逻辑上存在,物理上不存在这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,java运行时的一些环境或类信息,不存在垃圾回收,关闭JVM就会释放这个区域的内存
元空间的发展历程:
1.6之前 永久代 常量池是在方法区中
1.7永久代 但慢慢退化了-有了去永久代的想法 常量池在堆中
1.8后 无永久代 常量池在元空间中
老年代中的对象:
年龄达到一定的程度(默认是15)的对象
在 survivor 空间中相同年龄所有对象大小的总和>survivor空间的一半
二.JVM调优
gc垃圾回收机制
当程序运行时,至少会有两个线程开启启动,一个是我们的主线程,一个时垃圾回收线程,垃圾回收线程的priority(优先级)较低。
垃圾回收器会对我们使用的对象进行监视,当一个对象长时间不使用时,垃圾回收器会在空闲的时(不定时)对对象进行回收,释放内存空间,程序员是不可以显示的调用垃圾回收器回收内存的,但是可以使用System.gc()方法建议垃圾回收器进行回收,但是垃圾回收器不一定会执行。
调优目的
使用较小的内存占用来获得较高的吞吐量或者较低的延迟。
重要指标
- 内存占用:程序正常运行需要的内存大小。
- 延迟:由于垃圾收集而引起的程序停顿时间。
- 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。
调优
根据内存调优
错误用例
public class Demo {
public static void main(String[] args) {
String str="123456";
while (true){
str+=str+new Random().nextInt(888888888)+new Random().nextInt(999999999);
}
}
}
先设置jvm的属性
-Xmx1024m:设置JVM最大可用内存为1024M。
-Xms1024m:设置JVM促使内存为1024m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-XX:+PrintGCDetails 用于打印输出详细的GC收集日志的信息.
报错信息
解决方案
- 先尝试扩大堆内存看结果
- 分析内存,看一下那个地方出现了问题(专业工具)
常用JVM参数参考
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
Xms 是设置初始化内存分配大小 默认是1/64
Xmx 是设置最大分配内存 默认是1/4
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
gc垃圾回收设置优化
G1的适用场景
- 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
- 最主要的应用是需要低GC延迟并具有大堆的应用程序提供解决方案(G1通过每次只清理一部分而不是全部Region的增量式清理来保证每次GC停顿时间不会过长)
- 在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒
用来替换掉JDK1.5中的CMS收集器,以下情况,使用G1可能比CMS好
- 超过50% 的java堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
- GC停顿时间过长(大于0.5至1秒) - 从经验上来说,整体而言:
- 小内存应用上,CMS大概率会优于 G1;
- 大内存应用上,G1则很可能更胜一筹。
- 这个临界点大概是在 6~8G 之间(经验值)
其他收集器适用场景
- 如果你想要最小化地使用内存和并行开销,请选择Serial Old(老年代) + Serial(年轻代)
- 如果你想要最大化应用程序的吞吐量,请选择Parallel Old(老年代) + Parallel(年轻代)
- 如果你想要最小化GC的中断或停顿时间,请选择CMS(老年代) + ParNew(年轻代)
jvm调优步骤
监控分析
分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点。
如何生成GC日志
生成GC日志
-XX:+UseG1GC 代表使用G1垃圾收集器
-XX:MaxGCPauseMillis=100 垃圾收集最大停顿时间
-Xmx256m 代表堆内存最大大小
-XX:+PrintGCDetails 输出GC详细信息
-XX:+PrintGCTimeStamps 打印GC时间戳
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC 代表执行GC前和之后堆内存状态
-Xloggc:G:\y2\JVM专题\gc.log 代表日志输出目录
产生dump文件
生成dump配置
准备工作
- idea先下载JProfiler工具
- JProfiler官网下载软件
- idea中设置JProfiler
java OOM错误示例
public class Demo {
public static void main(String[] args) {
String str="123456";
try {
while (true){
str+=str+new Random().nextInt(888888888)+new Random().nextInt(999999999);
}
}catch (Error e){
e.printStackTrace();
}
}
}
java 运行jvm配置
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
java 运行结果
java 生成dump文件
查看dump文件
双击打开*.hprof文件
后续待更