JVM总结

Posted ohana!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM总结相关的知识,希望对你有一定的参考价值。

目录

一,Java虚拟机 

1.概念

2.作用

3.运行流程

二,运行时数据区域

理解线程私有

1.Java虚拟机栈(线程私有)

作用

栈帧的组成

2.本地方法栈(线程私有)

3.程序计数器(线程私有)

作用

4.堆区(线程共享)

作用

5.方法区(线程共享)

作用

三,内存布局的异常问题

1.内存溢出

概念

原因/什么情况下会导致

结果

解决方法

2.内存泄漏

概念

原因/什么情况下会导致

结果

解决方法

3.栈溢出

概念

原因

解决方法

四,JVM类加载

1.类加载时机

2.类加载过程

3.双亲委派机制

概念

作用

类型 

优缺点 

破坏双亲委派机制 

五,垃圾回收

1.死亡对象的算法

1)引用计数法

2)可达性分析

2.垃圾回收算法

1)标记-清除算法

2)复制算法

3)标记-整理算法

4)分代算法

3.垃圾收集器

1)ParNew + Parallel Scavenge 新生代收集器,使用复制算法

2)Parallel Old 老年代收集器:标记整理算法

3)CMS 老年代收集器

六,JMM(内存模型)

内存模型的作用

1.主内存与工作内存

2.内存间的交互操作

操作

happen —before原则


一,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)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
  • 常说的堆内存、栈内存中,栈内存指的就是虚拟机栈

栈帧的组成

  1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量
  2. 操作数栈:每个方法会生成一个先进后出的操作栈
  3. 动态链接:指向运行时常量池的方法引用
  4. 方法返回地址: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(内存溢出)

原因/什么情况下会导致

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
  3. 代码中存在死循环或循环产生过多重复的对象实体
  4. 使用的第三方软件中的BUG
  5. 启动参数内存值设定的过小

结果

会导致程序异常

解决方法

  • 优化空间利用率
  • 加大堆的内存

2.内存泄漏

概念

没有用的数据,一直占用内存,无法被gc

原因/什么情况下会导致

  1. io资源未关闭,多个用户都未关闭时,就会造成内存泄漏
  2. 单例模式造成的内存泄露

结果

  • 内存泄漏太多,就会造成内存溢出
  • 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏
  • 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的
  • 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次
  • 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏

解决方法

  1. 从程序本身入手,定时清理不使用的数据
  2. 使用软引用或是弱引用
  3. 加大内存
  4. 定时重启程序

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区不够的情况,老年代担保,可将数据存放到老年代

  1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
  2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
  3. 部分对象会在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.内存间的交互操作

操作

  1. lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  2. unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
  6. assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
  7. store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用。
  8. write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

happen —before原则

如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

以上是关于JVM总结的主要内容,如果未能解决你的问题,请参考以下文章

20145207 《Java程序设计》第5周学习总结

20145123刘森明《Java程序设计》第五周学习总结

Java GC工作原理以及Minor GCMajor GCFull GC简单总结

07JVM参数调优-05最后总结

JVM内存区域划分总结

JVM 内存区域总结:方法区+堆内存+本地方法栈+元空间——JVM系列