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. 【uva 1395】Slim Span(图论--最小生成树+结构体快速赋值 模版题)

    题意:给一个N(N<=100)个点的联通图(无自环和平行边),求苗条度(最大边-最小边的值)尽量小的生成树. 解法:枚举+Kruskal.先从小到大排序边,枚举选择的最小的边. 1 #inclu ...

  2. Codeforces Round #501 (Div. 3) D. Walking Between Houses (思维,构造)

    题意:一共有\(n\)个房子,你需要访问\(k\)次,每次访问的距离是\(|x-y|\),每次都不能停留,问是否能使访问的总距离为\(s\),若能,输出\(YES\)和每次访问的房屋,反正输出\(NO ...

  3. A - 你能数的清吗 51Nod - 1770

    题目: 演演是个厉害的数学家,他最近又迷上了数字谜.... 他很好奇  xxx...xxx(n个x)*y 的答案中 有多少个z,x,y,z均为位数只有一位的整数. 大概解释一下: 22222*3 = ...

  4. C#线程Thread类

    在System.Threading 命名空间下,包含了用于创建和控制线程的Thread 类.对线程的常用操作有:启动线程.终止线程.合并线程和让线程休眠等. 1 启动线程 在使用线程前,首先要创建一个 ...

  5. 力扣992.K个不同整数的子数组-C语言实现

    题目 原题链接 给定一个正整数数组 A,如果 A 的某个子数组中不同整数的个数恰好为 K,则称 A 的这个连续.不一定独立的子数组为好子数组. (例如,[1,2,3,1,2] 中有 3 个不同的整数: ...

  6. Kubernets二进制安装(18)之安装部署Heapster

    1.下载heapster镜像 在运维主机(mfyxw50.mfyxw.com)执行 [root@mfyxw50 ~]# docker pull quay.io/bitnami/heapster:1.5 ...

  7. docker 支持systemctl start|stop|status等操作

    用docker运行centos7容器时候,无法使用systemctl,官方解释是centos7的一个bug,可以有修复的办法: 在docker run的时候,加上--privileged 并且cmd使 ...

  8. 操作系统 part3

    1.操作系统四特性 并发:一个时间段,多个进程在宏观上同时运行 共享:系统中的资源可以被多个并发进程共同使用(互斥共享,同时共享) 虚拟:利用多道程序设计,利用时分复用(分时系统)和空分复用(虚拟内存 ...

  9. 让你像黑客一样写代码(not really)

    让你像黑客一样写代码(not really) http://poznan.tvp.pl 这是一个波兰的视频网站. poznan 波兹南(波兰城市 视屏链接 http://video.sina.com. ...

  10. js array & unshift === push head

    js array & unshift === push head const arr = [1, 2, 3]; console.log(arr.unshift(4, 5)); // 5 con ...