关于JVM

Java Virtual Machine

JVM特性

平台无关性

Java虚拟机是实现Java平台无关性的关键,引入JVM后,Java语言在不同平台上运行时不需要重新编译。JVM在执行字节码时,把字节码解释成具体平台上的机器指令执行。

JVM的位置

JVM位置

JVM的体系结构

JVM体系结构

  • 栈、本地方法栈、程序计数器肯定不会有垃圾回收

  • 程序计数器

    唯一一个在JVM中没有任何OOM情况的区域

    非常小的内存空间

    每个线程都是有一个程序计数器的,是线程私有的,相当于一个指针

  • 小总结

    Java文件经过编译后变成.class字节码文件

    字节码文件通过类加载器被搬运到JVM中

    JVM主要的五部分:

    ​ 方法区,堆为线程共享区域,有线程安全问题

    ​ 栈、本地方法栈、程序计数器是独享区域,不存在线程安全问题

    JVM调优99%是在调方法区和堆,大部分是堆

JVM、JRE、JDK三者关系

JDK是常用的开发包,用于编译和调试Java程序的

JRE是Java运行环境,写好的程序必须在JRE才能运行

JVM是虚拟机,将字节码解释成为特定的机器码进行运行(需要先编译为.class文件,否则JVM不认识)

类加载器(类装载器)ClassLoader

作用:加载Class文件,将编译后的.class文件加载内存当中

类加载器只负责class文件的加载,是否能够运行则由Execution Engine来决定

Car car1 = new Car(); //名字在栈里面,引用的对象在堆里面

类加载器

类加载器的加载顺序
  1. 启动类(根)加载器Bootstrap classLoader 由C/C++实现,主要负责加载核心的类库 rt.jar

  2. 扩展类加载器Extension ClassLoader 主要负责加载jre/lib/ext目录下的一些扩展的jar

  3. 应用程序加载器Application ClassLoader 主要负责加载应用程序的主函数类

  4. 自定义的类加载器Custom ClassLoader

    加载时是按顺序一层一层往上找的,先找应用程序加载器(双亲委派机制)

public class Car{
  public static void main(String[] args){
    //类是模版,对象是具体的
    Car car1 = new Car();
    Car car2 = new Car();
    Car car3 = new Car();
    
    System.out.println(car1.hashCode());//11632....
    System.out.println(car2.hashCode());//195672...
    System.out.println(car3.hashCode());//356573...
    
    Class<? extends Car> aClass1 = car1.getClass();
    Class<? extends Car> aClass2 = car1.getClass();
    Class<? extends Car> aClass3 = car1.getClass();
    
    System.out.println(aClass1.hashCode());//460141958
    System.out.println(aClass2.hashCode());//460141958
    System.out.println(aClass3.hashCode());//460141958
    
    ClassLoader classLoader = aClass1.getClassLoader();
    
    System.out.println(classLoader);//AppClassLoader
    
    System.out.println(classLoader.getParent());//ExtClassLoader   /jre/lib/ext
    
    System.out.println(classLoader.getParent().getParent());//null    1.不存在 2.Java程序获取不到(底层是C或者C++写的) rt.jar
    /*
    为什么会有调用native方法?
    因为JVM的下一层是操作系统,JVM无法获取,所以要调用native方法
    */
  }
}
package java.lang;

public class String{//与根加载器的类同名、同方法
  
  public String toString(){
    return "Hello";
  }
  
  public static void main(String[] args){
    String s = new String();
    s.toString();
  }
}
//运行会报错说String方法里面没有main方法
//原因如下:
/*
双亲委派机制:安全
运行时:APP-->EXT-->BOOT(rt.jar)
寻找该类,BOOT中有String方法,所以运行BOOT中的而不是上面写的
如果没有,则一层一层向下寻找
BOOT-->EXT-->APP的顺序
1. 类加载器收到类加载的请求
2. 将这个请求向上委托给父类加载器,一直向上,直到启动类加载器
3. 启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器
	否则,抛出异常,通知子加载器进行加载 Class Not Found~
4. 重复步骤3
*/
双亲委派机制

优点:避免重复加载+避免核心类被篡改

当一个类加载器收到一个类加载的请求,会先将该请求委派给父类加载器去加载,直到Bootstrap ClassLoader,只有当父类加载器反馈无法完成这个类的加载请求时,子类加载器才尝试加载。

双亲委派流程图

在JVM中判断两个类是否是同一个类取决于类加载器和类本身,类加载器不同,那么加载的两个类一定不相同。

沙箱安全机制

Java安全模型的核心就是Java沙箱sandbox。沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。

运行时数据区

本地方法栈

native:凡是带了native关键字的,说明java的作用范围达不到了,回去调用底层C语言的库

会进入本地方法栈,调用本地方法本地接口(JNI:Java native interface)

JNI的作用:扩展Java的作用,融合不同的编程语言为Java所用,因为底层是C、C++写的

在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native方法,在最终执行的时候,通过JNI加载本地方法库中的方法

程序计数器 Program Counter Register

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),再执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

如果执行的是native方法,那这个指针就不工作了

方法区Method Area

方法区被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间。

静态变量static、常量final、类信息Class(构造方法、接口定义)、运行时的常量池存在方法区中

BUT,实例变量存在堆内存中,和方法区无关

创建对象内存分析👇
public class Pet{
  public String name; //name:null
  public int age;//age:0
  
  public void shout(){
    System.out.println("shout at you!");
  }
}

public class Application{
  public static void main(String[] args){
    Pet dog = new Pet();
    dog.name = "Jack";
    dog.age = 3;
    dog.shout();
    
    Pet cat = new Pet();
    
  }
}

创建对象内存分析

  1. 系统启动一个JVM进程,找到.class的二进制文件,将类信息加载到运行时数据区的方法区内
  2. JVM找到主程序入口,执行main方法
  3. 创建一个Pet对象,但是方法区没有Pet类的信息,JVM此时加载Pet类,把信息放到方法区中
  4. 在堆中为一个新的Pet实例分配内存,然后初始化Pet实例,此时实例持有指向方法区中的Pet类的类型信息的引用
  5. 根据引用找到dog对象,根据dog对象持有的引用定位到方法区中Pet类的类型信息的方法表,获得方法的字节码地址
  6. 执行shout()方法
  • 方法运行完了就被栈弹出去
  • 对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法

栈也叫栈内存,负责Java程序的运行,是在线程创建时创建的,生命周期和线程的生命周期一致,同时消亡,线程结束了栈就释放了,所以栈不存在垃圾回收

栈管运行,堆管存储

一种数据结构 程序 = 数据结构 + 算法

栈:先进后出,后进先出

队列:先进先出,后进后出FIFO

  • 为什么main()最先运行,最后结束?

    栈先进后出

  • 递归为什么可能出现栈溢出?

    public class Test{
      public void a(){
        b();
      }
      public void b(){
        a();
      }
    }
    //在栈里面,a()先压入栈,a中调用b,b()被压入栈,b调用a,a()被压入栈。。。
    //StackOverFlowError!
    
  • 栈内存,主管程序的运行,生命周期和线程同步

    线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收

  • 8大基本类型+对象引用+实例的方法都在栈中分配内存

  • 栈运行原理:栈帧(每执行一个方法,就产生一个栈帧)

    程序正在执行的方法,一定在栈的顶部

  • 栈+堆+方法区的交互关系

堆Heap

一个JVM只有一个堆内存,堆内存的大小是可以调节的,堆被整个JVM的所有线程共享

JVM内存划分为堆内存(年轻代Eden,Survivor和老年代)和非堆内存(永久代)

非堆内存就是方法区

JDK1.8中已经移除永久代,替代品是元空间MetaSpace,metaSpace是不存在于JVM中的,使用的是本地内存

  • 类加载器读取了类文件后,一般会把什么东西放入堆中?

    类、方法、常量、变量、保存所有引用类型的真实对象

  • 堆内存还要细分为三个区域:

    • 新生代Young/New (伊甸园区+幸存区0区+幸存区1区)
    • 老年代Old
    • 永生代Perm

    GC垃圾回收,主要是在伊甸园区(Eden Space)和老年代

    伊甸园:轻GC

    老年代:重GC(Full GC)

OOM:OutOfMemory是堆内存满了

新生代

对象诞生和成长的地方,甚至死亡

伊甸园区:所有的对象都是在伊甸园区new出来的

幸存区(0,1区):伊甸园区满了就会触发轻GC,还持有引用的对象存活下来,进入幸存0区

幸存0区满了触发Minor GC,将幸存对象移动到1区,并将from和to的指针交换

当一个对象经历了15次(默认值,可以改,最大为15)GC还没有死,就会进入老年区

如果老年区也满了,就会触发重GC

老年代在Full GC之后还无法进行操作,就是OOM了

永久代

这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,java运行时的一些环境

不存在垃圾回收!关闭虚拟机就会释放这个区域的内存

  • jdk1.6之前:永久代,常量池在方法区中

  • jdk1.7 :永久代,逐渐退化,去永久代,常量池在堆中

  • Jdk1.8之后:无永久代,常量池在元空间中

  • 什么情况会在永久区出现OOM?

    一个启动类加载了大量的第三方jar包

    Tomcat部署了太多的应用

    大量动态生成的反射类不断被加载

元空间是堆里面一个特殊的部分,为了和堆区分开,也叫非堆

方法区是元空间的一小部分,常量池是方法区中的一部分

元空间:逻辑上存在,物理上不存在

Q:遇到OOM怎么办

OOM是堆内存满了

  1. 尝试把堆内存扩大看结果

    -Xms1024m -Xmx1024m -XX:+PrintGCDetails

  2. 如果还满,那就是有垃圾代码或者死循环代码

GC:垃圾回收

作用区域:只有堆和方法区

JVM在进行GC时,并不是对所有区域统一回收,大部分时候回收都是新生代

轻GC:伊甸园区,幸存区

重GC:老年区

  • 判断无用的类要符合以下三个条件
    • 该类的所有实例都已经回收,Java堆中不存在该类的任何实例
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
GC算法:引用计数法

引用计数法

计数器本身也有消耗

对象多的话,方法并不高效,并且不能解决循环引用的问题

所以用的比较少

GC算法:复制算法

幸存区0区和1区由于经常互换位置,也叫from区和to区,谁空谁是to

新生区主要用复制算法

从其中一个幸存区复制到另一个幸存区,保证有一个幸存区是空的

Eden区活下来的也复制到幸存区to区

图示过程

好处:没有内存的碎片

坏处:浪费内存空间(有一半空间永远是空的)

复制算法最佳使用场景:对象存活度较低的地方,即新生区

GC算法:标记清除算法

图示

缺点:两次扫描,浪费时间,会产生内存碎片

优点:不需要额外的空间

GC算法:标记压缩

再优化:多了一次扫描,多了一个移动成本,但是减少了内存碎片

图示

在标记清除压缩方法基础上调优:

先多几次标记清除,然后再进行压缩

GC算法总结:

内存效率:复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

Q:有没有最优算法?

A:没有最好的算法,只有最合适的算法----->

GC:分代收集算法

年轻代:

  • 存活率低
  • 复制算法

老年代:

  • 区域大
  • 存活率高
  • 标记清除+标记压缩混合实现 (JVM调优:清多少次再压缩,内存碎片到一定量再压缩)

关于JVM调优

线程共享数据区大小= 新生代大小+ 老年代大小+ 持久代大小(一般固定为64M)

如果老年代过小,会增多Full GC,推荐为Java堆的3/8

推荐Eden : Survivor0 : Survivor1 = 8 : 1 : 1

posted @ 2021-02-03 14:57  GladysChloe  阅读(77)  评论(0)    收藏  举报