JVM详解

一.JVM简介

JDK:Java开发工具
JRE:Java运行环境
JVM:Java虚拟机,对应平台虚拟机(字节码,语言跨平台)
JMM:内存模型(逻辑概要,描述共享程序的结构)
内存区域划分:JVM中空间划分的具体区域

虚拟机:VMWare/Virtual Box
通过软件模拟的具有完整硬件功能,运行在完全隔离环境中的计算机系统。
JVM是通过软件模拟java的字节码集,JVM只保留了PC寄存器,而普遍的虚拟机有很多的寄存器。
从JDK1.3至今HotSpot为默认JVM.

JVM内存区域划分->判断对象是否存活->垃圾回收算法->垃圾回收器->JVM性能检测

二.JVM内存区域划分

1.线程私有内存

每个线程都有,彼此之间完全隔离。
1.1.程序计数器:
程序计数器是比较小的内存空间,当前线程所执行的字节码的行号指示器。若当前线程执行的是java方法,记录的是正在执行的JVM字节码指令地址;若当前线程执行的是native方法,计数器为空。程序计数器是唯一一块不会产生OOM异常,程序计算器保证了线程切换后能恢复到正确的执行位置
1.2.Java虚拟机栈:
虚拟机栈描述java方法执行的内存模型。每个方法执行的同时都会创建一个栈帧存储局部变量表(存放了编译器可知的基本数据类型,对象引用),操作数栈,方法出口等信息。每个方法从调用直到执行完毕的过程,对应一个栈帧在虚拟机栈的入栈与出栈过程。生命周期与线程相同:在创建线程时同时创建此虚拟机栈,线程执行结束,虚拟机栈与线程一同回收。此区域一共会产生两种异常:
(1).若此线程请求的栈深度大于JVM允许的深度(-Xss设置栈容量),跑出StackOverFlowError异常。(单线程)
(2).虚拟机在动态扩展时无法申请到足够的内存,跑出OOM(OutOfMemoryError)异常。(多线程)
1.3.本地方法栈:
地方法(native方法)执行的内存模型(与Java虚拟机的作用完全相同)。HotSpot虚拟机中本地方法栈与虚拟机栈是同一块内存区域。

2.线程共享内存

所有线程共享此内存空间,此内存空间对所有线程可见。
2.1.java堆(GC堆):
java堆(java Heap)是JVM管理的最大内存区域。在JVM启动时创建。所有线程共享此内存,此内存存放的都是对象的实例及数组。java堆是垃圾回收器管理的最主要的内存区域。java堆可处于物理上不连续的内存空间。-Xmx:设置堆最大值。-Xms设置堆最小值。
若在堆中没有足够的内存完成对象实例分配并且堆无法再次扩展时抛出OOM异常。
2.2.方法区:
用于存储已被类加载的类信息,常量,静态变量以及编译器编译后的数据。JDK1.8以前方法区也叫永久代,此区域的内存回收主要针对常量池的回收以及对类型的卸载。JDK1.8以后成为元空间(Meta Space).方法区无法满足内存分配时会抛出OOM异常。
2.3:运行时常量池:
运行时常量池是方法区的一部分,存放字面量与符号引用。字面量:字符串常量(JDK1.7移到堆中),final常量,基本数据类型的值。符号引用:类,字段,方法的完全限定名,名称,描述符。对象产生:符号引用->类->对象

直接内存不属于虚拟机运行时数据区的一部分,也不是虚拟机中规范定义的内存区域,这个区域也可能产生OOM异常

三.java堆溢出

OOM异常出现的两种情况:
(1)内存溢出:内存中的对象确实还存活,但由于堆内存中的内存不够用产生异常。
(2)内存泄漏:泄露对象无法被GC。

四.垃圾回收器与内存分配策略

主要是方法区与Java堆

1.如何判断对象已死:

  • 引用计数法:
    算法思想:给每个对象附加一个引用计数器,每当有一个地方引用此对象时,计数器+1;每当有一个引用失效时,计数器-1;在任意时刻,只要计数器值为0的对象就是不能再使用,即对象已经死。引用计数法实现简单,判断效率高。Python使用引用计数法来管理内存,但是无法解决循环引用问题。JVM并未采用此办法。

  • 可达性分析算法:java采用此算法。
    算法核心:通过一系列被称为“GC Roots”的对象作为起点,从这些节点开始向下搜索对象,搜索走过的路径,称为“引用链”,当一个对象到任意一个GC Roots对象没有任何作用的引用链相连时(从GC Roots对象不可达),证明此对象已死。
    java中能作为GC Roots的对象:
    (1)虚拟机栈中的引用对象。
    (2)类静态变量引用的对象。
    (3)方法中的常量引用的对象。
    (4)本地方法栈中本地方法引用的对象。

对象的拯救:finalize protected void finalize() throws Throwable
在可达性分析算法中不可达的对象,也并非“非死不可”,所有不可达的对象处于缓刑阶段。要宣告一个对象彻底死亡要经历两次标记过程:若对象在进行可达性算法分析后发现到GC Roots不可达,此对对象进行第一次标记并且进行一次筛选过程。筛选的条件是此对象是否有必要执finalize()。当对象没有覆盖finalize()方法或finalize()方法已被JVM调用过,JVM会将此对象彻底宣判死亡。 筛选成功(对象覆写了finalize()方法并且未被执行),会将此对象放入F-Queue,如果对象在finalize()成功自救(对象会与GC Roots建立联系),则对象会在第二次标记时被移除回收集合,成功存活;若对象在finalize中仍与GC Roots不可达,宣告死亡。任何一个对象的finalize()方法都只会被系统自动调用一次,空方法

2.引用

JDK1.2对引用的扩充(引用类型的数据中存储的数值代表的是另一块的内存的起始地址,称这块内存代表着一个引用):配置内存:-XX:PrintGC

  • 强引用(Strong Reference):代码中普遍存在的,类似于“Object obj=new Object()”,只要强引用还存在,垃圾回收器永远不会回收。
  • 软引用(Soft Reference):软引用用来描述一些有用但不必须的对象。对于仅被软引用指向的对象,在系统将要发生内存溢出之前,会将所有的软引用进行垃圾回收。若内存够用,这些对象仍保留。在JDK1.2之后提供SoftReference来实现软引用。
  • 弱引用(Weak Reference):弱引用强度比软引用差点。仅被弱引用关联的对象最多只能生存到下一次GC开始之前。当垃圾回收器开始工作时,无论当前内存够不够用,都会回收掉被弱引用关联的对象。JDK1.2以后,使用WeakReference来实现弱引用。
    虚引用(Rhantom Reference):
  • 虚引用也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用来实例化一个对象。为对象设置唯一的目的就是能在这个对象被GC前收到一个通知。JDK1.2后,使用PhantomReference类来实现。弱引用好处:程序员不知道GC是什么时候可以通过虚引用了解到。用finalize()也可以知道。

3.回收方法区

方法区回收主要回收两部分内容:废弃常量和无用的类(也仅仅是"可以"而不是必然)
以常量池中字面量(直接量)的回收为例,假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池的"abc"常量,也没有在其他地方引用这个字面量,如果此时发生GC并且内存不够用的话,这个"abc"常量会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判断一个类是无用类:

  • 该类的所有实例都已经被回收(java堆中不存在该类的任何实例)。
  • 加载类的类加载器已经被回收。
  • 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法。

4.垃圾回收算法

4.1.标记-清除法

核心思想:算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
主要问题:
(1).效率问题 : 标记和清除这两个过程的效率都不高.
(2). 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

4.2.复制算法(新生代回收算法)

核心思想:复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉
出现的问题
(1)内存利用率低。现在的商用虚拟机(包括HotSpot都是采用这种收集算法来回收新生代)。
改进后的复制算法:
新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下:
(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),最终如果还是存活,就存入到老年代
缺点:当对象存活率较高时,因多次复制,复制消耗大,效率会降低。

4.3.标记-整理算法(老年代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法
核心思路:标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存

4.4.分代收集算法

当前JVM垃圾回收采用的方法
核心思想:根据对象的存活周期将内存(Java堆)划分为以下两块。
新生代:每次GGC都有大量对象死去,只有少量存活。因此采用复制算法。
老年代:对象存活率较高,没有额外空间对其分配担保,采用标记-整理法。

MinorGC FullGc的区别
(1).MinorGC称为新生代GC:指的是发生在新生代的垃圾回收。由于新生代对象大多存活周期较短。因此发生频率非常频繁,一般回收速度也较高。
(2)FullGC称为老年代GC或MajorGC:指发生在老年代的垃圾收集。出现了老年代GC,经常会伴随至少一次的新生代GC(并非绝对,在Parallel Scavenge收集器中就有直接进行老年GC的策略选择过程)。新生代GC的速度一般会比老年代 GC快10倍以上

4.垃圾回收器(回收算法的具体实现:JDK1.8):

4.1新生代垃圾回收器:Serial(串行GC收集器),ParNew(并行GC收集器),Parallel Scavenge(并行GC收集器)
4.2老年代垃圾回收器:CMS(并发GC垃圾回收器),Serisl Old(串行GC收集器),Parallel Old(并行收集器)
4.3全区域回收器:G1(并发)—唯一一款全区域的垃圾回收器

  • 串行:垃圾回收线程与用户线程在JVM中顺序执行。
  • 并行:多个垃圾回收线程一起执行,用户线程仍处于等待状态。
  • 并发:垃圾回收线程与用户线程一起执行。
  • STW:当垃圾回收线程工作时,用户线程处于等待状态。

5.对象分配策略

5.1对象优先在Eden分配:

大多数情况下,对象在新生代Eden区分配空间,当Eden区没有足够的空间分配时,JVM发生一次新生代 GC。
JVM参数:-Xmn:设置新生代大小
-XX:SurvivorRatio=8设置比例。

5.2大对象(需要大量连续空间的Java对象)直接进入老年代:

设置对象大小:-XX:PretenureSizeThreshold=字节大小

5.3新生代中长期存活对象进入老年代:

JVM给每个对象定义了一个叫对象年龄的计数器。若对象在Eden区出生并经历一次Minor GC后任然存活并且能被Survivor容纳,将此对象的年龄置为1.
此后对象每再Survivor区域中经历一次Minor GC,年龄就增加1岁。当期年龄增加到一定程度(默认15),此对象晋升到老年代。
设置参数:-XX:MaxTenuringThreshold=年龄

5.4.动态年龄判定:

JVM并不是永远要求对象的年龄必须达到MaxTenuringThreshold才晋升老年代。若Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间(From或To区)的一半,此时年龄大于等于该年龄的所有对象直接进入老年代。无须等到MaxTenuringThreshold要求的年龄。

6.JDK命令行工具:

  • jps(重点):JVM进程状态工具
    列出正在运行的JVM进程,并返回进程ID.
    常用参数:jps -l:输出主类全名,返回进程ID

  • jstat:JVM统计信息监视工具
    显示本地或远程JVM中类装载,内存,垃圾回收等数据。
    常用参数:jstat -gcutil PID:显示垃圾回收信息。

  • jinfo:JVM配置信息查看工具
    jinfo -flags PID

  • jmap(重点):内存映像工具
    jmap用于生成堆转储快照(堆的快照)
    常用参数:jmap -heap PID:显示JVM堆具体信息。
    map -histo PID:显示JVM中对象的统计信息。

  • jhat:heap文件的分析工具
    jhat heap文件路径。

  • jstack(重点):java堆栈跟踪工具
    jstack生成当前JVM的快照。
    可用于定位线程出现长时间停顿的原因,如线程间死锁,死循环,请求外部资源导致的长时间等待等问题,当线程出现停顿时 就可以用jstack各个线程调用的堆栈情况

    常用参数:

  • jstack -l:除堆栈外,显示关于锁的附加信息。

  • jstack -F:当正常输出的请求不被响应时,强制输出线程堆栈。

  • jstack -m:如果调用到本地方法的话,可以显示C/C++的堆栈。

7.java内存模型(JMM)

基于线程的内存模型,JMM定义的主要目的是为了定义程序中各个变量的访问规则(JVM如何将变量从内存中取出以及如何将变量再写回内存等细节)。此处的变量包括实例字段,静态字段与数组元素。

7.1.主内存(所有线程共享)与工作内存(每个线程独有):

(1).JMM规定所有变量必须存在主内存中。
(2).每条线程都有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本。
(3).线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,不能直接读写主内存变量。
(4).不同线程之间也无法直接访问彼此的工作内存变量,线程之间变量值的传递需要通过主内存来完成。

7.2.内存间交互操作

(主内存与工作内存的交互协议,变量从如何从主内存拷贝到工作内存中,如何从工作内存同步到主内存之类的实现细节)
锁定与解锁不是原子性操作,剩下都是原子性操作
(1).lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
(2).unlock(解锁):用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
(3).read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
(4).load(载入): 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
(5).use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
(6).assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
(7).store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用。
(8).write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

7.3JVM内存中的三大特性

(只有同时满足,程序的线程才是安全的,是保证线程安全的重要凭证):
(1).原子性:
基本数据类型的访问,读写是具备原子性的,如若需要更大范围的原子性,需要内建锁或者lock体系的支持(i++,i–等操作)。
(2).可见性:
当一个线程修改了共享变量的值,其他线程能够立即得知修改。
实现:volatile,final,synchronized可以实现可见性。
(3).有序性:
若在本线程内观察,所有操作都是有序性的(线程内存表现为串行);若在线程之外观察另一个线程,所有线程都是无序的(指令重排,工作内存与主内存同步延迟)。

JMM具备先天的有序性,即无需通过任何手段就能保证的有序性。称为JMM的happens-before原则,若两个操作的次序无法从happens-before中推导出来,则无法保证其有序性,JVM可以随意对其进行重排序。
happens-before原则(先发生原则):
(1).程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
(2).锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
(3).volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
(4).传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
(5).线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
(6).线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
(7).线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、
(8).Thread.isAlive()的返回值手段检测到线程已经终止执行
(9).对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值