JVM,全程Java Virtual Machine,是java虚拟机的意思!
是一个类似计算机的存在,是在计算机上模拟真实计算机运行的平台,它代替Java代码和各种计算机设备之间的交互!
Java程序把代码翻译成字节码交给虚拟机,虚拟机会将其解释成设备能识别的机器指令交给设备,
是一个中间层的概念!
相关概念
JRE/JDK/JVM 三者之间的关系
JDK:是Java开发工具包,开发者用起进行编译、调试,他本身也是一个Java程序
JRE:是Java运行环境,所有Java代码必须在jre环境内执行
JVM:是JRE的一部分,可跨平台
JVM内存结构
上面只是JVM的一种规范模型,具体每个模块都是啥意思,JDK1.7,1.8又是怎么实现的,下面会细说
内存概念模型
堆:存储各实例对象,属于线程共享
方法区:存储已被虚拟机加载的类信息、常量(静态常量池、运行时常量池)、即时编译器编译后的代码等数据,属于线程共享。
类信息:类的版本、字段、方法、接口和父类等信息
运行时常量池:存储的是类加载时生成的直接引用等信息。
静态常量池:主要存储的是字面量,以及符号引用等信息,也包括了我们说的字符串常量池
虚拟机栈:由一个一个的栈桢组成,每个Java方法从运行到结束都意味着一个栈桢从入栈到出栈的过程(这对方法递归的理解有所帮助),栈桢存储的是:局部变量表、操作数栈、动态链接、方法出口等信息 ,属于线程私有。
本地方法栈:和虚拟机栈功能类似,只不过他适用于Java调用一些native方法,属于线程私有。
比如 System.currentTimeMillis();
native 方法是 Java 中声明,由操作系统中具体方法实现
程序计数器:每个形成都有一个自己的程序计数器,记录的是下一条需要执行的字节码指令,也就是说记录了程序该走哪一步,线程停止程序停在了哪一步,下一次唤醒应该从哪一步走起;分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
属于线程私有。
我们知道Java并不能直接执行,他需要被编译成JVM指令,然后在虚拟机中被调用给到机器执行,但其实这些指令也不能直接交给CPU去执行,他们需要被解释器解释成一个一个的机器码然后交给CPU去执行,所以计数器记录的也就是一条条指令的地址!
比如下面例子中的左侧是JVM指令,右侧是相应的源代码。
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
执行流程加上程序计数器后的样子
比如拿到了第一条getstatic指令,交给了解释器,解释器把他变成机器码,
然后再交给CPU运行,但是在与此同时,他就会把下一条指令即下面的astore_1
指令的地址即把3放入程序计数器。
所以等第一条指令执行完了以后,解释器就会到程序计数器里去取下一条指令,
根据地址3再找到下调指令astore_1,然后再重复刚才的过程。
当然,在执行3这条指令的时候,再把3的下一条即例子中的4存入程序计数器。
总之,他记录了下一个JVM的指令地址。
所以,如果没有程序计数器,他就不知道接下来该执行哪条命令了,这是程序计数器的基本作用。
现在来说下不同产品对JVM内存模型的不同实现
这里主要区别我觉得是方法区的实现
在HotSpot虚拟机,就会常常提到 永久代,这个词。HotSpot虚拟机在 JDK8前用永久代实现了方法区,而很多其他厂商的虚拟机其实是没有永久代的概念的。在JDK8中,已经用元空间来替代了永久代作为方法区的实现了,然后在JDK1.7已经对元空间做了变动,已经把 常量池 迁移到了堆空间进行存储!1.8在这一基础上彻底废弃了永久代,将这一概念变成了元空间,并且直接放到了内存进行存储!
垃圾回收:
什么是垃圾
没有用的对象就是垃圾,那问题就是如何判断没有被引用呢?
方法一:引用计数法,一个对象被引用的时候引用次数就+1,引用次数为0的认为就是垃圾对象,但这样会产生互相引用,会导致这俩永远都不会被认为是垃圾(非法抱团)
方法二:可达性分析法,简单说就是和GC Roots对象没有关系的就都是垃圾,那GC Roots是啥呢?比如上面提到的栈桢,那里面就有引用,栈顶的栈桢一定是当前执行的程序,那里面的引用肯定有用,这就可以是GC Roots,也就是说和他扯上关系的一个不能动!
GC Roots还有:本地方法栈中的引用、方法区中静态属性引用的对象、方法区中静态常量池中引用的对象。
有哪些垃圾清除方式
标记清除:识别到是垃圾标记下,然后直接清除即可,这种办法简单粗暴,但是会造成碎片问题,假如有20MB,我已经清除了10MB,但可能由于空间很分散,一块一块的,就可能会导致我一个需要连续1MB存储空间的对象都放不下,这就是碎片问题!
这个能避免吗?
复制算法:我把空间分两个区域,每次只用其中一个区域,需要清理的时候把活对象复制到另一个区域,再清空垃圾,这不就没有碎片了
吗?这样虽然没有碎片问题,但其实内存利用率也不高,归根揭底和标记清除都有着内存利用的短板
标记-整理:我不需要划分空间,我给碎片移动到一起不就行了,每次我标记好,完事把活着的移动到一端,再进行清理就OK了,不过这显然有些慢的
没有完美的方法?是的,好像是没有,毕竟这个空间换时间,时间换空间 ,这词语不是平白捏造的.....,但是按特性分配劳动力往往也能解决一些事情
下面来聊聊我自己的看法
先聊下Java对象,正常情况下我们所创建的很多对象,也就只用那一会的,所以说很多对象都是朝生夕死的,这些实例可以说是产的多死的快,所以这就意味这需要频繁的进行垃圾清除,但是也一定是有一些相对长期存在的对象。
那综合上述实例特点和三种算法比较:
1、对于朝生夕死的对象,使用第三种应该是不合适的,因为它需要频繁移动,效率低。那再看第一种,由于多,那好像也很容易产生碎片,折中好像只能取第二个!第二种用率低,但是我们可以只要一小部分内存,大不了我们先把大对象单独拿出来,很大的对象应该也很少吧。另外我们再想想,大部分(98%)朝生夕死,小部门一次清理中能留下来,总感觉可以用很小的空间就能给他存下来,那我们就划分一小部分,那就假设出来一小部部分。
现在我们可以想象有两块 大A,小b:
一开始把对象都存到大A,等到清理的时候,把活的移动到小b,然后清空大A内存,之后再把对象都往大A里放,之后再清除,等等,此时好想需要清除两部分,而且没有第三个空间可以用了,那就再造个第三空间吧,想一想,这个第三空间应该多大?显然只需要很小,因为我存储的也只是存活的!
现在我们可以想象有三块了, 大A,小b,小c
接着上面等等来,两块全部标记之后,把活的全移动到小c内,再把大A和小b 清空,这样下次清理又有一个空白内能用,好像挺不错的哈
2、那再看长期存在的,没啥说的,就用第三种了,虽然慢但是我不频繁!那上面说的大对象咋办呢?大对象,那碎片问题应该可以忽略不计的,因为比你小的对象一定是占大多数的!你被清理了腾出来的空间一定是很大的,我再时不时用第三种整理下,这样好像真的很不错!
这样综合来看,我们好像可以把内存分为四个区域(大A 小b 小c 存长期和大对象的)
然后再看下JVM的堆内存模型,,,哎,居然和他一样啊,没错就是一样,因为我就是根据那个自己假设的,,,,
清除过程:第一次清除之前,from和to 都是空的,老年代不一定是空的,因为就像上面说的,如果一个对象足够大会直接进入老年的,i第一次清除,会整理Eden区,把存活对象放到from
第二次清除,会整理Eden区和from区,然后把存活的放入to区,清空from,然后from和to再互换身份,也就是说第一次之后,from总是存放对象的,to总是空的!
之后的每次清楚就会循环第次清楚的步骤!
进入老年代的情况:
1、Eden园区存不下,进行清理,清理之后from区也放不下,那此时就会启动担保机制,使一些对象进入老年代
2、对象够老,也就是上面自已YY的长期对象,怎么判断够老?每次进行from-to转移时,把存活的都给他+1,默认活过15次清除进入老年代
两个GC概念
Minor GC:年轻代垃圾清除
Full GC:老年代垃圾清除(可手动:System.gc)
具体有哪些清除工具
个人认为理解几个概念就行
有针对上述各个算法实现的,有在清楚时会让业务线程全部暂停的,有和业务线程并发执行的
串行垃圾收集器
Serial和Serial Old,一般两者搭配使用。
新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。
-XX:+UseSerialGC开启串行垃圾回收器。
并行垃圾收集器
ParNew:Serial收集器的多线程版本,默认开启的收集线程数和CPU数量一样,运行数量可以通过修改ParallelGCThreads设定。用于新生代手机,复制算法。
用-XX:+UseParNewGC,和Serial Old收集器组合进行内存回收。
G1从整体看还是基于标记-清除算法的,但是局部上是基于复制算法的。
4、垃圾清除落地实现