JVM虚拟机
Posted miraclemaker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM虚拟机相关的知识,希望对你有一定的参考价值。
1.基础知识:
- java代码执行流程:java源代码编译后为字节码,每一行字节码为一条jvm指令;程序计数器(由寄存器组成)记住当前执行的jvm指令地址,解释器从程序计数器中取得jvm指令地址后解释成为机器码,之后交给cpu运行。
- 栈和栈帧:线程运行需要的内存空间,内部有栈帧组成(方法调用时占用的内存)。栈内存方法调用结束后自动回收,不需要垃圾回收。StackOverFlowError:线程请求栈的深度超过虚拟机栈的最大深度。
- 本地方法栈:java为了调用更底层或其他语言编写的代码,我们需要调用很多本地方法(用native修饰的,比如Thread的start(),hashCode(),clone(),getClass()等),这些方法使用的空间。
- 堆:new出来的对象,jdk1.8后还包括stringtable(串池),需要垃圾回收。OutOfMemoryError:对内存不足,并且GC也无法清理足够的内存。
- 方法区:
载类的一些信息如,静态变量,字段信息,类信息,常量池(保存字面量和符号引用的表),运行时常量池;存在垃圾回收。数据的引用在栈中,先去堆里面找对象实例,再去方法区找对象的类数据。
。jdk8之后移除永久代,出现元空间,包含方法区的基本功能,最大区别在于元空间所占用的是jvm本地内存,不占用虚拟机运行时数据区。
。在主机内存中有一块被称为JVM,JVM内部包括运行时数据区,类加载器,执行引擎等,永久代在运行时数据区内,元空间在运行时数据区外,JVM内。
- 线程与资源:私有资源:虚拟机栈,本地方法栈,程序计数器,共享资源有:堆,元空间。因为线程切换后回到正确的执行位置,pc是线程私有的,而栈私有是因为防止其他线程访问,修改自己的局部变量。
- 单核多线程:单核不存在真正的并发,按理说多线程存在上下文切换效率会更低,但多线程提高效率和IO密切相关,IO操作时将数据从磁盘读取到内存这一步巨慢,我们用这个时间去执行其他任务就可以提升效率。
2.内存溢出的情况:
- 堆溢出:对象一直创建二未被回收,虚拟机栈的线程越来越多,加载的类越来越多
- 栈溢出:方法调用次数过多,一般都是递归不当造成。
3.如何判断对象可以被回收:
- 引用计数法:为每个对象添加引用计数器,有人引用就+1,循环引用时无法回收,已淘汰。
- 可达性分析法:将 虚拟机栈中引用的对象,方法区中静态变量引用的对象,本地方法栈中引用的对象作为GCRoot,从GCRoot向下搜索,没有经过任何引用链的对象可被回收。
4.垃圾回收算法:
- 标记清除算法:第一步先标记垃圾,第二步在清除垃圾(会造成内存碎片化)。
- 标记整理算法:第一步标记清除法,第二步把占用内存的对象向前移动整理(但操作复杂,速度慢)。多用在老年代。
- 复制法:第一步标记垃圾,第二步把占用内存的对象内存(FROM)复制到另一块内存(TO)中,第三步把垃圾清理后内存作为TO。多用在新生代。
5.垃圾回收流程:
- 定义:整个堆内存被分为新生代(伊甸园+幸存区FROM+幸存区TO)与老年代
- 流程:当有新变量来了,先分配到伊甸园;伊甸园满了之后会进行一次Minor GC,将伊甸园与幸存区FROM中无用的垃圾清除,有用的转存到幸存区TO中,并将幸存区TO中所有对象代+1,复制有用内存到TO后交换FROM与TO指针指向的内存(一次Minor GC后伊甸园与幸存区TO都是空的)。当代数达到某个阈值,将其转移到老年代中。当老年代与新生代内存都不够用时,进行一次FULL GC。若对象过大,则直接放入老年代
6.垃圾回收器:
- Parallel:关注吞吐量,新生代采用复制算法,老年代采用标记-整理算法
- CMS:以最小停顿时间为目标,采用标记清除算法,可以并发;
- G1:jdk9后的默认垃圾回收器,同时注重吞吐量和最小停顿时间,采用可达性分析算法找到垃圾,标记整理-复制为清除算法清除垃圾。支持并发。
7.引用:
- 强引用:new出来的对象,内存溢出的时候也不会回收,报OOM也不回收。
- 软引用:用软引用创建的对象,只有在内存不足时才会被回收(经常使用,加速垃圾回收,保护系统安全)
- 弱引用:每次垃圾回收都会被回收。
- 虚引用:不是引用,配合引用队列,只是作为变量是否被回收的标志(先将虚引用放入队列,再回收对象)
8.jvm内存参数:
- -Xmx[]:堆空间最大内存。当空余堆内存过少时,堆内存扩大至此。
- -Xms[]:堆空间最小内存。当空余堆内存过多时,堆内存会缩小至此,服务端一般设置Xmx和Xms一致。
- -Xmn[]:新生代最大内存。
- -xx[survivorRatio=4],伊甸园区与from+to区的内存占比,默认为4;
- -xx[use G1]:指定垃圾回收器
- -xss:设置单个线程大小,适中即可,过大会导致线程数偏少。
- 一般我们设置堆空间为最大物理地址的1/4。
9.对象的创建过程:
- 去常量池中定位该类符号引用,看符号引用对应的类是否被加载过,若未加载,先加载类。
- 为对象以cas形式分配空间,防止分配空间时,指针还未修改就同时被另一个对象拿来分配内存。
- 先为属性赋值为0或者null。
- 设置对象头。
- 为属性赋值,执行构造方法,引用指向该内存。
10.类加载过程与初始化顺序:
- 加载:java代码编译后形成字节码,记载就是将字节码以二进制形式读入内存
- 链接:
。验证:验证字节码的正确性和安全性
。准备:为静态变量分配内存,final修饰的编译时就已经赋值,实例变量实例化对象的时候才分配
。解析:将符号引用解析为直接引用(指向类的地址)
- 初始化:执行类构造器,为静态变量赋初值并初始化静态代码块。
- 初始化顺序:父类静态变量和静态代码块->子类静态代码块和静态变量->父类代码块和普通变量->父类构造方法->子类代码块和普通变量->子类构造方法。
11.类加载器和双亲委派机制:
- 从父类加载器到子类加载器分别为:
。BootStrapClassLoader 加载路径为:JAVA_HOME/jre/lib
。ExtensionClassLoader 加载路径为:JAVA_HOME/jre/lib/ext
。ApplicationClassLoader 加载路径为:classpath:
。自定义类加载器
- 双亲委派机制:
。机制:当一个类加载器收到类加载请求时,会先把这个请求交给父类加载器处理,若父类加载器找不到该类,再由自己去寻找。
。作用:能够保证同名的类都只加载一次,避免篡改java核心class造成安全问题。
12.对象头中的信息:
- MarkWork:存储运行时的数据,如hashcode,gc代数,gc标记,锁的状态,获取到锁的线程id等。
- ClassPointer:记录对象所属类的内存地址。
- 如果是数组,还会有数组长度。
13.StringTable:
- 储存字符串常量。如果我们创建的部分变量已经在Stringtable存在,就会去里面取,减少了内存使用
- 程序中所有“”中的字符串都已经放入串池中。如果一个新定义的String有确定结果(s3),那就放到串池中。如果新定义的无确定结果,就会创建新对象到堆中。
- intern():将字符串对象尝试放入串池,有就不放,没有就放。会返回一个串池中的对象。
14.方法区与元空间:
- 方法区加载类的一些信息并保存,JDK8前实现为永久代,jdk8后实现为元空间;
- 永久代包括类信息,字段信息,静态常量,常量池,运行时常量池,串池等,需要进行FullGC进行类的回收,处于运行时数据区。
- 元空间包括类信息,字段信息;静态变量和串池被转移到堆中,JVM的Full GC不需要考虑元空间,处于JVM直接内存中。
- 为什么要替换:
。永久代的对象需要Full GC,Full GC会导致程序暂停,放于直接内存中就减少了压力。
。直接内存中内存更大,不容易导致数据溢出,能够加载更多的类。
- 为什么串池要放到堆中:永久代的对象回收需要Full GC,GC回收效率太低,导致创建的字符串大量无人引用仍然不会回收,放到堆中就能高效的回收。
15.cpu飙升你怎么解决:
- cpu上下文切换过多(文件传输导致线程阻塞),cpu资源过度消耗(运行的程序太多了,有死锁)
- 用top命令查看线程情况,如果是几个线程占用的平均分布,那可能是开的程序太多了,如果是一个占用的特别多,可能是发生了bug。我们用jstack命令生成线程的快照,在快照中可以看到发生问题的代码。
以上是关于JVM虚拟机的主要内容,如果未能解决你的问题,请参考以下文章