JVM总结
Posted ohana!
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM总结相关的知识,希望对你有一定的参考价值。
目录
1)ParNew + Parallel Scavenge 新生代收集器,使用复制算法
一,Java虚拟机
1.概念
从硬件层面理解
- 虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的
- Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统
- JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行
从进程的角度理解
- 启动一个Java进程
- 会创建一个Java虚拟机
- 并加载class字节码文件
- 运行时翻译机器码,让cpu执行
2.作用
- Java中的所有类,必须被装载到JVM中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中
- JVM就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行(也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行)
3.运行流程
二,运行时数据区域
理解线程私有
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存
1.Java虚拟机栈(线程私有)
作用
- Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型
- 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 常说的堆内存、栈内存中,栈内存指的就是虚拟机栈
栈帧的组成
- 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量
- 操作数栈:每个方法会生成一个先进后出的操作栈
- 动态链接:指向运行时常量池的方法引用
- 方法返回地址:PC 寄存器的地址
2.本地方法栈(线程私有)
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的
3.程序计数器(线程私有)
作用
用来记录当前线程执行的行号的
- 程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
- 如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果正在执行的是一个Native方法,这个计数器值为空。
- 程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!
4.堆区(线程共享)
作用
程序创建的所有对象都在堆中保存
- 堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)
5.方法区(线程共享)
作用
用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的
- jdk1.7时,方法区是在Java进程的内存中
- jdk1.8时,是叫元空间,属于本地内存(不在Java进程中)
三,内存布局的异常问题
1.内存溢出
概念
某个运行时数据区域,如果创建的数据,内存不足,就会造成OOM(内存溢出)
原因/什么情况下会导致
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
- 使用的第三方软件中的BUG
- 启动参数内存值设定的过小
结果
会导致程序异常
解决方法
- 优化空间利用率
- 加大堆的内存
2.内存泄漏
概念
没有用的数据,一直占用内存,无法被gc
原因/什么情况下会导致
- io资源未关闭,多个用户都未关闭时,就会造成内存泄漏
- 单例模式造成的内存泄露
结果
- 内存泄漏太多,就会造成内存溢出
- 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏
- 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的
- 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次
- 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏
解决方法
- 从程序本身入手,定时清理不使用的数据
- 使用软引用或是弱引用
- 加大内存
- 定时重启程序
3.栈溢出
概念
栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出
原因
对于一台服务器而言,每一个用户请求,都会产生一个线程来处理这个请求,每一个线程对应着一个栈,栈会分配内存,此时如果请求过多,这时候内存不够了,就会发生栈内存溢出
解决方法
- 用栈把递归转换成非递归
- 使用静态对象替代非静态局部对象
- 增大堆栈大小值
四,JVM类加载
1.类加载时机
1)Java 类名 :Java程序的的入口类,需要先执行类加载,在执行main()
2)运行时,执行静态方法调用,静态变量操作等
3)new 对象的时候
4)通过反射创建一个类对象,然后就可以再通过反射,生成实例对象,或调用静态方法
2.类加载过程
加载:加载class字节码(二进制数据)到方法区,在堆中,生成一个Class类对象
验证:验证class字节码数据,是否安全,以及是否符合Java虚拟机规范
准备:静态变量设置为初始值(对象初始值就是null,基础数据类型,就是对应的初始值)常量 (final修饰的)会设置为真实的值
解析:将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
初始化:静态变量真正的初始化赋值,静态代码块初始化
3.双亲委派机制
概念
不直接执行当前类加载器,而是先要查找当前类加载器的父类加载器,父类加载器也是类似的,先找父类,直到找到最顶层的类加载器,开始执行类加载,(找到class类就进行,找不到就交给下一级类加载器)
作用
- 除了顶层的启动类加载器,其他的类加载器在加载之前,都会委派给它的父加载器进行加载,一层层向上传递,直到所有父类加载器都无法加载,自己才会加载该类。
- 双亲委派模型,更好地解决了各个类加载器协作时基础类的一致性问题,避免类的重复加载;防止核心API库被随意篡改
类型
-
启动类加载器(Bootstrp ClassLoader),加载 /lib/rt.jar、-Xbootclasspath
-
扩展类加载器(Extension ClassLoader)sun.misc.Launcher$ExtClassLoader,加载 /lib/ext、java.ext.dirs
-
应用程序类加载器(Application ClassLoader,sun.misc.Launcher$AppClassLoader),加载 CLASSPTH、-classpath、-cp、Manifest
-
自定义类加载器
优缺点
好处:
确保安全,如Object,String类等,都是使用jdk提供的类,而不是使用自己定义的java.lang.Object
缺点:
保证了安全,但扩展性就没那么好了(灵活性降低)
破坏双亲委派机制
-
JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
-
Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
-
OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
五,垃圾回收
1.死亡对象的算法
1)引用计数法
概念
每当堆中多一个引用时,计数器+1,少一个,计数器-1
缺陷
无法解决循环引用问题
2)可达性分析
核心思想
- 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的
- 对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象
2.垃圾回收算法
1)标记-清除算法
概念
分为“标记”和“清除”两个阶段
标记:标记需要gc的对象
清楚:使用gc,将标记好的对象回收
缺陷
(1)效率低
(2)内存碎片:存放对象时,即使可用空间足够,但连续空间不足,也会触发另一次的gc
2)复制算法
概念
把用的内存划分为两块一样大小的空间,每次只使用其中一块,把存活的对象复制到另一块空间,然后把之前使用的空间清空
优点:清空半区的效率高,不会出现内存碎片
缺点:内存利用率低(不会超过50%)
3)标记-整理算法
类似于标记清楚算法,采取的方案是将存活的对象,移动到连续的空间,再清空剩余的空间
优点:不会出现内存碎片的问题
4)分代算法
在堆中,根据对象创建及回收的特性,分为了两块区域:
(1)新生代
Eden(E区),2个Survivor(S区)
原因:对象朝生夕死,很快的创建的,由很快的变为不可用的垃圾
采取的算法:复制算法
过程:如果出现S区不够的情况,老年代担保,可将数据存放到老年代
- 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
- 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
- 部分对象会在From和To区域中来回复制,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
(2)老年代
对象可能长期存活
采取的算法:标记清除算法,标记整理算法
(3)新生代GC vs 老年代GC
3.垃圾收集器
从Java应用程序来说:大略上,可以分为2种类型
(1)用户体验优先(实时性要求比较高):用户要使用的程序,系统每次STW的时间最少,总的 STW的时间可能变多 ——ParNew(新生代)+CMS(老年代) G1
(2)吞吐量优先(批处理任务):用户不使用的,之执行任务的,总的STW时间最少
——Parallel Scavenge (新生代)+ Parallel Old (老年代)
1)ParNew + Parallel Scavenge 新生代收集器,使用复制算法
2)Parallel Old 老年代收集器:标记整理算法
3)CMS 老年代收集器
特性:用户体验优先(并发收集,低停顿)
采取”标记清除“算法
缺陷
- cpu比较敏感:gc线程需要cpu资源,相对就需要更多的cpu资源
- 无法处理浮动垃圾
- 内存碎片问题
4)G1 全堆收集器
用户体验优先
内存划分:把堆划分为相等的很多个区域,每个区域根据需要设置为E,S,T(老年代)
算法:整体上看基于”标记整理算法“,局部上看基于”复制算法“
六,JMM(内存模型)
内存模型的作用
不同硬件及操作系统,对内存的访问操作也不同,Java采取统一的Java内存模型来屏蔽以上的差异
1.主内存与工作内存
主内存:线程共享的,Java进程的内存
工作内存:线程私有的,cpu执行线程指令时,使用寄存器来保存上下文
2.内存间的交互操作
操作
- lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
- unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
- assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
- store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用。
- write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
happen —before原则
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
以上是关于JVM总结的主要内容,如果未能解决你的问题,请参考以下文章