JVM、GC、类加载、想了解的都在这里!

目录

一、JVM内存管理

1、程序计数器

2、Java虚拟机栈

3、本地方法栈

4、Java堆(Heap)

5、方法区

6、运行时常量池

Java堆溢出

虚拟机栈和本地方法栈溢出

二、动态内存管理器 GC -- 进行内存管理

1、如何判断对象已死

2、引用分类

3、垃圾回收算法

4、回收方法区

5、垃圾收集器

三、类加载过程

1、加载

2、链接

3、初始化

四、一些面试题

1、请问了解Minor GC和Full GC么,这两种GC有什么不一样吗

2、对象的一生

3、“对象”什么时候进入老年代?

4、类加载时机

5、初始化顺序


JVM定义:指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。

一、JVM内存管理

线程私有区域:程序计数器、Java虚拟机栈、本地方法栈(与线程生命周期一致)

线程共享区域:Java堆、方法区、运行时常量池

1、程序计数器

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个本地方法,这个计数器值为空。

程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM(OutOfMemoryError内存使用尽了)情况的区域! 

2、Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型 : 每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈 的过程。声明周期与线程相同。

局部变量表 : 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。
此区域一共会产生以下两种异常:

1. 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError(栈溢出)异常。

2. 虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutOfMemoryError)异常

3、本地方法栈

本地方法栈与虚拟机栈的作用完全一样,只是本地方法栈为虚拟机使用的本地方法服务,虚拟机栈为JVM执行的Java方法服务。 在HotSpot虚拟机中,本地方法栈与虚拟机栈是同一块内存区域。 

4、Java堆(Heap)

Java堆是JVM所管理的最大内存区域。在JVM启动时创建,此内存区域存放的都是对象实例

Java堆是垃圾回收器管理的主要区域,因此很多时候可以称之为"GC堆"。根据JVM规范规定的内容,Java堆可以处于物理上不连续的内存空间中。Java堆在主流的虚拟机中都是可扩展的(-Xmx设置大值,-Xms设置小值)。 如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM。

5、方法区

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

JVM规范规定:当方法区无法满足内存分配需求时,将抛出OOM异常。 

6、运行时常量池

运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

Java堆溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免GC清除这些对象,那么在对象数量达到堆容量后就会产生内存溢出异常。

内存泄漏 :泄漏对象无法被GC 内存溢出

内存溢出:内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆内存调大; 或者检查对象的生命周期是否过长。

虚拟机栈和本地方法栈溢出

由于我们HotSpot虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来设置。

关于虚拟机栈会产生的两种异常: 如果线程请求的栈深度大于虚拟机所允许的大深度,会抛出StackOverFlow异常 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常。

二、动态内存管理器 GC -- 进行内存管理

本质上,回收不再被使用的内存其实就是回收死对象。

为什么就是回收死对象呢?

PC/栈:和唯一的一个线程强绑定,线程出现就需要这块内存,线程结束,这块内存就可以回收了,这块内存的分配/回收时机是明确的,就不需要 GC 来操心。

常量池+方法区:首先,这两块内存占比较小,其次,里面的数据很少“失去作用”。回收这块内存性价比不高,也不是重点考虑的。

堆:GC 的重点就是如何管理堆上的内存。堆上的内存是以对象为单位进行管理的,分配与回收的都是一个完整的对象。因此,回收转换成了回收死对象的问题。

整体思想:“标记”+“回收”找到所有分配出来的对象中,哪些是死对象。把内存收回来。

1、如何判断对象已死

死对象:没有引用指向的对象。

1.1 引用计数法ReferenceCount:无法解决对象的循环引用问题

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。 引用计数法实现简单,判定效率也比较高。

1.2 可达性分析算法

核心思想: 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。

堆中的对象其实是通过一个或多个“图形结构”进行组织的。GC Roots的对象包含下面几种:

  1. 虚拟机栈(栈祯中的本地变量表)中引用的对象
  2. 方法区中的静态属性不回收,所以有引用指向的对象必须活着,常量引用的对象
  3. 常量池中引用指向的对象

如果在GC过程中,可达性分析后对象的引用关系发生了变化,有可能造成对象回收错误,所以通常在GC中,会通知应用线程的执行(STW:Stop The World) 

finalize方法:
一个对象被回收之前有且仅有一次一定会调用 finalize 方法。设计目的:在对象回收前做一些收尾工作,比如清理以下和对象关联的一些其他资源。

2、引用分类

在这里插入图片描述

3、垃圾回收算法

3.1 标记-清除算法:标出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

算法缺点:

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

3.2 复制算法(新生代回收算法)

为了解决“标记-清除”的效率问题。适用于活着的对象占比小,但会浪费空间。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。

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

适用于活着的对象比较多的情况,但时间上比较慢。

3.4 分代收集算法

根据对象存活的周期不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

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),终如果还是存活,就存入到老年代。

3.5 GC分类:

Particial GC:部分GC

Full GC:全局GC

Minor GC:进行新生代的GC --- GC次数多,单次GC时间短

Major GC:老年代GC--GC次数少,单次GC时间长,但由于老年代GC是由新生代GC引起的,所以Major GC可以看做是 Full GC。

3.6 如何评判 GC 算法的好坏

4、回收方法区

5、垃圾收集器

新生代基本采用复制算法,老年代采用标记整理算法。cms采用标记清理。 

  1.  Serial收集器(新生代,串行):最古老,最稳定,它工作时其他线程必须暂停,简单高效。使用复制算法

  2. ParNew收集器(新生代,并行):是Serial收集器的多线程版本。新生代采用复制算法,老年代采用标记整理

  3. Parallel Scavenge(新生代,并行)Parallel收集器更关注系统的吞吐量,使用复制算法

  4. Serial Old收集器(老年代,串行):新生代采用复制,老年代采用“标记-整理”算法是Serial的老年版

  5. Parallel Old 收集器(老年代,并行),Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法

  6. CMS收集器(老年代,并行),是一种以获取最短回收停顿时间为目标的收集器。“标记—清除”算法

  7. G1收集器,G1 (Garbage-First)是唯一一款全区域的垃圾回收器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。(整体来看是基于"标记-整理"算 法,从局部(两个region之间)基于"复制"算法) 

三、类加载过程

类和对象(1)类和对象(2)

java 类加载过程:加载,链接(验证,准备,解析)初始化。

1、加载

1.1 加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

字节码来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及Java源文件动态实时编译,并执行加载。

类加载器:通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

  1. 启动类加载器(Bootstrap Class-Loader),加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar。

  2. 扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext 包下面的 jar 文件。

  3. 应用类加载器(Application or App Class-Loader),根据程序的类路径(classpath)来加载 Java 类。

如果以上三种类加载器不能满足要求的话,程序员还可以自定义类加载器(继承 java.lang.ClassLoader 类)。

为什么会有自定义类加载器?

  • 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。

  • 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

1.2 双亲委派模型如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。

优点:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

2、链接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。

2.1 验证

这一阶段的目的是为了确保加载进来的字节流符合虚拟机规范,不会造成安全错误。

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2.2 准备

主要是为类的静态变量分配内存,并且赋予默认初始值。这些变量所使用的内存都将在方法区中进行分配。实例变量将会在对象实例化时随着对象一起分配在堆中。

2.3 解析

将常量池内的符号引用替换为直接引用的过程。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

两个重点:

  • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。

  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

3、初始化

这个阶段主要是对类变量初始化,是执行类构造器的过程。换句话说,只对static修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

四、一些面试题

1、请问了解Minor GC和Full GC么,这两种GC有什么不一样吗

1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特 性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。

2. Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至 少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。 Major GC的速度一般会比Minor GC慢10倍以上。

2、对象的一生

  1. 对象诞生在伊甸区,且对象是有年龄的,每经过一次GC,可以简单认为增长一岁
  2. 随着GC的发生,大部分对象也结束在伊甸区
  3. 如果活过本次GC,对象被复制到生存区但大部分对象在生存区也活不了多久,在生存区的对象活着的话会在生存区from和to之间来回复制
  4. 如果对象活过了一个阈值(比如15),就会被移到老年代
  5. 老年代的GC 次数相比于新生代就会少很多,每次GC按照清除或整理方式进行

3、“对象”什么时候进入老年代?

  1. 大对象直接诞生在老年代,因为对象太大了复制算法效果差,GC则遵守老年代的GC。这样做的目的在于避免Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存) 
  2. 长期存活的对象进入老年代。虚拟机给每个对象定义了一个年龄计数器,对象在Eden区出生,经过一次Minor GC后仍然存活,并且能被Survivor区容纳的话,年龄就+1,等加到15时,就会被移动到老年代中。
  3. 动态年龄判定:根据对象年龄有另外一个策略也会让对象进入老年代,不用等待够15次GC,假如当前放对象的Survivor,一批对象的总大小大于这块Survivor内存的一半,那么大于这批对象年龄的对象,就可以直接进入老年代了;

4、类加载时机

  • 创建类的实例,也就是new一个对象
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(Class.forName(“com.lyj.load”))
  • 初始化一个类的子类(会首先初始化子类的父类)
  • JVM启动时标明的启动类,即文件名和类名相同的那个类

5、初始化顺序

静态初始化块和静态变量的先后顺序是:他们在类中出现的先后顺序,非静态变量与代码块的执行顺序同代码执行顺序

 (1)正常类的加载顺序:静态变量/静态代码块 -> main方法 -> 非静态变量/代码块 -> 构造方法

(2)继承关系下:静态优先于非静态,父类优先于子类。

(类加载)父类静态变量和代码块 -- > 子类静态变量和代码块 -- > (实例化对象后)父类非静态,即变量和代码块 -- > 父类构造器 -- > 子类非静态 -- > 子类构造器

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值