资讯专栏INFORMATION COLUMN

JVM小结

Lin_R / 2001人阅读

摘要:执行引擎负责解释指令,提交给操作系统执行。如图栈帧是最先被调用的方法,先入栈,然后方法又调用了方法,栈帧处于栈顶的位置,栈帧处于栈底,执行完毕后,依次弹出栈帧和栈帧,线程结束,栈释放。了解性参数永久代初始值永久代最大值新生代大小

JVM即Java Virtual Machine(Java虚拟机)的缩写,身为一名java开发者,适当了解JVM,拓展一下知识面并没有坏处,本人结合最近的学习对JVM做了简单总结,现给大家分享。

1 JVM结构

1.1 Class Loader

class loader顾名思义是类加载器,我们的类文件(.class)是保存在硬盘上的,如果想要被jvm执行,需要有一个中间层把它加载到jvm中,这个工作就是由class loader做的,它通过IO流的形式把.class文件载入到虚拟机,类加载器分四种:

①启动类加载器(Bootstrap)

这部分是由c/c++编写的,属于最底层的类加载器。他会加载$JAVA_HOME/jre/lib/rt.jar中的所有类,这个jar包中有我们常用的最基本的类,比如java.lang.Object、java.lang.String等,这也就解释了为什么我们在使用这些类时不需要导包的原因,启动类加载器已经事先加载到jvm中了。

②扩展类加载器(Extension)

使用java编写,它会加载$JAVA_HOME/jre/lib/ext/*.jar

③应用程序类加载器(AppClassLoader)

也叫系统类加载器,使用java编写,加载当前应用的$CLASSPATH中的所有类。

④用户自定义加载器

Java.lang.ClassLoader的子类,用户可以定制类的加载方式。(一般用不到)

双亲委派机制和沙箱机制

提到类加载器,就不得不提这两个机制,所谓双亲委派是指:当应用类加载器接收到一个加载类的请求时,不会马上进行加载,而是委托给它的父类加载器——扩展类加载器去加载,而扩展类加载器又委托给启动类加载器,如果启动类加载器在它的范围内没有找到该类,则会抛一个ClassNotFoundException异常,这时它的子类加载器才会逐级向下去尝试加载,直到找个这个类。那么这有什么意义呢?设想,假如你建了一个java.lang的包,又在该包下建了一个String类,如果没有这个双亲委派机制,那么你自己写的String类是不是就把jre标准的String给覆盖了?java为了保护自身标准的类不会被覆盖,于是就采用了双亲委派把这些类隔离开来,也就是所谓的“沙箱机制”。

获取类加载器

可以通过java.lang.Class中的getClassLoader方法来获取当前类加载器。

public class JVMTest01 {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(obj.getClass().getClassLoader());
        JVMTest01 test = new JVMTest01();
        System.out.println(test.getClass().getClassLoader());
        System.out.println(test.getClass().getClassLoader().getParent());
        System.out.println(test.getClass().getClassLoader().getParent().getParent());
    }
}

输出结果:

null
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null

我们来分析一下这个结果,第二行和第三行的输出应该容易理解,JVMTest01是一个用户自定义的类,是由应用类加载器加载的,而它的父类加载器是扩展类加载器。但奇怪的是第一行和第四行的结果,为什么是null?我们知道Object类是由启动类加载器加载的,应用类加载器的父类的父类加载器也是启动类加载器,那为什么获取不到呢?因为启动类加载器是jvm最底层的直接跟操作系统打交道的接口,是由c++编写的,已经很底层了,单靠java已经获取不到了,所以是null。

1.2 Execution Engine

执行引擎负责解释指令,提交给操作系统执行。

1.3 Native Interface

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生之初正是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等。

1.4 Native Method Stack

它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

1.5 PC寄存器

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

1.6 Method Area

静态变量+常量+类信息+运行时常量池存在方法区中,该区被所有线程共享。

注:实例变量存在堆内存中,和方法区无关
1.7 Stack 1.7.1 栈是什么

栈主管Java程序运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致,是线程私有的。

1.7.2 栈中存放什么

栈帧中主要保存3类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
栈操作(Operand Stack):记录出栈、入栈的操作。
栈帧数据(Frame Data):包括类文件、方法等等。

1.7.3 栈运行原理

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”/“后进先出”原则。
如图:

栈帧 2是最先被调用的方法,先入栈,然后方法 2 又调用了方法1,栈帧 1处于栈顶的位置,栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1和栈帧 2,线程结束,栈释放。
设想:如果方法中不断调用方法,栈帧一帧一帧的往上堆叠,终于超过了栈空间的上限,于是就报了java.lang.StackOverflowError。这就是无限递归调用:

    public void test() {
        test();
    }

调用这个方法就会产生这个结果:

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


图中表示的关系是这样的:在栈中,保存了局部变量(基本类型+引用类型),而引用类型指向了堆内存中的一块对象实例,而这个实例是依据什么为蓝图创建的呢?就是存在于方法区中的类信息,它记录了该类的“DNA”,基于该类的所有实例都以此为模版进行创建。

注:本地方法存在于本地方法栈中,和普通Java方法不在同一个栈
2 堆体系结构概述

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,堆内存分为三部分:

Young Generation Space 新生区 Young/New

Tenure generation space 养老区 Old/Tenure

Permanent Space 永久区 Perm

注:JDK1.8开始,永久区替换为了元空间

新生区又分为:

伊甸区(Eden Space)

幸存0区(Survivor 0 Space)

幸存1区(Survivor 1 Space)

图例:

所有的对象都是在伊甸区被new出来的,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常java.lang.OutOfMemoryError

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。

注:
Jdk1.6及之前:有永久代, 常量池1.6在方法区
Jdk1.7:有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后:无永久代,常量池1.8在元空间
3 堆参数调优入门

常用参数:

-Xms 设置初始分配大小,默认为物理内存的1/64

-Xmx 最大分配内存,默认为物理内存的1/4

-XX:PrintGCDetails 输出详细GC日志

Demo01
    public static void main(String[] args) {
        long maxMemory = Runtime.getRuntime().maxMemory();    //返回 Java 虚拟机试图使用的最大内存量
        long totalMemory = Runtime.getRuntime().totalMemory();    //返回 Java 虚拟机中的内存总量
        
        System.out.println("MAX_MEMORY = " + maxMemory + "Byte " + (maxMemory / (double)1024 / 1024) + "MB");
        System.out.println("TOTAL_MEMORY = " + totalMemory + "Byte " + (totalMemory / (double)1024 / 1024) + "MB");
    }

在eclipse中配置jvm参数:

输出结果:

由图,我们利用-Xms和-Xmx参数将初始内存和最大内存都设置为1024MB(实际结果981.5MB属于误差)

注:永久代/元空间 只是JVM逻辑上有这么一块区域,但实际物理内存中并不存在,如何证明呢?如图:新生代+养老代 的内存总和已经等于TOTAL_MEMORY,说明实际内存中只有新生区和养老区,永久代/元空间只是逻辑上存在。
Demo02
    public static void main(String[] args) {
        String str = "hello world!";
        while (true) {
            str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999);
        }

    }

参数配置:

-Xms8m -Xmx8m -XX:+PrintGCDetails

运行结果:

分析:我们故意把堆内存调小至8M,然后再不断地在堆中生成String对象,直到产生OOM异常,从输出日志中可以看到,在抛出异常前JVM不断进行GC,直到最后一次Full GC之后,堆内存依旧没有足够的空间new出新的对象,于是就抛出了OOM异常。一般OOM异常都是在Full GC之后产生的。

-XX:+HeapDumpOnOutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError这个长参数是比较特别的,所以这里多带带提一下,它的作用是当JVM产生OOM异常时,生成一个dump文件到你的工程目录下,可以配合eclipse的MAT(Eclipse Memory Analyzer)插件分析内存泄漏。

了解性参数

-XX:PermSize 永久代初始值

-XX:MaxPermSize 永久代最大值

-Xmn 新生代大小

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68365.html

相关文章

  • 金三银四面试季节之Java 核心面试技术点 - JVM 小结

    摘要:直接对栈的操作只有两个,就是对栈帧的压栈和出栈。中将永久代移除,同时增加元数据区。在中,本地方法栈和虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。 原文:https://github.com/linsheng97... 描述一下 JVM 的内存区域 程序计数器(PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的...

    XGBCCC 评论0 收藏0
  • java学习(四) —— 内存分配浅析

    摘要:内存分配解析四方法执行完毕,立即释放局部变量所占用的栈空间。内存分配解析五调用对象的方法,以实例为参数。堆和栈的小结以上就是程序运行时内存分配的大致情况。 前言 java中有很多类型的变量、静态变量、全局变量及对象等,这些变量在java运行的时候到底是如何分配内存的呢?接下来有必要对此进行一些探究。 基本知识概念: (1)寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序...

    henry14 评论0 收藏0
  • 聊聊GC

    摘要:复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。针对老年代老年代的特点是区域较大,对像存活率高。这种情况,存在大量存活率高的对像,复制算法明显变得不合适。 GC(Garbage Collection)即Java垃圾回收机制,是Java与C++的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C++程序...

    developerworks 评论0 收藏0
  • jvm基础篇一之内存区域

    摘要:堆区堆是虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块内存区域,该区域在虚拟机启动的时候创建。 运行时数据区域    想要了解jvm,那对其内存分配管理的学习是必不可少的;java虚拟机在执行java程序的时候会把它所管理的内存划分成若干数据区域。这些区域有着不同的功能、用途、创建/销毁时间。java虚拟机所分配管理的内存区域如图1所示 程序计数器    程序计数器是一块比较...

    Zachary 评论0 收藏0

发表评论

0条评论

Lin_R

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<