JVM总结

JVM

前言

一、java内存区域

在这里插入图片描述

java运行时内存数据区,它的划分具体如下:
(1)虚拟机栈:虚拟机栈是线程私有的数据区,java虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机栈中创建一个栈帧(stack frame)。每个方法执行的过程就对应了一个入栈和出栈的过程,
在这里插入图片描述
栈帧中包括:局部变量表、操作数栈、动态链接和返回地址

(2)本地方法栈:本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是Java中使用native修饰的方法。
(3)程序计数器:程序计数器也是线程私有的数据区,用于存储线程的指令地址,用于判断线程的分支循环跳转异常、线程切换和恢复等功能。
(4)方法区:方法区是各个线程共享的内存区。它用于存储虚拟机加载的类信息、常量、静态变量和即时编译后的代码等数据
(5)堆:堆是线程共享的数据区。堆是JVM中最大的一块儿存储区域。所有的对象实例都会在分配在堆上。

1.运行时常量池的作用

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外。还有一项信息是常量池表。用于存放编译器生成的各种字面量与符号引用。这部分内容在类加载后存放到运行时常量池。一般除了保存Class文件中描述的符号引用之外还会把符号引用翻译的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的一个重要特征是动态性。Java不要求常量只有编译期才能产生。运行期间也可以将新的常量放入池中。这种特性使用较多的是String类型的intern方法。

运行时常量池是方法区的一部分。受到方法区内存的限制。当常量池无法申请到内存时会抛出OutOfMemoryError。

2.直接内存是什么?

直接内存不属于运行时数据区,也不属于JVM规范定义的内存区。但这部分内存也会被频繁使用。

NIO(非阻塞的IO 基于通道与缓冲区的IO),它可以使用Native函数直接分配堆外内存。通过DirectByteBuffer对象作为内存的引用进行操作。避免了Java堆和Native堆直接的数据拷贝。

直接内存分配不受java堆大小的限制,但是还是会受到本机总内存以及处理器寻址空间的限制。

3.为什么使用元空间(Meta space)代替永久代

在这里插入图片描述
永久代的方法区和堆使用的物理内存是连续的。永久代可以通过PermSize来配置大小;对于永久代来说如果动态生成很多class文件的话,就可能出现OOM PermGen space错误。因为永久代的空间配置有限。最典型的场景是在web开发中比较多的jsp页面的时候。

JDK8之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续。而是直接存于本地内存中。理论上机器内存有多大元空间就有多大。

4.方法区、堆、栈之间有什么联系?

package com.company;

public class Student {
    int id;
    String name;
    int age;
    Computer comp;

    void study(){
        System.out.println("我在学习"+comp.brand);
    }
    void play(){
        System.out.println("我在玩游戏");
    }

    //构造方法。用于创建这个类的对象,无参的构造方法由系统自动创建
    Student(){

    }


	//程序入口
    public static void main(String[] args) {
        Student stu=new Student();
        stu.id=1001;
        stu.name="zx";
        stu.age =18;

        Computer c1=new Computer();
        c1.brand="lenovo";
        stu.comp=c1;


        stu.play() ;
        stu.study();
    }

}

class Computer{
    String brand;//品牌
}

在这里插入图片描述

5.为什么需要两个一样的Survivor区?

最大的好处就是解决了碎片化问题。刚刚新建的对象在Eden区中。Eden区满了之后,触发一次minor GC。Eden中的存活对象就会被移动到Survivor区。这样循环下去,下一次Eden区满的时候。Eden和survivor区各有存活对象。如果此时将Eden区的存活对象放到Survivor中。这两部分对象所占用的内存是不连续的,导致了内存碎片化。

如果是有两个一样的survivor中,就可以永远有一个区是空的,另一个是连续的。

6、java中的类加载机制

java虚拟机负责把描述类的数据从Class文件加载到系统内存中。并对类的数据进行校验、转换、解析和初始化。最终形成虚拟机可以直接使用的Java类型。这个过程被称为java的类加载机制。

加载、链接、初始化、使用和卸载。
其中链接阶段包括三个阶段:验证、准备、解析。这三个阶段顺序不确定,通常是交替进行。解析阶段通常在初始化之后开始。是为了支持动态绑定。

(1)加载阶段:通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流表示的存储结构转换为运行时数据区中方法区的数据结构。并且在内存中生成一个class对象。这个对象代表了这个数据结构的访问入口。
(2)验证阶段:验证就是确保Class文件的字节流中的内容符合《Java虚拟机规范》中的要求,保证这些信息在运行时不会威胁虚拟机的安全。
验证主要包括:文件格式、元数据、字节码、符号引用验证。
(3)准备阶段:是为类中变量分配内存并设置初始值的阶段。这些变量所使用的内存都应当在方法区中进行分配。JDK8之后变量则会随着Class对象一起放在Java堆中

pubic static final int value =666”;

这种情况在编译期就会被设置为666;
(4)解析是Java虚拟机将常量池中的符号引用转换为直接引用的过程。

在编译的时候每个Java 类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用代替。而解析的过程就是为了把符号引用转为真正的地址引用。

解析包括:类接口的解析、字段解析、方法解析、接口方法解析

(5)初始化是类加载过程的最后一个步骤。对初始化阶段《Java虚拟机规范》严格规定了只有下面六种情况才会触发类的初始化。

在遇到new、getstatic、putstatic、invokesttaic这四条字节码时;如果未进行初始化那么首先触发初始化。
初始化的时候如果父类没有初始化那么先对父类进行初始化。
java.lang.reflect包的方法进行反射调用的时候。
虚拟机启动的时候,用户需要指定执行主类的时候。

7、JVM加载class文件的原理

类装载方式有两种:隐式加载和显式加载;
隐式装载:new对象的时候,隐式调用类加载器加载对应的类到JVM中
显式装载:通过Class.Forname()等方法,显式加载需要的类。

类的加载是动态的它不会一次性将所有类全部加载后再运行而是保证程序运行的基础类完全加载到JVM其他的类在需要的时候再加载。

8、 类加载器有哪些

启动类加载器(Bootstrap ClassLoader)用来加载Java核心类库,无法被Java程序直接引用。
扩展类加载器(extendsions ClassLoader) 用来加载Java的扩展类库。Java虚拟机的实现会提供一个扩展目录。
系统类加载器(system class loader)系统类加载器,它根据Java应用的类路径(classpath)来加载java类。一般来说,Java应用的类都是由它加载完成的。可以通过ClassLoader.getSystemClassLoader() 来获取它。
用户自定义类加载器,通过继承java.lang.ClassLoader 类的方式实现。

9、Java虚拟机是如何判定两个Java类是相同的?

Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否相同。即便是相同的字节码,被不同的类加载器加载之后所得到的类也是不同的。

比如:一个Javacom.example.Sample,编译之后生成了字节码文件Sample.class.使用两个不同的类加载器ClassLoaderAClassLoaderB分别读取了这两个Sample.class 文件,并定义出两个Java.lang.Class类的实例来表示这个类。这两个实例是不相同的。

对于JVM来说这两个不同的类相互赋值的时候会抛出运行时异常。ClassCastException

10.类加载器是如何加载Class文件的

第一个阶段是找到.class文件并把这个文件包含的字节码加载到内存中。
第二个阶段字节码验证、class类数据结构分析以及相应的内存分配和最后的符号表链接
第三个阶段是类中静态属性初始化和赋值,以及静态块的执行等

11 、双亲委派模型

双亲委派模型除了顶层的启动类加载器(BootStrap class loader)之外,其余的类加载器都应有自己的父类加载器。子类加载器和父类加载器不是以继承的关系来实现,而是通过组合(composition)关系来复用父类的代码。每个类加载器都有自己的命名空间(由该加载器及所有父类加载器所加载的类组成)。在同一个命名空间中不会出现类的完整名字相同的两个类。在不同命名空间中可能会出现类完整名字相同的两个类。

12、双亲委派模型的工作过程

1.当前ClassLoader 首先从自己已经加载的类中查询此类是否已经加载,如果已经加载则直接返回已经加载的类。每个类加载器都有自己的加载缓存。当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回。

2.当前classLoader的缓存中没有被加载的类时,委托父类加载器进行加载。父类加载器采用同样的策略,首先从缓存中查看,如果没有则委托它的父类去加载,一直到bootstrap classLoader 当所有父类加载器都没有加载的时候,再由当前的类加载器加载并将其放入自己的缓存中,以便下次有加载请求的时候直接返回。

13、如何破坏双器委派模型

如果不想打破双亲委派模型,就重写ClassLoader类中的findclass() 方法即可,无法被父类加载器加载的类最终会通过这个方法进行加载。如果想打破双亲委派模型则重写loadClass() 方法。典型的打破双亲委派模型的中间件的有tomcat

Tomcat 应用的类加载器优先进行自行加载应用目录下的class,并不是委派给父类加载器,这样做的目的是为了完成应用间的类隔离。

14、自定义类加载器

用户根据需求自定义的类加载器,需要继承classLoader重写方法findclass()。如果想要编写自己的类加载器之需要两步:继承classloader类,覆盖findclass方法。

15、 JVM中对象是如何创建的

虚拟机加载new指令字节码时,会检查这个指令的参数能否在常量池中定位到一个符号引用,并且检查这个符合引用所代表的类是否已经被加载、解析和初始化。

**如果未经过类加载,那么就执行相应的类加载过程。**类检查完成之后为新生对象分配内存。对象所需的大小在类加载完成之后便可确定。 内存分配相当于从堆中将一块固定的内存划分出来。JVM将划分出来的内存空间初始化为0值。如果使用了本地线程分配缓冲,这项初始化工作可以在TLAB分配时进行。这一步操作保证了对象实例字段在Java代码中可以不赋值就直接使用。

接下来JVM还会对对象进行必要的设置。比如确定对象时哪个类的实例、对象的hashcode、对象的gc分代年龄。这些信息存放在对象头中。(Object Header)中。

在这里插入图片描述
1.1 new 类名

虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,先执行相应的类加载过程。

1.2 分配内存

虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后就可以确定,为对象分配内存等同于把一块确定大小的内存从Java堆中划分出来。

(1)内存分配的方式有两种:

① 指针碰撞: java堆如果规整,一边是用过的内存,一边是空闲的内存,中间一个指针作为边界指示器; 分配内存只需向空闲那边移动指针空出与对象大小相等的空间;

② 空闲列表: 如果不规整,即用过的和空闲的内存相互交错;则虚拟机需要维护一个列表,记录哪些内存可用;分配内存时查表找到一个足够大的内存,并更新列表记录。
选择哪种分配方式是根据这个虚拟机所采用的垃圾收集器是否带有压缩整理功能决定的:如果虚拟机的虚拟器带压缩整理功能,则系统采用指针碰撞的内存分配算法;否则采用空闲列表的算法。

(2)线程安全问题

并发时,上面两种方式分配内存的操作都不是线程安全的,有两种解决方案:
① 同步处理
JVM采用CAS(Compare and Swap)机制加上失败重试的方式,保证更新操作的原子性;
CAS:有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做;
② 本地线程分配缓冲区(TLAB)
把分配内存的动作按照线程划分在不同的空间中进行:每个线程在Java堆预先分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB);哪个线程需要分配内存就从哪个线程的TLAB上分配;只有TLAB用完需要分配新的TLAB时,才需要同步处理。
JVM通过"-XX:+/-UseTLAB"指定是否使用TLAB。

1.3 初始化零值

内存分配完之后,虚拟机需要将分配到的内存空间都初始化为零值。如果用TLAB,则在TLAB分配时进行。这保证了程序中对象(及实例变量)不显式初始赋零值,程序也能访问到零值。

1.4 设置对象信息

虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、 如何才能找到类的元数据信息、 对象的哈希码、 对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。

1.5 构造对象

执行init方法,即按照程序员的意愿进行初始化。至此真正可用的对象才算完全被构造出来。

16、内存分配方式有哪些

内存分配方式有两种 :指针碰撞和空闲列表。
Java堆如果是规整的,使用过的内存放在一边,未使用过的放在另一边。中间通过指针做分界指示器。当为新对象分配内存空间就相当于把指针向空闲的空间挪动对象大小相等的距离。

如果java堆中的内存并不是规整的,已经被使用的内存和未被使用的内存相互交错在一起,这种情况下就没有办法使用指针碰撞。这里就要使用另外一种记录内存的方式:空闲列表(Free List)。空闲列表通过维护一个内存使用记录,来标识哪些空间可用。分配完成更新了空闲列表的记录。

在一些垃圾回收器的实现中Serial 、ParNew等带压缩整理过程的收集器使用的是指针碰撞;而使用CMS这种基于清除算法的收集器时,使用的是空闲列表。

17、对象一定分配在堆中吗

不一定。JVM中通过逃逸分析 哪些逃不出方法的对象会在栈上分配。
逃逸分析是指变量或者对象在方法中分配后,其指针有可能被返回或者被全局引用。这样就会被其他方法或线程所引用。通俗来讲如果一个对象的指针被多个方法或者线程引用时那么我们就称这个对象的指针发生了逃逸

栈上分配可以减少内存使用(因为不用生成对象头)内存回收效率高、GC的频率会减少。

18、 对象的内存布局

对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
在这里插入图片描述
对象头包括:mark word(运行时元数据)、KlassPointer(类型指针)。
在这里插入图片描述
32位JVM对象头的分配情况如图所示:
25bit用来存放hashcode、4bit用来存放分代年龄1bit用来存放是否偏向锁。2bit用来存放锁的标识位。
其中偏向锁的划分:23bit用来存放线程ID(ThreadId)2bit存放epoch值(epoch,其本质是一个时间戳,代表了偏向锁的有效性);

二、GC (Gabage Collection)垃圾回收

在这里插入图片描述

1.如何判断对象已死

(1)引用计数法 :引用计数法是在对象中添加一个引用计数器,每当一个地方引用它时,计数器的值就会加1;当引用失效时,引用计数器就会减一;只要任何时刻计数器为零的对象就是不会被使用的对象。但是不能解决对象直接的循环引用问题。所以主流的JVM实现并没有采用这种方式
(2)可达性分析
通过一系列的被称为GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索。搜索过程中走过的路径被称为引用链(Reference Chain)。如果从GC Roots到这个对象不可达,则证明这个对象是无用对象。

2.可以称为GC Roots的对象有哪些?

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象
(2)方法区中类静态属性引用的变量、方法区中常量引用的对象。
(3)被synchronized持有的对象
(4)本地方法栈中JNI(Java Native Interface)引用的对象
(5)JVM内部引用对象,eg:基本数据类型对应的Class对象,一些异常对象比如:NPE、OOM对象等还有类加载器等

3、Java中四种引用类型

(1)强引用:发生GC的时候不会被回收。是开发场景中使用最多的。当JVM内存空间不足时,抛出OOM
(2)软引用(soft Referance):有用但是不是必须的对象。在发生内存溢出之前会被回收。应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存就可以保留缓存。当内存不足时清理掉。
(3)弱引用(weak referance):有用但不是必须的对象,在下一次GC的时候会被回收。
(4)虚引用(Phantom Referance)实现虚引用。虚引用的用途是在GC时返回一个通知。

4、finalize 方法

1.如果对象在可达性算法中不可达,那么它会被第一次标记并进行一次筛选。筛选的条件是是否需要执行finalize方法。当对象没有覆盖finalize方法或者finalize方法已经执行过了,就不会被回收。

2.如果这个对象有必要执行finalize方法。那么对象会被放到F-Queue中。在二次标记时,如果在重写的finalize() 方法中将对象自己赋值给某个类变量或者对象的成员变量。那么会将它移出“即将回收”的集合

5.垃圾回收算法

GC最基础的算法有三种:标记-清除、复制、标记-压缩算法。我们常用的垃圾回收器一般都采用分代算法。

标记-清除算法:标记-清除算法(Mark-Sweep)分为标记和清除两个阶段。首先标记好需要清除的对象,在标记完成后统一清理。

复制算法:复制(copying)的收集算法,它将内存直接按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存使用完了之后,就将存活的对象复制到另外的一块上面。然后再把已经使用过的内存空间一次性清理掉。

标记-压缩:标记需要清除的对象之后,后续将存活对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:分代收集,把堆分为老年代和新生代。根据各个年代的特征使用不同的算法。

6、垃圾回收器

Serial收集器(复制算法):单线程垃圾回收器;
ParNew收集器(复制算法):多线程版本;
Parallel Scavenge(复制算法):新生代并行收集器,追求高吞吐,高效利用CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间)。高吞吐量可以高效率利用CPU时间。尽快完成程序的运算任务。适合后台应用等对交互响应要求不高的场景。

Serial Old (标记整理) 老年代单线程收集器;
Parallel Old(标记整理) 老年代并行收集器。
CMS(Concurrnet Mark Sweep) 收集器。老年代并行收集器,以获取最短停顿时间目标的收集器。

G1垃圾收集器(标记-整理算法):Java 堆并行收集器。G1收集器基于“标记整理”算法实现,也就是说不会产生内存碎片。G1垃圾回收器针对的是整个Java堆。

G1垃圾回收器将整个JVM堆划分为多个大小相等的独立区域regin。跟踪个各个regin里面的垃圾堆积的价值大小。在后台维护一个优先列表。每次根据允许的收集时间,优先回收最大的regin。

在GC根节点的枚举范围加入remembered set 可以保证不对全堆进行扫描。

回收步骤:
(1)初始标记:标记GC Roots直接关联的对象
(2)并发标记:对堆中对象进行可达性分析,找出存活对象,耗时长,与用户进程并发工作。
(3)最终标记:修正并发标记期间用户进程继续运行产生变化的标记
(4)筛选回收:对各个regin的回收价值进行排序,然后根据期望的GC停顿时间制定回收计划。

7 、触发Full GC的条件

(1)调用System.gc 时,系统建议执行Full GC。
(2)老年代空间不足
(3)通过MInor GC进入老年代的平均大小> 老年代的可用内存;
(4)由Eden区的 From space 向 To Space区复制时,对象大小大于To Space 可用内存。则把该对象转到老年代,且老年代的可用内存小于所需空间。就会触发Full GC。

8、垃圾回收过程

(1)在Eden区执行了第一次GC之后,存活的对象会被移动到其中一个Survivor分区。
(2)Eden区再次GC,这时会采用复制算法,将Eden和from区一起清理,存活的对象被复制到to 区。
(3)移动一次对象年龄+1,对象年龄到达阈值直接移动到老年代。
(4)survivor区相同年龄所有对象和>Survivior区的目标使用率时。大于或者等于该年龄的对象进入老年代。
(5)超过制定大小的对象直接在老年代分配内存
(6)Major GC 指的是老年到的垃圾收集器。但并未找到明确的说明何时进行的MajorGC;
(7)Full GC 整个堆的垃圾收集,触发条件1.每次晋升到老年代的对象平均大小>老年代剩余空间;System.gc;元空间不足;堆内存分配很大的对象;通过MInor GC进入老年代的平均大小> 老年代的可用内存;由Eden区的 From space 向 To Space区复制时,对象大小大于To Space 可用内存。则把该对象转到老年代,且老年代的可用内存小于所需空间。

9、内存泄漏和内存溢出

内存泄漏就是申请的内存空间没有被正确释放,新对象无法分配更多内存而导致内存溢出。

10、线上CPU飙高如何排查

1) 使用top命令查找对应使用CPU最多的进程记录下pid;
(2)top -H pid 显示Java进程pid 对应的线程id tid;
(3)printf %x tid 转为十六进制
(4)使用jstak工具把线程信息输出到对应的日志文件中。 jstak pid > pid.log
 (5) jstack pid | grep  -A 10 tid

11、如何解决线上GC频繁的问题

1.根据监控信息分析出现问题的时间点
2.首先确认是否有上线、基础组件升级等。
3.分析JVM参数设置是否合理
4.分析日志信息从元空间、内存泄漏、代码显示调用gc方法的角度去排查问题。
5.针对大对象或者长生命周期对象导致的FGC,结合dump堆内存文件的使用情况结合JVM参数设置,判断是否满足进入到老年代的条件

12、内存溢出的原因

(1)java.lang.outofmemoryError: java heap space 堆栈溢出;代码出问题的可能性极大
(2)java.lang.outofmemoryError:GC over head limit exceeded 系统处于高频GC的状态。且回收效果依然不佳。这种情况一般是产生了很多不可被释放的对象。
(3)java.lang.outofmemoryError: Direct buffer memory 直接内存不足;因为JVM垃圾回收器不会去清理直接内存的数据所以使用了直接内存后没有clear也会导致这个问题
(4)java.lang.stackoverflowError -xss 设置的太小了

三、JMM

四、java 线程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值