类加载、jvm、GC

类加载机制

1.装载(load)

查找和导入class文件
1.通过类的全限定名找到定义该类的二进制字节流
2.将这个二进制字节流锁代表的静态存储结构转换为方法区的运行时数据结构
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

2.链接(link)

验证(Verify)

保证被加载类的正确性
  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备(Prepare)

为静态变量分配内存,并将其初始化为默认值

解析(resolve)

把类中的符号引用转换为直接引用

3.初始化(Initialize)

对类的静态变量,静态代码块执行初始化操作

类加载机制图解

在这里插入图片描述

类装载器

bootstrap ClassLoad: 加载jre/lib/rt.jar
Extension ClassLoader: 加载jre/lib/*.jar
App ClassLoader: 加载classpath中的jar
Custom ClassLoader: 自定义类加载器,通过java.lang.ClassLoad 加载自定义的类,如tomcat等中间件
加载器加载顺序图解
在这里插入图片描述
加载原则:检查某个类是否已经加载为至底而上加载的顺序是自顶而下

双亲委派机制
定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把
这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就
成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
优势:Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的
Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型
最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用
双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object
类。
破坏:可以继承ClassLoader类,然后重写其中的loadClass方法。

运行时数据区

在这里插入图片描述

方法区(Method Area)

方法区是线程共享的内存区域,在虚拟机启动时创建
用于存储被虚拟机加载的类的信息、常量、静态变量、即使编译器编译后的代码。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

//典型方法区指向堆
private static Object obj=new Object();

堆(heap)

堆是java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,为线程所共享,java对象及数组都分配在堆上。

//堆指向方法区
方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?

虚拟机栈(Java Virtual Machine Stacks)

虚拟机栈是一个线程执行的区域,保存着一个线程的方法调用状态,换句话说,一个java线程的运行状态由虚拟机栈保存,所以虚拟机栈是线程私有的,独占的。随着线程的创建而创建。
每一个被线程执行的方法称为该虚拟机栈中的栈帧,即每个方法对应一个栈帧。调用一个方法就会向虚拟机中压入一个栈帧,一个方法完成就会将该栈帧弹出。
如下图:
在这里插入图片描述
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to
the run-time constant pool)、方法返回地址(Return Address)和附加信息。

  • 局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中,局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
  • 操作数栈:以压栈和出栈的方式存储操作数的
  • 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
  • 方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
public void A(){
	//典型的栈指向堆
	Object obj=new Object()
}

程序计数器(The pc Register)

程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时
间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够
恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。

本地方法栈(Native Method Stacks)

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。

内存模型

在这里插入图片描述
默认新生代中Eden:S1:S2 = 8:1:1
常见GC方式

  • Minor GC: 新生代
  • Major GC:老年代
  • Full GC:新生代加老年代

对象在内存中所在区域详解

根据之前对于Heap的介绍可以知道,一般对象和数组的创建会在堆中分配内存空间,关键是堆中有这么多区域,那一个对象的创建到底在哪个区域呢?
比如有对象A,B,C等创建在Eden区,但是Eden区的内存空间肯定有限,比如有100M,假如已经使用了100M或者达到一个设定的临界值,这时候就需要对Eden内存空间进行清理,即垃圾收集(Garbage Collect),这样的GC我们称之为Minor GC,Minor GC指得是Young区的GC。经过GC之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到Survivor区,然后再清空Eden区中的这些对象。
接着上面的GC来说,比如一开始只有Eden区和From中有对象,To中是空的。此时进行一次GC操作,From区中对象的年龄就会+1,我们知道Eden区中所有存活的对象会被复制到To区,From区中还能存活的对象会有两个去处。若对象年龄达到之前设置好的年龄阈值,此时对象会被移动到Old区,如果Eden区和From区没有达到阈值的对象会被复制到To区。 此时Eden区和From区已经被清空(被GC的对象肯定没了,没有被GC的对象都有了各自的去处)。这时候From和To交换角色,之前的From变成了To,之前的To变成了From。也就是说无论如何都要保证名为To的Survivor区域是空的。
从上面的分析可以看出,一般Old区都是年龄比较大的对象,或者相对超过了某个阈值的对象。在Old区也会有GC的操作,Old区的GC我们称作为Major GC。

垃圾回收

如何确定一个对象是垃圾

  • 引用计数法
    对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
    弊端 :如果AB相互持有引用,导致永远不能被回收。
  • 可达性分析
    通过GC Root的对象,开始向下寻找,看某个对象是否可达
    能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

垃圾回收算法

  • 标记-清除(Mark-Sweep)
    在这里插入图片描述
    找出内存中需要回收的对象,并且把它们标记出来---->清除掉被标记需要回收的对象,释放出对应的内存空间

缺点: 标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程 序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 (1)标记和清除两个过程都比较耗时,效率不高 (2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无 法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

  • 复制(Copying)
    在这里插入图片描述
    将内存划分为两块相等的区域,每次只使用其中一块,当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
    缺点: 空间利用率降低。
  • 标记-整理(Mark-Compact)
    在这里插入图片描述
    标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活
    的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

垃圾收集器

在这里插入图片描述

  1. Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯
一选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更
重要的是其在进行垃圾收集的时候需要暂停其他线程。

优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器

在这里插入图片描述

  1. ParNew收集器

可以把这个收集器理解为Serial收集器的多线程版本。

优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

在这里插入图片描述

  1. Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集
器,看上去和ParNew一样,但是Parallel Scanvenge更关注 系统的吞吐量 。
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序
的运算任务。

-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
-XX:GCTimeRatio直接设置吞吐量的大小。
  1. Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算
法",运行过程和Serial收集器一样。
在这里插入图片描述

  1. Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理算法"进行垃圾
回收。

吞吐量优先
  1. CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。
采用的是"标记-清除算法",整个过程分为4步

(1)初始标记 CMS initial mark 标记GC Roots能关联到的对象 Stop The World--
->速度很快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容 Stop The
World
(4)并发清除 CMS concurrent sweep

由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来
说,CMS收集器的内存回收过程是与用户线程一起并发地执行的

优点:并发收集、低停顿
缺点:产生大量空间片、并发阶段会降低吞吐量

在这里插入图片描述

  1. G1收集器

G1特点

并行与并发
分代收集(仍然保留了分代的概念)
空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集
上的时间不得超过N毫秒)

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个
大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再
是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
工作过程可以分为如下几步

初始标记(Initial Marking) 标记一下GC Roots能够关联的对象,并且修改TAMS的值,需要暂
停用户线程
并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发
执行
最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需
暂停用户线程
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据
用户所期望的GC停顿时间制定回收计划

垃圾收集器分类

  • 串行收集器->Serial和Serial Old

只能有一个垃圾回收线程执行,用户线程暂停。 适用于内存比较小的嵌入式设备 。

  • 并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old

多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于科学计算、后台处理等若交互场景 。

  • 并发收集器[停顿时间优先]->CMS、G1

用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。 适用于相对时间有要求的场景,比如Web 。

理解吞吐量和停顿时间
  • 停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间
  • 吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任
务。

这两个指标也是评价垃圾回收器好处的标准,其实调优也就是在观察者两个变量。

垃圾收集器的选择
  • 优先调整堆的大小让服务器自己来选择
  • 如果内存小于100M,使用串行收集器
  • 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
  • 如果允许停顿时间超过1秒,选择并行或JVM自己选
  • 如果响应时间最重要,并且不能超过1秒,使用并发收集器

常用工具及指令

jvm参数
在这里插入图片描述

jvm自带工具指令

  • jps 查看java进程
  • jinfo 实时查看和调整JVM配置参数
//查看
# jinfo -flag name PID
# jinfo -flag MaxHeapSize PID
# jinfo -flag UseG1GC PID
//修改
# jinfo -flag [+|-] PID
# jinfo -flag = PID
//查看曾经赋过值的一些参数
jinfo -flags PID
  • jstat 查看虚拟机性能统计信息

查看垃圾收集信息

# jstat -gc PID 1000 10
  • jstack 查看线程堆栈信息

查看线程堆栈信息

 # jstack PID
  • jmap 生成堆转储快照

dump出堆内存相关信息:

# jmap -dump:format=b,file=heap.hprof PID
  一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof

工具
jvisualvm jconsole Arthas MAT(分析dump) GCViewer(分析GC日志)

常见问题思考

(1)内存泄漏与内存溢出的区别
内存泄漏:对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存溢出:内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。
(2)young gc会有stw吗?
不管什么 GC,都会有 stop-the-world,只是发生时间的长短。
(3)major gc和full gc的区别
major gc指的是老年代的gc,而full gc等于young+old+metaspace的gc。
(4)G1与CMS的区别是什么
CMS 用于老年代的回收,而 G1 用于新生代和老年代的回收。
G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生。
(5)什么是直接内存
直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
(6)不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
(7)方法区中的无用类回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
(8)不同的引用
JDK1.2以后,Java对引用进行了扩充:强引用、软引用、弱引用和虚引用

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值