摘要:执行引擎负责解释指令,提交给操作系统执行。如图栈帧是最先被调用的方法,先入栈,然后方法又调用了方法,栈帧处于栈顶的位置,栈帧处于栈底,执行完毕后,依次弹出栈帧和栈帧,线程结束,栈释放。了解性参数永久代初始值永久代最大值新生代大小
JVM即Java Virtual Machine(Java虚拟机)的缩写,身为一名java开发者,适当了解JVM,拓展一下知识面并没有坏处,本人结合最近的学习对JVM做了简单总结,现给大家分享。
1 JVM结构 1.1 Class Loaderclass 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
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等等。
它的具体做法是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):包括类文件、方法等等。
栈中的数据都是以栈帧(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区被占满。
注:3 堆参数调优入门
Jdk1.6及之前:有永久代, 常量池1.6在方法区
Jdk1.7:有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后:无永久代,常量池1.8在元空间
常用参数:
-Xms 设置初始分配大小,默认为物理内存的1/64
-Xmx 最大分配内存,默认为物理内存的1/4
-XX:PrintGCDetails 输出详细GC日志
Demo01public 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这个长参数是比较特别的,所以这里多带带提一下,它的作用是当JVM产生OOM异常时,生成一个dump文件到你的工程目录下,可以配合eclipse的MAT(Eclipse Memory Analyzer)插件分析内存泄漏。
了解性参数-XX:PermSize 永久代初始值
-XX:MaxPermSize 永久代最大值
-Xmn 新生代大小
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/68365.html
摘要:直接对栈的操作只有两个,就是对栈帧的压栈和出栈。中将永久代移除,同时增加元数据区。在中,本地方法栈和虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。 原文:https://github.com/linsheng97... 描述一下 JVM 的内存区域 程序计数器(PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的...
摘要:内存分配解析四方法执行完毕,立即释放局部变量所占用的栈空间。内存分配解析五调用对象的方法,以实例为参数。堆和栈的小结以上就是程序运行时内存分配的大致情况。 前言 java中有很多类型的变量、静态变量、全局变量及对象等,这些变量在java运行的时候到底是如何分配内存的呢?接下来有必要对此进行一些探究。 基本知识概念: (1)寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序...
摘要:复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。针对老年代老年代的特点是区域较大,对像存活率高。这种情况,存在大量存活率高的对像,复制算法明显变得不合适。 GC(Garbage Collection)即Java垃圾回收机制,是Java与C++的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C++程序...
摘要:堆区堆是虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块内存区域,该区域在虚拟机启动的时候创建。 运行时数据区域 想要了解jvm,那对其内存分配管理的学习是必不可少的;java虚拟机在执行java程序的时候会把它所管理的内存划分成若干数据区域。这些区域有着不同的功能、用途、创建/销毁时间。java虚拟机所分配管理的内存区域如图1所示 程序计数器 程序计数器是一块比较...
阅读 2634·2023-04-26 02:44
阅读 6839·2021-11-22 14:44
阅读 2096·2021-09-27 13:36
阅读 2420·2021-09-08 10:43
阅读 630·2019-08-30 15:56
阅读 1373·2019-08-30 15:55
阅读 2867·2019-08-28 18:12
阅读 2816·2019-08-26 13:50