jvm学习
JVM是Java Virtual Machine(Java虚拟机)的缩写,运行在操作系统之上。
JVM体系结构图:
![](https://img2018.cnblogs.com/i-beta/1496599/201911/1496599-20191124153339310-1447302578.png)
类装载器:
![](https://img2018.cnblogs.com/i-beta/1496599/201911/1496599-20191124153915231-1638162509.png)
类装载分为两类:虚拟机自带和用户自定义
虚拟机中自带了三种虚拟机:
- 启动类加载器(Bootstarp):加载lib/rt.jar下的文件
- 扩展类加载器(Extension):加载lib/ext/*.jar下的扩展文件
- 应用程序类加载器(AppClassLoader)也叫系统加载器:加载classPath下的所有类,如我们自定义的car类
demo:
class MyObject { } public class ClassLoaderDemo { public static void main(String[] args) { Object object = new Object(); //null Bootstrap 类加载器 System.out.println(object.getClass().getClassLoader()); /* //异常 上面已经没有了,Bootstarp是第一个类加载器 System.out.println(object.getClass().getClassLoader().getParent()); //异常 System.out.println(object.getClass().getClassLoader().getParent().getParent());*/ MyObject myObject = new MyObject(); //AppClassLoader 类加载器 System.out.println(myObject.getClass().getClassLoader()); // Extension 扩展类加载器 System.out.println(myObject.getClass().getClassLoader().getParent()); //Bootstarp 类加载器 null System.out.println(myObject.getClass().getClassLoader().getParent().getParent()); } }
myObject.getClass().getClassLoader():可以获取到加载器得名字
myObject.getClass().getClassLoader().getParent():可以得到当前加载器的上一个加载器,当已经获取到Bootstrap加载器,再调用getParent()会报空指针异常;
类加载器的顺序是从上往下加载
Bootstarp———》Extension————》AppClassLoader
双亲委派:
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,
每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈
自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是,比如加载位于rt,jar包中的类java.lang.Object, 不管是哪个加载器加载这个类,
最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
本地接口:
在我们看源码的时候,有时会看到native标记的方法,这就是本地方法,它是和底层硬件进行交互。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载native libraies。
pc寄存器:
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一
条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节这块内存区域很小,它是当前线程所执行的
字节码的行号指示器,字节如果执行的是一个Native方法,那这个计数器是空的。用以完成分支、循环、跳转、异常处理、
线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误。
简单理解就是告诉计算机程序运行到了哪一步。
Method Area方法区:
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool) 、
字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,
最典型的就是永久伏(PermGen space,1.7以前版本) 和元空间(Metaspace,1.8版本)
栈stack:
栈是一种数据结构,遵循先进后出/后进先出的原则,例如弹夹。
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,
每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体
JVM的实现有关,通常在256K~ 756K之 间,与等于1Mb左右。
每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈
demo:
class Test{ public void add(){ add(); } } public class StackDemo { public static void main(String[] args) { Test test = new Test(); test.add(); } }
那么栈里面的结构图是这样:
它会先执行add()方法,再去执行main()方法,但栈都是有内存空间的,当调用的方法多的话,内存空间就会溢出。
例如,上面的demo我们是递归调用,结果就栈内存溢出。发生Exception in thread "main" java.lang.StackOverflowError,这是一个error错误。
堆Heap:
堆结构:
新生代+老年代+永久代(1.7之前)
新生代+老年代+元空间(1.8)
元空间与永久代之间最大的区别在于:
永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。MinorGC的过程(复制->清空->互换):
1: eden、 SurvivorFrom复制到SurvivorTo, 年龄+1
首先,当Eden区满的时候会触发第一 次GC,把还活着的对象拷贝到SurvivorFrom区, 当Eden
区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回
收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋
值到老年代区),同时把这些对象的年龄+1
2: 清空eden、 SurvivorFrom
然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to
3: SurvivorTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一 次GC时的SurvivorFrom区。 部
分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold
决定,这个参数默认是15),最终如果还是存活,就存入到老年代
性能调优:
参数简介:
-Xms:设置初始分配大小,默认为物理内存的“1/6”
-Xmx:最大分配内存,默认为物理内存的“1/4”
-XX:+PrintGcDetails,输出详细的Gc处理日志
idea可以在项目Vm options设置这些参数:
demo:可以获取到虚拟机的一些信息。
//处理器核数 System.out.println(Runtime.getRuntime().availableProcessors()); //java虚拟机器使用的最大内存量 Long memroyMax = Runtime.getRuntime().maxMemory(); //java虚拟机的内存总量 Long memroyTotal = Runtime.getRuntime().totalMemory(); System.out.println("memroyMax"+memroyMax/(double)1024/1024+"M, \nmemroyTotal "+memroyTotal/(double)1024/1024+"M");
这样我们就可以写一个堆内存溢出(OOM)的demo
public class HeapDemo { public static void main(String[] args) { String str = "yang"; while (true){ str += str+ new Random().nextInt(888888888)+new Random().nextInt(9999999); } } }
打印信息[Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space这是一个Error错误。
Gc日志:
在上面demo中,如果我们设置了-XX:+PrintGCDetails参数还可以看到Gc日志的详细信息:
[GC (Allocation Failure) [PSYoungGen: 2040K->504K(2560K)] 2040K->743K(9728K), 0.0036043 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2331K->483K(2560K)] 2570K->1185K(9728K), 0.0038536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2391K->360K(2560K)] 4338K->2619K(9728K), 0.0142528 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 1645K->360K(2560K)] 7642K->6980K(9728K), 0.0059927 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 360K->0K(2560K)] [ParOldGen: 6619K->3805K(7168K)] 6980K->3805K(9728K), [Metaspace: 3468K->3468K(1056768K)], 0.0141733 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 1326K->32K(2560K)] 6378K->6329K(9728K), 0.0007138 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 6297K->3182K(7168K)] 6329K->3182K(9728K), [Metaspace: 3468K->3468K(1056768K)], 0.0152305 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 40K->0K(1536K)] 5714K->5674K(8704K), 0.0006500 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 5674K->4428K(7168K)] 5674K->4428K(8704K), [Metaspace: 3468K->3468K(1056768K)], 0.0050228 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4428K->4428K(9216K), 0.0005205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4428K->4408K(7168K)] 4428K->4408K(9216K), [Metaspace: 3468K->3468K(1056768K)], 0.0092480 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
YoungGc参数解析:
Full Gc:
可以使用java自带的jvisualvm进行监控,在应用位于bin/jvisualvm.exe
Minor GC和Full GC的区别:
JVM在进行GC时, 并非每次都对上面三个内存区域一 起回收的,大部分时候回收的都是指新生代。
因此GC按照回收的区域又分了两种类型,一种是普通GC (minor GC or young GC),一种是全局GC (major GC or Full GC)
普通GC (minor GC) :只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以
Minor GC非常频繁,- -般回收速度也比较快。
全局GC (major GC or Full GC) :指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少- -次的Minor GC (但.
并不是绝对的)。Major GC的速度- -般要 比Minor GC慢上10倍以上
GC收集算法:
- 复制算法
- 标记清除
- 标记压缩
复制算法:HotSpot JVM把年轻代分为S三部分: 1 个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,-般情况下,新创建的
对象都会被分配到Eden区(- -些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在
Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一-定程度时,就会被移动到年老代中。因为年轻代中的对象
基本都是朝生夕死的(90%以上:), 所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只
用其中一块,当这一块内存用完,就将还活着的对象复制到另外-块上面。
特点:复制算法不会产生内存碎片,但会占用空间。用于新生代
标记清除:算法分成标记和清除两个阶段,先标记出要回收的对象,然后统-回收这些对象。
特点:不会占用额外空间,但会两次扫描,耗时,容易产生碎片,用于老年代
示意图:
标记压缩:和标记清除一样,这里还多了一步压缩
特点:不会产生碎片,但是会耗时,标记以后还要整理存活对象的引用地址,用于老年代。
三者对比:
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不- -定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。
可以看出,效率上来说,复制算法是效率高德,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法
相对来少占用内存,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,标记压缩又比标记/清除多了一个整理内存的过程。
待更。。。