JVM学习
1. JVM的位置
2.JVM的体系结构
-
!堆和方法区是所有线程共有的,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。
3.类加载器
加载class文件~
-
引导类加载器(bootstrap class loader)
-
扩展类加载器(extensions class loader)
-
应用类加载器(application class loader)
-
自定义类加载器(custom class loader)
引导类>扩展类>系统
4.双亲委派机制
百度,搜索就会有很多通俗易懂的介绍,也可以看下面一张图片理解一下
根据上图中我们就可以容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
还是不能理解可看这篇文章https://blog.csdn.net/codeyanbao/article/details/82875064
这个机制的好处就是消除安全隐患,防止危险代码植入!
5.Native关键字
native是java中的一个关键字, java语言是运行在虚拟机上的,java又是不允许直接访问硬件的(也就是java安全性的体现),而java想要做一些操作硬件的事情的话,必然要用到底层一些的调用。这就引出了native的关键字。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。
**!补充:**使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。
native关键字的实现原理
- native关键字作用在方法上,并且不提供实现体,它会进入本地方法栈,通过调用JNI(Java Native Interface)Java本地接口实现对其他语言代码和代码库的使用。
- 内存中有一块专门开辟的区域:Native Method Stack(本地方法栈),登记Native方法。
- JNI调用C流程图
- 可以将native方法比作Java程序同C程序的接口,其实现步骤:
- 在Java中声明native()方法,然后编译;
- 用javah产生一个.h文件;
- 写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);
- 将第三步的.cpp文件编译成动态链接库文件;
- 在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。
6.栈、堆和方法区
-
栈
-
虚拟机栈(JVM Stack)。虚拟机栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、
方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
先进后出,正在运行的方法永远都在栈顶!
-
本地方法栈(Native Stack)。本地方法栈与Java栈的作用和原理非常相似,区别是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
-
-
堆
Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。
所有通过new创建的对象的内存都在堆中分配,其大小可以通过**-Xmx和-Xms**来控制。
堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(s0)和ToSpace(s1)组成。结构图如下所示:
下图中的Perm代表的是永久代,但是注意永久代并不属于堆内存中的一部分,同时jdk1.8之后永久代也将被移除,改名为元空间。
-
方法区
方法区(Method Area)也称”永久代“,与java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译后的代码缓存等数据。方法区在JVM启动时候就被创建,并且它的实际物理内存空间和java堆区一样都是可以不连续的。
Java虚拟机规范中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾回收或进行压缩”,但是对于HotSpot JVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆区分开来。所以,方法区看作是一块独立于堆的内存空间。
方法区,元空间,永久代三者关系是什么呢?
方法区是java虚拟机规范的一部分,而元空间和永久代是一个具体的实现,元空间的本质和永久代类似,都是对JVM规范方法区的实现,不过两者最大的区别就是:元空间不在虚拟机中设置内存,而是直接使用本地内存.
在JDK8之前,方法区又称为"永久代",而JDK8以及8之后改为"元空间"
扩展:PC寄存器----线程私有
PC寄存器也叫程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。
每一条JVM线程都有自己的PC寄存器。在任意时刻,一条JVM线程只会执行一个方法的代码,该方法称为该线程的当前方法(Current Method)。如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址;如果该方法是native,那PC寄存器的值是undefined。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
7.JVM调优
-
常见的JVM实现
- Hotspot
- oracle官方,我们做实验用的JVM
- java -version
- Jrockit
- BEA,曾经号称全世界最快的JVM
- 被oracle 收购,合并于Hotspot
- J9-IBM
- 市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM
- 广泛用于IBM的各种Java产品,也称为Eclipse OpenJ9
- Microsoft VM
- TaoBaoVM
- hotspot深度定制版
- LiquidVM
- 直接对硬件
- Hotspot
-
GC垃圾回收
前面的JVM体系结构讲解到,jvm调优99%都是在堆中(包括 ‘ 元空间 ’),因为在运行时数据区中,只有堆中存在着垃圾回收。所以学习调优就嘚深入学习堆的内部结构和知识,掌握常有调优参数和垃圾回收算法。
-
堆的内部结构
调优参数
-Xms:初始分配大小,默认为物理内存的1/64
-Xmx:最大分配内存,默认为物理内存的1/4
-XX:+PrintGCDetails:输出详细的GC处理日志
以IDEA为例,在需要运行的类的configuration里面的 VM options 里面。如下图:
- 常用参数配置
- 收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器 - 垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename - 并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) - 并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
- 收集器设置
- 常用参数配置
-
GC算法
GC是分类收集算法,JVM在进行GC的时候并不是每次对三个区域一起回收,大部分时候是回收新生代。频繁收集Young区,较少收集Old区,基本不动元空间。GC按照回收的区域分成了:普通GC minor GC和全局GC Full GC。
- minor GC
复制 ☞ 清空 ☞ 互换
- eden、survivor from 复制到 survivor to,对象年龄+1。
当eden区满,触发第一次GC,存活对象拷贝到survivor from区。当eden区再次触发GC,会扫描eden和from,对这两个区进行垃圾回收,将存活的对象,复制到to区,对象年龄+1。(如果有对象年龄达到了老年的标准,拷贝到老年代,对象年龄+1)
- 清空eden、survivor from
清空eden和survivor from中对象,此时from为。
- survivor from 和 survivor to 互换
to区存在对象,变成下一次GC的from区,from区成为下一次GC的to区,部分对象会在form和to区域复制往来15次(JVM的MaxTenuringshold参数默认是15),如果最终还是存活,就存入老年代。
- Full GC
Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。触发Full GC的原因有很多:
- 当年轻代晋升到⽼年代的对象⼤⼩,并⽐⽬前⽼年代剩余的空间⼤⼩还要⼤时,会触发Full GC;
- 当⽼年代的空间使⽤率超过某阈值时,会触发Full GC;
- 当元空间不⾜时(JDK1.7永久代不足),也会触发Full GC;
- 当调⽤**System.gc()**也会安排⼀次Full GC。
GC有四大算法
-
引用计数法
引用计数法每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题,还有一个问题是如何解决精准计数。这种方法现在已经不用了 。
-
复制算法
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。此算法用于新生代内存回收,从E区回收到S0或者S1
-
标记清除
标记-清除算法采用从根集合进行扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!适合老生代去回收。
-
标记压缩。
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
-
JVM调优工具
- Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
- JProfiler:商业软件,需要付费。功能强大。
- VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。
这些只是我个人参考多篇文章及视频的理解,有个别地方写得不好的理解一下,关于JVM调优的知识还有很多且很深,感兴趣的同学可以搜集一下JVM调优更深入的学习!