JVM简单入门

初识JVM

JVM的位置:jre中包含jvm。

双亲委派机制

双亲委派机制:是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。

在IDE中编写的Java源代码会被编译器编译成.class文件,然后再由ClassLoader(类加载器)将这些class文件加载到JVM中执行。

JVM中提供了三层的ClassLoader:

  • Bootstrap classLoader: 主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
  • AppClassLoader:主要负责加载应用程序的主函数类。

沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏,沙箱主要限制系统资源访问,那系统资源包括什么?CPU,内存,文件系统,网络。不同级别的沙箱对这些资源的访问权限也可以不一样。

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

在Java中将执行程序分为本地代码和远程代码两种,本地代码默认视为可信任的,可以访问一切本地资源;而远程代码则被视为不受信任的,对于未授信的远程代码在早期的Java实现中,安全依赖于沙箱机制。

如此严格的安全机制给程序的拓展带来了障碍,当远程代码需要访问本地资源的时候就无法实现。因此在Java1.1中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问。在Java1.2版本中,再次改进了安全机制,不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。

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

沙箱的基本组件

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

  • 类装载器(ClassLoader):类装载器在三个方面对Java沙箱起作用。

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

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

    类装载器采用的机制是双亲委派机制。

    • 由最内层JVM自带的类加载器开始加载,外层恶意同名类得不到加载从而无法使用。
    • 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问的内层类,破坏代码就自然无法实现。
  • 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制策略的设定,也可以由用户指定。

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

  • 安全软件包(security package):java.cecurity下的类和拓展包下的类,允许用户为自己的类增加新的安全特性,包括:

    • 安全提供者
    • 消息摘要
    • 数字签名
    • 加密
    • 鉴别

Native

native:凡是带native关键字的,说明Java的作用范围达不到了。会去调用底层c语言的库,进入本地方法栈,调用本地方法本地接口(JNI)。

JNI作用:拓展Java的使用,融合不同的编程语言为Java所用。

native method stack作用:登记native方法,在(Execution Engine)执行引擎执行的时候加载native libraies(本地库)。

PC计数器

程序计数器:program counter register。

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

方法区

img

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

静态变量,常量,类信息(构造方法,接口),运行时的常量池存在方法区中,但是实例变量存在堆内存中,与方法区无关。

(jdk1.8已经将方法区去掉了,将方法区移动到直接内存)

JDK1.8为什么要移除方法区

1)永久代来存储类信息、常量、静态变量等数据不是个好主意, 很容易遇到内存溢出的问题.JDK8的实现中将类的元数据放入 native memory, 将字符串池和类的静态变量放入java堆中. 可以使用MaxMetaspaceSize对元数据区大小进行调整;

2)对永久代进行调优是很困难的,同时将元空间与堆的垃圾回收进行了隔离,避免永久代引发的Full GC和OOM等问题;

栈:数据结构(先进后出),栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存释放。

对于栈来说,不存在垃圾回收问题。

栈:主要存储八大基本数据类型和对象引用。

栈运行原理:栈帧用于存储局部变量表,动态链接,方法出口等信息,方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。

堆(Heap):一个JVM只有一个堆内存,堆内存的大小是可以调节的。

堆:此内存区域唯一的目的就是存放对象实例。所有的对象实例都在这里分配内存

堆内存中分为三个区域

  • 新生区:
    • 伊甸园区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代),当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
    • 幸存区0区:保留了一次MinorGC过程中的幸存者。
    • 幸存区1区:上一次GC的幸存者,作为这一次GC的被扫描者。
  • 老年区:存放稳定的对象(年龄到达设定的值 ,一般为15)。
  • 永久区:常驻内存的,存放Java运行时的一些环境或类信息(这个区不存在垃圾回收,关闭JVM就释放这个区的内存)。
    • Java17之前:永久代,常量池在方法区;
    • jdk1.7:去永久代,常量池在堆中;
    • jdk1.8:无永久代,常量池在元空间(元空间逻辑上存在,物理上不存在);
package com.jvm;

public class Test02 {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();//字节
        //jvm的初始化总内存
        long total = Runtime.getRuntime().totalMemory();
        System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"M");
        System.out.println("total="+total+"字节\t"+(total/(double)1024/1024+"M"));
    }
}

工具分析OOM

在一个项目中,出现了OOM,使用的内存分析工具(MAT,Jprofile)。

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

package com.jvm;

import java.util.ArrayList;
//dump文件
public class Test03 {
    byte[] array = new byte[1*1024*1024];

    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        int count = 0;
        try{
            while (true){
                list.add(new Test03());
                count+=1;
            }
        }catch (Exception e){
            System.out.println("count:"+count);
            e.printStackTrace();
        }

    }
}

GC算法

GC回收大部分都是在新生区。

什么时候触发GC

​ (1)程序调用System.gc时可以触发

​ (2)系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)

GC又分为 minor GC 和 Full GC (也称为 Major GC )

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

a.调用System.gc时,系统建议执行Full GC,但是不必然执行

b.老年代空间不足

c.方法区空间不足

d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存

e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

GC常用的算法:引用计数法,标记清除算法,标记压缩算法,复制算法分代收集算法

目前主流的JVM(HotSpot)采用的是分代收集算法。

引用计数法:当一个对象的引用为0时会清除该对象(使用较少)。

复制算法

复制算法:该算法将新生区内存平均分成两部分,每次只使用其中的一部分,当这部分内存快满的时候,将其中的幸存者复制到另一个内存上,将之前的内存清空。

  • 优点:不存在内存碎片,只需移动栈顶指针,按顺序分配内存即可。
  • 缺点:每次只使用一半的内存。
  • 每交换一次年龄加1,到达默认年龄15后,幸存者进入老年区(-XX: -XX:MaxTenuringThreshold=30设置进入老年代的年龄)。

标记清除算法

标记清除法:用在老年代中,为对象存储一个标记位,标记存活的对象,对死亡的对象执行清除操作。

  • 优点:不需要额外的空间,不需要移动对象。
  • 缺点:两次扫描效率比较低(全栈遍历),没有移动对象会产生内存碎片。

标记压缩算法

标记压缩算法是标记清除算法的一个改进版,再次扫描,将存活的对象向一端移动,

  • 优点:没有内存碎片。
  • 缺点:存活的对象较多时,会进行多次的移动操作,效率低。

分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

GC算法总结

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

内存整齐度:复制算法 = 标记压缩算法 > 标记清楚算法(内存碎片)

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

所以JVM采用分代收集算法!!

JMM

Java memory model:Java内存模型。

Tags: