JVM

什么是JVM?

​ JVM是java虚拟机的缩写,本质上是一个程序,能识别.class字节码文件(.java文件编译后产生的二进制代码),并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。

​ 关于java语言的跨平台性(一次编译,多次运行),就是应为JVM,可以把它想象出一个抽象层,运行在操作系统之上的,与硬件没有直接的交互,只要这个抽象层JVM正确执行了.class文件,就能运行在各种操作系统之上了。

​ 介绍几个术语:

  • JDK:java开发工具包,JDK=JRE+javac/java/jar等指令工具
  • JRE:java运行环境,JRE=JVM+java基本类库

JVM体系结构

​ java虚拟机主要分为五大模块:

  • 类加载器
  • 运行时数据区
  • 执行引擎
  • 本地方法接口
  • 垃圾收集模块

​ 方法区是一种特殊的堆,栈里面不回有垃圾,用完就弹出了,否则阻塞了main方法。垃圾几乎都在堆里,所以JVM性能调优%99都针对与堆。

​ 目前最常用的JVM是Sun公司的HotSpot,此外还有BEA公司的JRockit和IBM公司的J9 VM。

类加载器

​ 作用:加载.class字节码文件。

new一个对象的过程

//运行时,JVM将Test的信息放入方法区
public class Test{
public static void main(String[] args){
Student s1 = new Student("Tom");//引用放在栈里,具体的实例放在堆里
Student s2 = new Student("Jerry");
Student s3 = new Student("Victor");
//三个hashCode是不同的,因为是三个不同的对象,对象是具体的
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
//class1,class2,class3为同一个对象,因为这是类模版,模版是抽象的
Class<? extends Stedent> class1 = s1.getClass();
Class<? extends Stedent> class2 = s2.getClass();
Class<? extends Stedent> class3 = s3.getClass();
System.out.println(class1.hashCode());
System.out.println(class2.hashCode());
System.out.println(class3.hashCode());
}
}
  1. 首先Class Loader读取字节码文件,加载初始化生成Student模版类。
  2. 通过Student模版类new出三个对象。

类加载器的类别

public class Test{
public static void main(String[] args){
Student s = new Student("Tom");
Class<? extends Student> c = s.getClass();
ClassLoader classLoader = c.getClassLoader();
System.out.println(classLoader);//APPClassLoader
System.out.println(classLoader.getParent());//PlatformClassLoader
System.out.println(classzLader.getParent().getParent());//null,获取不到(C++写的)
}
}

​ 根据返回结果,级别从高到低有三种加载器:

  1. 启动类(根)加载器:BootStrapClassLoader。

    • c++编写的,加载java核心库,构造拓展类加载器和应用程序加载器
    • 根加载器加载拓展类加载器,并且将拓展类加载器的父加载器设置为根加载器
    • 然后在加载应用程序加载器,应将应用程序的加载器的父加载器设置为拓展类加载器
    • 由于根加载器涉及到虚拟机本地实现的细节,我们无法直接获取到启动类加载器的引用,这就是上面第三个结果为null的原因
    • 加载文件存在于/jdk/jdk1.8/jre/lib/rt.jar
  2. 拓展类加载器:PlatformClassLoader
    • java编写,加载扩展库,开发者可以直接使用标准扩展类加载器
    • java9之前称为ExtClassLoader
    • 加载文件存在于.../lib/ext
  3. 应用程序加载器:AppClassLoader
    • Java编写,加载程序所在的目录,是java默认的类加载器
  4. 用户自定义加载器:CustomeClassLoader
    • java编写,用户自定义的类加载器,可加载指定路径的class文件

​ 实际上,这些加载器的区别就是加载不同范围或不同路径的.class文件。

双亲委派机制

​ 双亲委派机制是类加载器收到类加载的请求,会将这个请求向上委托给父类加载器去完成,一直向上委托,直到根加载器BootStrapClassLoader。根加载器检查是否能够加载当前类,能加载就结束,使用当前类加载器,否则就抛出异常,通知子加载器进行加载。

​ 举个例子,我们重写java.lang包下的String类:

package java.lang;
public class String{
public String toString(){
return "xing";
}
public static void main(String[] args){
new String().toString;
}
} //Error:(1,1) java:程序包已存在于另一个模块中:java:base

​ 我们会发现报错,这就是双亲委派机制起的作用,当类加载器委托到根加载器的时候,String类已经被根加载器加载过一遍了,所以不会再加载,从一定程度上防止了危险代码的植入。

作用总结:

  1. 防止重复加载同一个.class,通过不断委托父加载器直到根加载器,如果父加载器加载过了,就不用再加载一遍,保证数据安全。
  2. 保证系统核心.class不被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个class对象,这样保证了class执行安全。

沙箱安全机制

什么是沙箱

​ java安全模型的核心就是java沙箱(sandbox)。

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

​ 沙箱主要限制系统资源访问,包括CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也不一样。

​ 所有的java程序运行都可以指定沙箱,可以定制安全策略。

java中安全模型的演进

​ 在java中将执行程序分为本地代码和远程代码两种:本地代码可信任,可以访问一起本地资源。远程代码不可信任,在早期的java实现中,安全依赖于沙箱机制。

​ 如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统文件的时候,就无法实现。因此在后续的java1.1中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。

​ 在java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或者远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。

​ 当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有的代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域对应不一样的权限,存在于不同域中的类文件就具有了当前域的全部权限。

组成沙箱的基本组件

  1. 字节码校验器(bytecode verifier)

    确保java类文件遵循java语言规范。这样可以帮助java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

  2. 类装载器(class loader)

    类装载器在3个方面对java沙箱起作用

    • 防止恶意代码去干涉善意的代码
    • 守护了被信任的类库边界
    • 将代码归入保护域,确定了代码可以进行哪些操作。

    虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由java虚拟机为每一个类装载器维护的,他们互相之间甚至不可见。

  3. 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定可以由用户指定。

  4. 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比如存取控制器优先级高。

  5. 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括安全提供者、消息摘要、数字签名、加密、鉴别。

Native本地方法接口

​ JNI:java native interface

​ 本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序。

​ 凡是带native关键字的,就说明java的作用范围达不到了,会去调用底层c语言库,进入本地方法栈,调用本地方法接口JNI,拓展java的使用,融合不同的语言为java所用。

​ java诞生的时候C/C++横行,为了立足,必须要能够调用C/C++程序,于是在内存区域中专门开辟了一块标记区域:Native Method Stack,登记Native方法,最终在执行引擎上执行的时候通过JNI加载本地方法库中的方法。目前该方法的使用越来越少了,除非是与硬件有关的应用,比如通过java程序驱动打印机或者java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以用Socket通信,也可以使用Web Service等。

运行时数据区

PC寄存器(Program Counter Register)

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

方法区(Method Area)

​ 方法区与java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器遍以后的代码等数据。虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名Non-Heap,因此实际上应该和堆区分开。

方法区中有啥?

  • 静态变量(static)
  • 常量(final)
  • 类信息(构造方法,接口定义)
  • 运行时的常量池

创建对象内存分析

public class Person{
int age;
String name = "xing";
public Person(int age, String name){
this.age = age;
this.name = name;
} public static void main(String[] agrs){
Person s1 = new Person(18,"Tom");
}
}
/*
创建一个对象时,方法区中会生成对应类的抽象模版;还有对应的常量池、静态变量、类信息、常量。
我们通过类模版去new对象的时候,堆中存放实例对象,栈中存放对象的引用,每个对象对应一个地址指向堆中相同地址的实例对象。
*/

​ 主管程序的运行,生命周期和线程同步。线程结束,栈内存就释放了,不存在垃圾回收。栈中存放8大基本类型,对象引用,实例的方法。

栈运行的原理

​ 栈表示java方法执行的内存模型,每调用一个方法就会为每个方法生成一个栈帧(Stack Frame),每个方法被调用的完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。程序正在执行的方法一定在栈的顶部。

堆栈溢出(StackOverflowError)

public class Test{
public static void main(String[] args){
new Test().a();
} public void a(){
b();
}
public void b(){
a();
}
} //最开始,main()方法压入栈中,然后执行a(),a()押入栈中,在调用b(),b()押入栈栈中,以此往复,最终导致栈溢出

​ 一个JVM只有一个堆内存(栈是线程级的),堆内存的大小是可以调节的,堆中存放实例化的对象。

堆内存详解

  1. 年轻代

    对象的诞生、成长甚至死亡的区

    • Eden Space(伊甸园区):所有对象都是在此new出来的
    • Survivor Space(幸存区)
      • 幸存0区(From Space),动态的From和To会互相交换
      • 幸存1区(To Space)

    Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。

  2. 老年代

  3. Perm元空间

    存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收。关闭虚拟机就会释放这个区域的内存,这个区域常驻内存,用来存放JDK自身携带的Class对象、Interface元数据。jdk1.8之前被称为永久代。

    注意:元空间在逻辑上存在,在物理上不存在。新生代+老年代的内存空间=JVM分配的总内存。

什么是OOM

​ 内存溢出,产生原因:

  • 分配的太少
  • 用的太多
  • 用完没释放

GC垃圾回收

​ 主要在年轻代和老年代。

​ 首先对象出生在伊甸园区,假设伊甸园区只能存在一定数量的对象,则每当存满时就会出发一次轻GC(Minor GC)。轻GC清理后,有的对象可能还存在引用,就活下来了,活下来的对象就进入幸存区;有的对象没用了,就被GC清理掉了;每次轻GC都会使得伊甸园区为空。

​ 如果幸存区和伊甸园区都满了,则会进入老年代,如果老年代满了,就会出发一次重GC(FullGC),年轻代+老年代的对象都会清理一次,活下来的对象都进入老年代。

​ 如果新生代和老年代都满了,则OOM。

  • Minor GC:伊甸园区满时触发,从年轻代回收内存
  • Full GC:老年代满时触发,清理整个堆空间
  • Major GC:清理老年代

​ 什么情况下永久区会崩?一个启动类加载了大量的第三方jar包,Tomcat部署了过多应用,或者大量动态生成的反射类,这些东西不断的被加载,知道内存满,就会出现OOM。

堆内存调优

查看并设置JVM堆内存

public class Test{
public static void main(String[] args){
//返回jvm试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
//返回jvm的初始化内存
long total = Runtime.getRuntime().totalMemory();
//默认情况下:分配的总内存为电脑内存的1/4,初始化内存为电脑内存的1/64
System.out.println("max=" + max / (double) 1024 / 1024 / 1024 + "G");
System.out.println("total=" + total / (double) 1024 / 1024 / 1024 + "G");
}
}

​ 我们可以手动调整堆内存的大小,在VM options 中可以指定jvm试图使用的最大内存和jvm初始化内存的大小。

-Xms1024m -Xmx1024m -Xlog:gc*
  • -Xms用来设置jvm试图使用的最大内存
  • -Xmx用来设置jvm初始化内存
  • -Xlog:gc*用来打印GC垃圾回收信息

怎么排除OOM错误?

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

  2. 利用内存快照工具JProfiler

    作用:分析Dump内存文件,快速定位内存泄漏;获得堆中的文件;获得大的对象

    Dump文件是进程的内存镜像,可以把程序的执行状态通过调试器保存到dump文件中

    import java.util.ArrayList;
    
    public class Test{
    byte[] array = new byte[1024*1024];//1M
    public static void main(String[] args){
    ArrayList<Test> list = new ArrayList<>();
    int count = 0;
    try{
    while(true){
    list.add(new Test());
    count++;
    }
    }catch(Exception e){
    System.out.println("count="+count);
    e.printStackTrace();
    }
    }
    }

    运行程序,报错OOM。

    接下来设置一下堆内存并附加生成dump文件的指令

    -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

    -XX:+HeapDumpOnOutOfMemoryError表示当JVM发生OOM时,自动生成DUMP文件。再次点击运行,下载了对应的Dump文件。

    分析步骤:

    • 右键该类,点击Show in Explorer
    • 一直点击上级目录,直到找到.hprof文件

    每次打开dump文件查看完后,建议删除,打开文件后生成了很多内容,占内存。

GC垃圾回收

​ 之前已经堆GC垃圾回收流程进行了大概的讲解:JVM在进行GC时,大部分回收都是在年轻代。

GC算法

  1. 引用计数法(很少使用)

    • 每个对象在创建的时候,就给这个对象绑定一个计数器。
    • 每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一;
    • 这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。
  2. 复制算法

    复制算法主要发生在年轻代(幸存0区和幸存1区)

    • 当Eden区满的时候,会触发轻GC,每触发一次,活的对象就被转移到幸存区,死的对象就被GC清理掉,所以每次触发轻GC时,Eden区就会清空
    • 对象被转移到了幸存区,幸存区又分为From SpaceTo Space,这两块区域是动态交换的,谁是空的谁就是To Space,然后From Space就会把全部对象转移到To Space去;
    • 那如果两块区域都不为空呢?这就用到了复制算法,其中一个区域会将存活的对象转移到另一个区域去,然后将自己区域的内存空间清空,这样该区域为空,又成为了To Space
    • 所以每次触发轻GC后,Eden区清空,同时To区也清空了,所有的对象都在From区

    好处:没有内存碎片

    坏处:浪费内存空间(浪费幸存区一半的空间);对象存活率较高的场景下,需要复制的东西太多,效率会下降。

    最佳使用环境:对象存活率较低的时候,也就是年轻代。

  3. 标记-清除算法

    为每个对象存储一个标记位,记录对象的生存状态。

    • 标记阶段:这个阶段内,为每个对象更新标记位,检查对象是否死亡。
    • 清除阶段:该阶段对死亡的对象进行清除,执行GC操作。

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

    优点:不需要额外的空间

  4. 标记-整理算法

    这个是标记-清除算法的一个改进版,又叫做标记-清除-压缩算法。不同的是在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存货的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。可以进一步优化,在内存碎片不太多的情况下,就继续标记清除,到达一定量的时候再压缩。

有没有最优的算法?

​ 没有最优算法,只有最合适的。

​ GC也称为分代收集算法,对于年轻代,对象存活率低用复制算法;对于老年代,区域大,对象存活率高,用标记清除+标记压缩混合实现。

聊一聊JVM的更多相关文章

  1. 聊一聊 JVM 的 GC

    原文链接:https://www.changxuan.top/?p=1457 引言 JVM 中的 GC 在技术博客中应该算是个老生常谈的话题,网络上也存在着许多质量参差不齐的文章,可以看出来大都是&q ...

  2. JVM垃圾回收详解

    通常,我们在写java程序的时候,似乎很少关注内存分配和垃圾回收的问题.因为,这部分工作,JVM已经帮我们自动实现了. 这样看起来,好像很美好,但是任何事情都有两面性.虽然JVM会自动的进行垃圾回收, ...

  3. JVM 面试题,安排上了!!!

    肝了一篇非常硬核的 JVM 基础总结,写作不易,小伙伴们赶紧点赞.转发安排起来! 原文链接 据说看完这篇 JVM 要一小时 JVM 的主要作用是什么? JVM 就是 Java Virtual Mach ...

  4. JVM基础系列第8讲:JVM 垃圾回收机制

    在第 6 讲中我们说到 Java 虚拟机的内存结构,提到了这部分的规范其实是由<Java 虚拟机规范>指定的,每个 Java 虚拟机可能都有不同的实现.其实涉及到 Java 虚拟机的内存, ...

  5. JVM锁实现探究2:synchronized深探

    本文来自网易云社区 作者:马进 这里我们来聊聊synchronized,以及wait(),notify()的实现原理. 在深入介绍synchronized原理之前,先介绍两种不同的锁实现. 一.阻塞锁 ...

  6. 聊一聊Java字符串的不可变

    前言 在 Java 开发中 String (字符串)对象是我们使用最频繁的对象,也是很重要的对象.正是使用得如此频繁,String 在实现层面上不断进行优化,从 Java6 到 Java7,再到 Ja ...

  7. 初步了解JVM第一篇

    大家都知道,Java中JVM的重要性,学习了JVM你对Java的运行机制.编译过程和如何对Java程序进行调优相信都会有一个很好的认知. 废话不多说,直接带大家来初步认识一下JVM. 什么是JVM? ...

  8. 初步了解JVM第二篇

    在一篇<初步了解JVM第一篇>中,我们已经了解了: 类加载器:负责加载*.class文件,将字节码内容加载到内存中.其中类加载器的类型有如下: 启动类加载器(Bootstrap) 扩展类加 ...

  9. 一文学会JVM配置参数与工具使用

    经过前面的各种分析,我们知道了关于JVM很多的知识,比如版本信息,类加载,堆,方法区,垃圾回收等,但是总觉得心里不踏实,原因是没看到实际的一些东西. 所以这在本文,咱们就好好来聊一聊关于怎么将这些内容 ...

随机推荐

  1. Scala面向对象—类详解2(继承相关)

    1.单例类 package com.zzzy class AAA {//单例 /*//java 思路--私有化构造方法,提供公开的getAAA 行不通 private def this(){ this ...

  2. CF 1288 E. Messenger Simulator

    CF 1288 E. Messenger Simulator 题目传送门 官方题解 题意想必大家都明白了这里就不赘述了,这里只想重点记录一下几种实现方法 分析 设向前移动的序列为\(a\)序列 对于没 ...

  3. hdu3706 Second My Problem First

    Problem Description Give you three integers n, A and B.  Then we define Si = Ai mod B and Ti = Min{ ...

  4. 牛客编程巅峰赛S1第11场 - 黄金&钻石 B.新集合 (DFS)

    题意:有\([1,n]\)这\(n\)个数,构造集合,集合中不能包含\(u\)和\(v\),问最多能构造多少个集合. 题解:被这题卡了一整场.....以为是推公式,结果答案是暴搜? ​ 首先我们先用一 ...

  5. ElasticSearch 交互使用

    Curl 命令 # 建立索引 [root@dbtest01 ~]# curl -XPUT 'http://10.0.0.121:9200/test' # 插入数据 [root@dbtest01 ~]# ...

  6. 洛谷p1981 表达式求值

    #include <iostream> #include <cstdio> #include <cstring> using namespace std; char ...

  7. codevs1154能量项链 环形区间DP 细节

    中文题..题意略 我们知道每次枚举最后合并哪两个.. 于是枚举中间节点k 我犯的错误是将转移方程写成了,dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]+a[l]*a ...

  8. 输入函数input()、运算符

    一.input()函数的基本使用 present = input('大圣想要什么礼物') 作用:接受来自用户的输入 返回值类型:输入值的类型为str 值的存储:使用 = 对输入的值进行存储 name= ...

  9. [USACO15JAN]Moovie Mooving G

    [USACO15JAN]Moovie Mooving G 状压难题.不过也好理解. 首先我们根据题意: she does not want to ever visit the same movie t ...

  10. Vue Login Form Component

    Vue Login Form Component Account Login <template> <div> <slot></slot> <el ...