JVM总结归纳

JVM介绍

说明:本文章属于个人学习归纳总结,其中内容有摘自他人博客内容,严禁转载。

1 jvm介绍

jvm是运行在操作系统之上的,与硬件系统没有直接的交互。

1.1 程序计数器

在CPU的寄存器中只有一个pc寄存器,存放下一条指令地址。每一条线程都有一个独立的程序计数器,Java虚拟机中的程序计数器指向正在执行的字节码地址,而不是下一条。

1.2 虚拟机栈

虚拟机栈是线程私有的,每个方法执行的时候都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法调用从调用到执行完成的过程都对应着一个栈帧在虚拟机中的入栈到出栈的过程。局部变量表存放了编译期可以知道的基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用(可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置),和返回后所指向的字节码的地址。其中64 位长度的long 和double 类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个 。

1.3 本地方法栈

​ 在HotSpot虚拟机将本地方法栈和虚拟机栈合二为一,它们的区别在于,虚拟机栈为执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

1.4 Java堆

​ Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域是用来存放对象实例的,几乎所有对象实例都会在这里分配内存。堆是Java垃圾收集器管理的主要区域(GC堆),垃圾收集器实现了对象的自动销毁。Java堆可以细分为:新生代和老年代;再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。可以通过-Xmx和-Xms控制 。

1.5 方法区

​ 方法区也叫永久代。在过去,类大多是“static”的,很少被卸载或收集,因此被称为“永久的”。由于类class是JVM实现的一部分,并不是由应用创建的,所以又被认为是“非堆(non-heap)”内存。

​ 永久代也是各个线程共享的区域,它用于存储已经被虚拟机加载过的类信息,常量,静态变量(JDK7中被移到Java堆),即时编译期编译后的代码(类方法)等数据。这里要讲一下运行时常量池,它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用(其实就是八大基本类型的包装类型和String类型数据(JDK7中被移到Java堆)) 。

​ 在JVM中共享数据空间划分如下图所示

上图中,刻画了Java程序运行时的堆空间,可以简述成如下2条

1.JVM中共享数据空间可以分成三个大区,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分为新生代和老年代

2.新生代可以划分为三个区,Eden区(存放新生对象),两个幸存区(From Survivor和To Survivor)(存放每次垃圾回收后存活的对象)

3.永久代管理class文件、静态对象、属性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information )

4.JVM垃圾回收机制采用“分代收集”:新生代采用复制算法,老年代采用标记清理算法。

作为操作系统进程,Java 运行时面临着与其他进程完全相同的内存限制:操作系统架构提供的可寻址地址空间和用户空间。

2 类加载

2.1 类加载示意图

加载-》链接(验证-》准备-》解析)-》初始化-》使用-》卸载的过程。

2.1 加载

​ 将class文件信息加载到内存,有硬盘移到内存。

2.2 链接

2.2.1 验证

​ 1.验证字节流是否符合class文件格式的规范。

​ 2.对类的元数据进行语义校验。

2.2.2 准备

​ 1.为类的静态变量分配内存,并将其初始化默认值。

​ 2.给常量分配内存,并设置值。

2.2.3 解析

将常量池中的符号引用转化成直接引用的过程。符号引用就是class文件中的:

  • CONSTANT_Class_info

  • CONSTANT_Field_info

  • CONSTANT_Method_info

    等类型常量。

2.3 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段, 开始真正执行类中定义的Java程序代码。

注意以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

  • 定义对象数组,不会触发该类的初始化。

  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

  • 通过类名获取Class对象,不会触发类的初始化。

  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

3 类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。

  • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。dk中的ClassLoader的源码实现:

  • 首先通过Class c = findLoadedClass(name);判断一个类是否已经被加载过。

  • 如果没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型,首先会通过递归从父加载器开始找,直到父类加载器是Bootstrap ClassLoader为止。

  • 最后根据resolve的值,判断这个class是否需要解析。

而上面的findClass()的实现如下,直接抛出一个异常,并且方法是protected,很明显这是留给我们开发者自己去实现的。

定义类加载器步骤

  (1)继承ClassLoader (2)重写findClass()方法 (3)调用defineClass()方法

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
​
public class MyClassLoader extends ClassLoader{
​
    private String classpath;
    
    public MyClassLoader(String classpath) {
        
        this.classpath = classpath;
    }
​
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte [] classDate=getDate(name);
            
            if(classDate==null){}
            
            else{
                //defineClass方法将字节码转化为类
                return defineClass(name,classDate,0,classDate.length);
            }
            
        } catch (IOException e) {
            
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }
    //返回类的字节码
    private byte[] getDate(String className) throws IOException{
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path=classpath + File.separatorChar +
                    className.replace('.',File.separatorChar)+".class";
        try {
            in=new FileInputStream(path);
            out=new ByteArrayOutputStream();
            byte[] buffer=new byte[2048];
            int len=0;
            while((len=in.read(buffer))!=-1){
                out.write(buffer,0,len);
            }
            return out.toByteArray();
        } 
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        finally{
            in.close();
            out.close();
        }
        return null;
    }
}
public static void main(String []args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException{
        //自定义类加载器的加载路径
        MyClassLoader myClassLoader=new MyClassLoader("D:\\lib");
        //包名+类名
        Class c=myClassLoader.loadClass("com.test.Test");
        
        if(c!=null){
            Object obj=c.newInstance();
            Method method=c.getMethod("say", null);
            method.invoke(obj, null);
            System.out.println(c.getClassLoader().toString());
        }
    }

4 垃圾回收

4.1 垃圾回收时机

1.当Eden中没有足够的内存空间时,发生一次GC,频繁,快

2.老年代中没有足够的空间,发生Full GC

4.2 计算策略

垃圾回收判断方法 原理
1.引用计数 对象被引用,计数+1,退出引用-1,引用计数为0时,垃圾收集,很难解决对象之间相互循环引用的问题。
2.根(GC Roots)搜索法 通过一系列名为GC Roots(GC根节点)的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径(GC不可达这个对象),此对象不可用。

根搜索中的根:

序列 可作为GC Roots 的对象
1 虚拟机栈(栈帧中本地变量表)中引用的对象
2 方法区中的静态属性引用的对象
3 方法区中常量引用的对象
4 本地方法栈JNI中引用的对象

4.3 垃圾收集器

垃圾收集器 描述
1.串行收集器 暂停所有应用的线程来工作,单线程
2.并行收集器 默认垃圾收集器,暂停所有应用,多线程
3.G1收集器 用于大堆区域,堆内存分隔,并发回收
4.CMS收集器 多线程扫描,标记需要回收的实例,清除

4.4 垃圾回收算法

算法 概述 影响
标记清除法 分为两个阶段,标记和清除。标记出需要回收的对象,清除被标记的对象所占用的空间 碎片化严重
复制算法 将内存分为大小相等的两块,每次只是用其中的一块,当一块内存满后,将尚存活的对象移到另一半内存,把使用内存清掉 内存压缩减半
标记整理法 标记后不清除对象,将存活的对象移到内存一端,清除边界外的对象  
分代收集算法 根据对象存活的不同生命周期,将内存划分为不同的区域(Eden和Survivor区),回收时,将存活对象复制到另一块Survivor区(Jvm默认) 新时代采用复制算法,老年代采用标记清除法

4.5 常见配置参数

​ JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制 。

4.5.1 常见配置汇总:

堆参数 参数描述
-Xms 初始堆大小
-Xmx 最大堆大小
-XX:NewSize=n 设置年轻代大小
-XX:newRatio=n 设置年轻代和老年代的比值。如:为3,表示年轻代与老年代比值为1:3
-XX:SurivorRation=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。 如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n 设置持久代大小
收集器参数 描述
-XX:+UseSerialGC 设置串行收集器
-XX:UseParallelGC 设置并行收集器
-XX:+UseParalledIOldGC 设置并行老年代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器

5 JVM调优参数

JVM是运行在一个独立的进程中的,但它可以并发的执行多个线程,每个线程都运行自己的方法。

调优参数
使用-XX:NewRatio,指定新生代和老年代的比例,默认:-XX:NewRatio=2,老年代占用对空间的2/3,新生代1/3
使用-Xmn,设定初始化和最大新生代大小,默认系统内存1/4,小于1GB
使用-XX:NewSize和-XX:MaxNewSize,设定初始化和最大新生代大小,堆中剩余大小就是老年代的大小
使用-Xss,设置线程中栈大小,范围320k-1024k之间
  • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

  • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

6 Java中内存泄露

6.1 内存泄露的原因

内存泄露是指无用对象(不在使用的对象)持续占有内存而得不到及时释放,从而造成内存空间的浪费,成为内存泄露。

长生命周期的对象持有短生命周期对象的引用,导致短生命周期的对象已经不在需要,不能被及时回收。

6.2 内存泄露的情况

  1. 静态集合引起的内存泄露(HashMap,Vector)

    静态变量的生命周期和应用程序一致,它们所引用的对象Object 不能被及时释放,因为它们一致被Vector等引用。

  2. 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。

  3. 监听器:

    在释放内存的时候,没有删除监听器,增加了内存泄露。

  4. 各种连接

    数据库连接,网络连接和IO连接,没有显示的调用Close()将其关闭,否则不会被GC回收。

猜你喜欢

转载自blog.csdn.net/qq_31108731/article/details/82079642
今日推荐