这篇博客主要讲述了JVM的地城运行过程,利用一个非常简单的例子,体验一下我们编写的class文件是如何通过JVM运行的,并且我们的数据、数据类型、对象、函数、变量等等元素在JVM中是如何存储已经变化的。
Jvm底层运行过程
利用一个简单的java程序来阐述Jvm底层运行过程:
-
编写了一个App.java文件,在其内部拥有main()方法和add()两个方法,我们通过build(ctrl+F9)可以在项目文件中生成一个橙色的target文件夹,在其内部中会自动利用javac把App.java文件转化为App.class文件,通过运行App.java文件中的main方法的实际效果也是一样的。
-
点击了run main()方法之后,系统就会将App.class文件用java直接运行到Java虚拟机也就是JVM中。
-
这里分析一下Jvm中的三大部分:
- 类加载子系统(classLoader)
- JVM运行时数据区(内存)
- 执行引擎(CPU)
在三个部分中,我们的App.class字节码文件会通过类加载子系统加载(classLoader从硬盘上加载)到JVM运行的数据区中,
运行区域如果按照内存就可以分为两块:
第一块是线程共享数据的部分:有堆和方法区。在高并发的情况下,就会出现线程安全的情况(输出的结果和我们预想的结果不一样)。
第二块就是线程私有数据的部分:有栈、本地方法栈(Native)和程序计数器组成。这个是私有的,所有不会出现线程安全问题,是独有的不存在高并发。
-
由于我们执行程序得到结果实际上是执行线程,因为线程是程序结果的最小单位。所以在这这个例子中运行main方法会生成main线程,其内部就是黄色的三大块内容会自动分配空间到main线程中。
-
然后我们的源码中主要有两个方法:add()和main()方法。这两个方法会按照栈的先进后出原则加载到我们的main线程中的栈中作为栈帧进行操作。
-
紧接着我们可以从图中看到一个栈帧中有着局部变量表、操作数栈、方法出口等等空间,这些东西实际上就是去运行我们的方法内部的实际代码块做的准备。利用javap App.class我们可以解析JVM内部的实际对App.class文件的执行过程。图中的最左边的一列顺序数字是我们的程序计数器。
-
我们主要用过add()方法中的代码可以看到JVM的内部运行机制是将1这个阿拉伯数字放入操作数栈中,然后再把a这个变量存储到局部变量表中并且将操作数栈的阿拉伯数字1赋给a,同样的b=2的操作也相同。然后通过
iload_1
和iload_2
将局部变量表中的a和b放到操作数栈中进行实际操作,而我们的bipush的实际意义则是8字节的最大整数——100放入操作数栈中。利用imul
直接运行出(a+b)*100=300的结果,然后将结果放回到局部变量表中赋给c,再用iload_3
放入到操作栈中方便最后一步的ireturn
返回给main栈帧。同样的在main栈帧中运行与上述add栈帧中相同类似的操作。
实际上是一个迭代运行的过程。
-
在主程序中有一行代码是
App app = new App();
这一行代码并不是基础类型的,在我们的线程栈中是找不到的,而是我们的第二种——引用类型。这种类型的是存储在我们线程共享数据中的堆中,我们的引用类型的数据实际上是依靠指针的形式来引用到JVM种的堆,从而实现实际功能的,如图所示。
本地方法栈: 因为一个方法是一个栈帧,但是有的方法并不是在我们本地上的,而是native属性的,是由C++或者C语言直接编译的过来的。我们没有加载过这种类似的栈帧,我们就会放到本地方法栈中,这就是本地方法栈的意义,而且特殊的,本地方法栈中的程序计数器都是0。
方法区(元空间) :这里面存储的是我们App.class文件中的一些类的属性:常量、变量和成员属性。
调优JVM内存以及垃圾回收——实际上是对JVM中线程共享数据的堆中操作
为什么要性能调优?
答:最基层的硬件和软件是配套使用的,是缺一不可的。我们的硬件中的物理内存假设只有8G,这个是无法改变的,是固定了的。我们的JVM分配内存也是在这8G的物理内存中运行的。
当用户越来越多的时候,我们的JVM内存就会变得越来越大,直到我们的8G内存不够利用了。这个时候就有了回收内存一个机制。我们假设回收内存的限定是6G,当JVM分配内存快达到6G的时候我们就会回收内存,当我们回收内存的时候也是需要产生内存的,所以才会导致我们手机或者电脑为什么会越用越卡的现象。
综上所述,Java调优的目的是为了在有限的空间内可以做无限的事情。
垃圾回收以及对象晋升的全过程
- 由于之前的例子中已经讲述到了堆的作用是引用类型的时候分配资源的,所以如果创建多个引用对象的话还是调用同一个App这个堆中的资源。
- 堆的构成:新生代(200M 1/3)和老年代(400M 2/3)
-
新生代又是由Eden:s0:s1=8:1:1的比例创建的,而我们的App这个资源则会根据他的内存大小分配到我们的新生代或者老年代中,但98%的比例会分配到新生代的Eden中,当Eden中的内存满了也就是代表着上面讲述到的分配的内存满了的意思。
这个时候就会触发一个新的机制:垃圾回收机制。
minor gc:minor gc会利用到gc root(判断App这个对象有没有索引或者被引用)来进行判断,如果判断你这个对象是没有链条或者链条之后的对象也是一个垃圾,则你这个对象就会被当做一个垃圾来处理。简单的来说,就是如果App这个对象没有被引用,则会被当做垃圾来删除;如果被引用了,那这个对象就会分配到幸存区,age+1。
由于服务器是24小时不间断的支撑的,不能无故重启或者关闭,所以会不断有App这种类似的资源被加载进来,类似于新的用户注册的道理。这时候又会重新触发垃圾回收机制,会将无用的垃圾清理掉,而之前的App的age继续+1,Eden的新App如果被幸存下来的话也是age+1,但是如果已经在s0或者s1中的age=1的App对象变成了垃圾,一样会被回收掉。简单的来说就是每一次触发了垃圾回收的时候,minor gc就会被调用,进行清理Eden、s0、s1中的所有垃圾。
-
由于我们s0和s1中的内存也是固定大小的,总有时候会被占满。这个时候如果假设age=15是最大的话,如果App的age达到了15还没有被处理掉的话,并且创建的对象大于所在内存区的50%(因为s0和s1之间是采用的一种复制的功能粘贴过去的,如果大于50%则就无法粘贴)的时候,就会发生对象晋升:age=15的App就会被放置到老年代的内存区域中,永远不会被删除掉。
-
总有时候,老年代里也会变得有很多的无法被删除无法被处理掉的对象。这个时候就会触发另外一个Full gc垃圾回收功能
Full gc: 所有的新生代和老年代都会被垃圾回收机制管理着。
如果这个时候有用户正在访问JVM也就是说访问网站的时候,正在垃圾回收,就会触发STW(stop the world)。所以现在很多大型公司都特别注意内存的存储,分配,优化,垃圾回收之类的相关问题。
-
通过一个死循环的小程序:
利用Java VisualVM工具就可以看到堆中实际的运行过程:
最后会抛出一个OOM的异常:
综上所述可以看到全过程如下:
总结: 我们对JVM的调优过程实际上就是减少Full gc的次数,让系统启动垃圾回收的次数减少,避免STW的发生。
如果你觉得这篇文章对您有帮助的话,麻烦帮我点个点个赞关注一下吧,创作不易,有你的支持才是我前进的动力~