JVM学习总结

JVM是什么

JVM就是java虚拟机,它是一个虚构出来的计算机,可在实际的计算机上模拟各种计算机的功能。JVM有自己完善的硬件结构,例如处理器、堆栈和寄存器等,还具有相应的指令系统。

Java字节码是执行在JRE((Java Runtime Environment Java运行时环境)上的。JRE中最重要的部分是Java虚拟机(JVM),JVM负责分析和执行Java字节码。

JVM是java字节码执行的引擎,还能优化java字节码,使之转化成效率更高的机器指令。
JVM中类的装载是由类加载器和它的子类来实现的,类加载是java运行时一个重要的系统组件,负责在运行时查找和装入类文件的类。

不同的平台对应着不同的JVM,在执行字节码(class文件)时,JVM负责将每一条要执行的字节码送给解释器,解释器再将其翻译成特定平台换将的机器指令并执行,这样就实现了跨平台运行。

(1)jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。
(2)jvm包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。(3)JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

hotspot vm

它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
在2006年的JavaOne大会上,Sun公司宣布最终会把Java开源,并在随后的一年,陆续将JDK的各个部分(其中当然也包括了HotSpot VM)在GPL协议下公开了源码, 并在此基础上建立了OpenJDK。这样,HotSpot VM便成为了Sun JDK和OpenJDK两个实现极度接近的JDK项目的共同虚拟机。

JVM工作原理

VM在整个JDK中处于最底层,负责与操作系统的交互。操作系统装入jvm是通过JDK中的java.exe来实现的,具体步骤如下:
a、创建JVM装载环境和配置;
b、装载jvm.dll;
c、初始化jvm.dll;
d、调用JNIEnv实例装载并处理class类;
e、运行java程序

JVM生命周期

  1. JVM实例对应了一个独立运行的java程序它是进程级别
    a) 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点
    b) 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程
    c) 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
  2. JVM执行引擎实例则对应了属于用户运行程序的线程它是线程级别的

JVM内存结构

深入理解JVM—JVM内存模型
参考URL: https://www.cnblogs.com/dingyingsi/p/3760447.html
【推荐】 JVM运行时数据区域划分
参考URL: https://blog.csdn.net/bruce128/article/details/79357870?utm_source=blogxgwz9

内存各区域认识

JVM学习总结_第1张图片

在hotsnop vm中,方法区和永久代是一个东西,java规范中是分开的。在jdk 1.7之前 常量池是放在方法区中。

元空间的本质和永久d代类似,都是对JVM规范中方法区的实现。不过元空间与永久带之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间大小仅受本地内存限制。


  1. 堆(heap)是存储java实例或者对象的地方,是GC的主要区域,同样是线程共享的内存区域。
    堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。

    老年代 : 三分之二的堆空间
    年轻代 : 三分之一的堆空间
    eden区: 8/10 的年轻代空间
    survivor0 : 1/10 的年轻代空间
    survivor1 : 1/10 的年轻代空间
    命令行上执行如下命令,查看所有默认的jvm参数

java -XX:+PrintFlagsFinal -version

参数 作用
-XX:InitialSurvivorRatio 新生代Eden/Survivor空间的初始比例
-XX:Newratio Old区 和 Yong区 的内存比例

  • 存放对象实例
  • 垃圾收集器管理的主要区域
  • 新生代,老年代

JVM 年轻代到年老代的晋升过程的判断条件是什么呢?
年轻代:所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。
年老代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。 jdk1.7以后该参数已废除。永久代被元数据区替代。

  1. 程序计数器
    每个线程一块,指向当前线程正在执行的字节码代码的行号。如果当前线程执行的是native方法,则其值为null。
  2. Java虚拟机栈
    线程私有,每个线程对应一个Java虚拟机栈,其生命周期与线程同进同退。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。
  • 栈帧
    每个方法执行,都会创建一个栈帧,伴随方法从创建到执行完成。用于存储局部变量表,操作数栈,动态链接,方法出口等。
  • 局部变量表
    a. 存放编译器可知的各种基本数据类型,引用数据类型,returnAddress类型。
    b. 局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多少内存是固定的,在方法运行期间是不会改变局部变量表大小。

注意:这里引用类型,比如User对象,我们指的这个对象User的引用存储在栈中,对象本身是存储在堆中。

  1. 本地方法栈
    hotspot 中 本地方法栈和JAVA虚拟机栈是不分的,在一起。
    从java规范来说,两者是分开的。
  • java虚拟机栈执行java方法服务
  • 本地方法栈为虚拟机执行native方法服务
    其他方面都是两者完全一样。
  1. 元数据区
    元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。

在JDK8之前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫做永久代的区域。永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小,32位机器默认的永久代的大小为64M,64位的机器则为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。但是有一个明显的问题,由于我们可以通过‑XX:MaxPermSize 设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOM)。

在JDK7之前的HotSpot虚拟机中,纳入字符串常量池的字符串被存储在永久代中,因此导致了一系列的性能问题和内存溢出错误。

随着Java8的到来,我们再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域就是我们要提到的元空间。
这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量

字符串常量池

JDK1.7 就开始“去永久代”的工作了。 1.7把字符串常量池从永久代中剥离出来,存放在堆空间中
Java进阶——Java中的字符串常量池
参考URL: https://blog.csdn.net/qq_30379689/article/details/80518283
JVM为了减少字符串对象的重复创建,其内部维护了一个特殊的内存,这段内存被成为字符串常量池(方法区中)。

运行时常量池在JDK1.6及之前版本的JVM中是方法区的一部分,而在HotSpot虚拟机中方法区放在了”永久代(Permanent Generation)”。所以运行时常量池也是在永久代的。
但是JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池

字符串对象创建
1、String str = new String(“abc”) 创建多少个对象?2个
在常量池中查找是否有”abc”对象,有则返回对应的引用实例,没有则创建对应的实例对象(1个)
在堆中 new 一个 String(“abc”) 对象(1个)
将对象地址赋值给str,创建一个引用

2、String str = new String(“A”+”B”)创建多少个对象?4个
在常量池中查找,字符串”A”,”B”,”AB”(3个)
在堆中 new 一个 String(“AB”) 对象(1个)
将对象地址赋值给str,创建一个引用

内存分配

java虚拟机对内存的分配策略。

  • 优先分配到eden
  • 大对象直接分配到老年代
  • 长期存活的对象分配到老年代
  • 空间分配担保(新生代不够向老年代借)

Eden区域

优先分配到eden。
指定堆内存大小参数。指定Eden大小,进行测试。
如下
制定堆总大小20M,10M是新生代,那么老年代也就是10。survivorRatio=8指定了Eden所占比例。
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

/**
 * Eden区测试
 */
public class EdenTest {

    public static void main(String[] args){

        byte[] b1 = new byte[2 * 1024 * 1024];
        byte[] b2 = new byte[2 * 1024 * 1024];
        byte[] b3 = new byte[2 * 1024 * 1024];

        byte[] b4 = new byte[4 * 1024 * 1024];

        System.gc();
    }
}

JVM学习总结_第2张图片
上图中第一个标红就是MinorGC,第二个就是Full GC。是我们代码中System.gc();触发的。
第一个GC触发,是因为我们前面3个2M的后,再加4M大于伊甸园的8M,所以触发了GC。它把前面那3个2M的对象放到了老年代区域,所以途中你看到大概6M,另外的新的4M,他又放到了Eden区。
如上图,
新的对象会先生成在Young area,也就是PSYoungGen中
在几次GC以后,如过没有收集到,就会逐渐升级到PSOldGen 及Tenured area(也就是PSPermGen永久代)中。

Minor GC、Major GC和Full GC之间的区别
参考URL: http://www.importnew.com/15820.html
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
Full GC 是清理整个堆空间—包括年轻代和老年代。

大对象直接进去老年代

JVM对大对象的判定不同,我们可以通过制定参数。
–XX:PretenureSizeThreshold
如下,我指定6M为大对象,我new 6M的字节数组对象,发现它放到了老年代。

-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=6M

JVM学习总结_第3张图片

长期存活对象将进入老年代

可以指定参数:
-XX:MaxTenuringThreshold

空间分配担保

空间分配担保就是新生代内存不够了,想老年代借用。
java虚拟机默认启用了空间分配担保。通过如下参数可以禁用。
-XX:+HandlePromotionFailure

逃逸分析

使用逃逸分析,把没有逃逸的对象在栈上分配。
其实就是对象的作用域。

如果这个对象只在内部使用没有外部引用,那么这个对象就会在栈上分配。否则这个对象发生逃逸。

只有在方法体内有效的对象,就是这个对象没有逃逸,否则就是对象发生了逃逸。

垃圾回收机制

对象的结构

  • Header 对象头(对象元数据)
    自身运行时数据MarkWord (哈希值,GC分代年龄(垃圾回收:分代收集算法),锁状态标志 等)
    类型指针(说明对象是哪个类的实例)
  • InstanceData
  • Padding
    HotSpot虚拟机对象头 Mark Word
    JVM学习总结_第4张图片

对象的访问定位

对象的访问方式有如下两种方式。

  • 使用句柄
  • 直接指针
    指针好理解,句柄则是 先通过指针指向堆中的句柄池,堆中的句柄池指向具体的对象。

hotspot采用的是 直接指针的方式。

垃圾回收算法

垃圾回收算法其实就是处理如下问题,重点前两个:

  • 如果判定对象为垃圾对象
    两个常见算法:
  1. 引用计算法
  2. 可达性分析法
  • 如何回收
    回收策略(标记-清楚算法、复制算法、标记-整理算法、分代收集算法)
    垃圾回收器(Serial、Parnew、Cms、G1)
  • 何时回收

引用计数算法

在对象中添加一个计数器,当有地方引用这个对象的时候,引用计数器+1,当用引用失效时,计数器就-1
目前的java虚拟机基本没有用这种实现。
因为引用计数,没法回收堆中相互引用(循环引用),栈中没有指向堆的情况,此时计数器不为0,所以无法回收。

jdk8 默认采用的 是Parallel 并行收集器。

可达性分析算法

效率、功能比较强大。
思路:从GCRoot对象向下一级一级查找,看是否引用,如果,找不到则判定为垃圾对象。
作为GCRoots的对象:

  • 虚拟机栈(局部变量表)
  • 方法区的类属性所引用的对象
  • 方法区中常量所引用的对象
  • 本地方法栈中所引用的对象

主流的JVM都是使用可达性分析算法。

垃圾回收常用算法

  1. 标记清除算法
    比较基础、简单。
    标记出所需要回收的对象,有个专门的清除程序清除这些对象。
    2个问题:标记和清除的效率不是很高。内存中会出现大量不连续空间。
  2. 复制算法(主要是针对新生代回收算法)
    为什么JVM新生代中有两个survivor?
    因为将eden区的存活对象复制到survivor区时,必须保证survivor区是空的,如果survivor区中已有上次复制的存活对象时,这次再复制的对象肯定和上次的内存地址是不连续的,会产生内存碎片,浪费survivor空间。

应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。

上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片

标记整理算法(主要针对老年代)

先进行标记,标记完再整理,整理完再清除
被标记的对象向内存的一端移动,整理到一块,然后一起删除。

分代收集算法

其实就是根据内存不同区域,比如新生代使用复制算法,老年代使用标记整理算法。

收集器

  • Serial收集器:出现比较早,历史悠久,单线程。
  • parnew收集器:多线程。
  • parallel收集器(回收新生代内存):多线程。设计目标:达到可控制的吞吐量。
    -XX:MaxGCPauseMillis 垃圾回收器最大停顿时间
    -XX:GCTimeRatio 吞吐量大小 0到100直接的数字,默认99 即 吞吐量99%的意思。

吞吐量 = (执行用户代码时间) / (执行用户代码时间 + 垃圾回收所占用的时间)

  • cms收集器:使用标记清除算法。
  • g1收集器:最厉害的垃圾回收器。

JVM(HotSpot) 7种垃圾收集器的特点及使用场景
参考URL: https://www.cnblogs.com/chengxuyuanzhilu/p/7088316.html
hotspot的JVM中的垃圾回收
参考URL: https://www.cnblogs.com/jinshiyill/p/5290675.html
HotSpot VM垃圾收集器
参考URL: https://www.aliyun.com/jiaocheng/793006.html

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。**虚拟机具体如何进行内存回收动作,是由虚拟机所采用的GC收集器所决定的,而通常虚拟机中往往不止有一种GC收集器,**像目前(JDK7时代)的HotSpot里面就包含有Serial、SerialOld、ParNew、ParallelScavenge、ParallelOld、ConcurrentMarkSweep和GarbageFirst七种收集器。

那么如何判断当前使用了哪个收集器呢?
加打印 -verbose:gc -XX:+PrintGCDetails,打印输出如下
JVM学习总结_第5张图片
PSYoungGen 就是Parallel收集器,说明这里新生代使用的是Parallel收集器。我们根据这里的名字就可以判断使用那个收集器。
使用什么由JDK所处的环境决定,一般JDK服务端(java -version查看)默认就是使用Parallel收集器。
JVM学习总结_第6张图片

你可以手动加参数让使用Serial收集器:
-XX:+UseSerialGc

类加载过程

深入理解JVM内幕
参考URL: https://blog.csdn.net/zhoudaxia/article/details/26454421?utm_source=blogxgwz1

Java提供了动态的装载特性;它会在运行时的第一次引用到一个class的时候对它进行装载和链接,而不是在编译期进行。JVM的类装载器负责动态装载。
 每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。如果两个类的全局限定名是一样的,但是如果命名空间不一样的话,那么它们还是不同的类。
 
JVM学习总结_第7张图片

常用JVM参数

JVM系列三:JVM参数设置、分析
参考URL: https://blog.csdn.net/see__you__again/article/details/51998038
JVM参数配置总结
参考URL: https://blog.csdn.net/u014351782/article/details/53098227?utm_source=blogxgwz0

  1. Java堆溢出转存
    下面的程中我们限制Java 堆的大小为20MB,不可扩展(将堆的最小值-Xms 参=
    数与最大值-Xmx 参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDump
    OnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump 出当前的内存堆转储
    快照以便事后进行分析。
  2. 垃圾回收器打印,加参数
    可以打印gc日志
    -verbose:gc -XX:+PrintGCDetails

你可能感兴趣的:(Java后台)