JVM(一)——架构基础
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
JVM
java虚拟机
java gc 主要回收的是 方法区 和 堆中的内容以下架构图是重点
方法区和堆是线程共享java栈、本机方法栈、程序计数器是线程私有。
运行时数据区可以用Runtime.getRuntime()获取
字节码执行引擎修改程序计数器执行方法区。
jvm自带插件
反汇编先进到target,class文件存放区
javap -c Math.class > Math.txt把字节码文件反汇编存入txt也可以idea设置外部插件
javap -v -p Math.class直接打印汇编指令。
jvisualvm可打开java VM。识别本机所有java进程并查看这些进程的运行信息。可再安装Visual GC插件实现查看GC情况。
类加载器ClassLoader
多线程等待cpu调用。
负责加载class文件class文件在文件开头有特定的文件标示(class文件内容前是cafe babe)将class文件字节码内容加载到内存中并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载至于它是否可以运行则由Execution Engine决定Execution Engine执行引擎负责解释命令提交操作系统执行。
Class是模板car1-3是实例
虚拟机自带加载器
- 启动类加载器Bootstrap) C++语言开发 加载JAVAHOME/jre/lib/rt.jar原始java类
- 扩展类加载器Extension) Java 加载JAVAHOME/jre/lib/ext/*.jar后期java类javax等
- 应用程序类加载器AppClassLoader也叫系统类加载器加载当前应用的classpath的所有类加载用户写的类
用户自定义加载器
Java.lang.ClassLoader的子类用户可以定制类的加载方式如下图虚线以下
该类是抽象类可继承实现。
代码验证
public class Hello {
public static void main(String[] args) {
//系统自带系统自带无法显示自己的加载器
Object object = new Object();
System.out.println(object.getClass().getClassLoader());
//自定义
Hello hello = new Hello();
System.out.println(hello.getClass().getClassLoader());
System.out.println(hello.getClass().getClassLoader().getParent());
System.out.println(hello.getClass().getClassLoader().getParent().getParent());
}
}
结果
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null
sun.misc.Launcher是jvm的程序调用入口
双亲委派
新建模拟java基础类
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("Hello");
}
}
运行main报错如下
加载类时不是从应用程序类加载器开始java设置先加载Bootstrap源码寻找类再加载Extension扩展源码寻找类如果存在包名类名一致的按优先级选择类Bootstrap>Extension>用户自定义AppClassLoader如果都没有则报classNotFound异常。这样保证了java源码安全性沙箱安全机制。
当一个类收到子类加载请求他着先不会尝试自己去加载这个类而是把这个请求委派给父类去完成每个层次类加载器都是如此因此所有的加载请求都应该传送到启动类加载其中只有当父类加载器反馈自已无法完成这个请求的时候在它的加载路没有找到所需加载的Class类加程器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位rt.jar包中的类java.lang.Object不管是哪个加载器加载这个类最终都是委托给顶层的启动类加载器进行加载这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
Native Interface
实例对象在堆引用在栈
类未实现方法不加native会报错navtive是关键字
Native主要用来调底层的第三方C/C++语言函数库dll有navtive标记的方法由第三方调用加载到native method 栈中。
本地接口的作用是融合不同的编程语言为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 执行时加载本地方法库。
PC程序计数器
类似汇编的cx寄存器
每个线程都有一个程序计数器是线程私有的就是一个指针指向方法区中的方法字节码用来存储指向下一条指令的地址也即将执行的指令代码由执行引擎读取下一条指令是一个非常小的内存空间几乎可以忽略不记。
这块内存区域很小它是当前线程所执行的字节码的行号指示器字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令如果执行的是一个Native方法那这个计数器是空的(其他语言)。
用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出OutOfMemory=OOM错误
方法区Method Area
供各线程共享的运行时内存区域。它存储了每一个类的结构信息类模板例如运行时常量池Runtime Constant Pool、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范在不同虚拟机里头实现是不一样的最典型的就是永久代PermGen space)和元空间Metaspace。
But
实例变量(main方法等)存在堆内存中和方法区无关
存放类模板+常量池+静态变量
java7 new 永久代
java8 new 元空间
java栈
栈管运行堆管存储
比如try,catch出异常打印的是e.printStackTrace();管理运行
栈也叫栈内存主管Java程序的运行是在线程创建时创建它的生命期是跟随线程的生命期线程结束栈内存也就释放对于栈来说不存在垃圾回收问题只要线程一结束该栈就Over生命周期和线程一致是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈存储什么
栈帧java栈中把方法压入一个方法就是一个栈帧中主要保存3类数据
- 本地变量Local Variables输入参数和输出参数以及方法内的变量。
- 栈操作Operand Stack)记录出栈、入栈的操作。
- 栈帧数据Frame Data包括类文件、方法等等。
栈运行原理
栈中的数据都是以栈帧Stack Frame的格式存在栈帧是一个内存区块是一个数据集是一个有关方法Method)和运行期数据的数据集当一个方法A被调用时就产生了一个栈帧F1并被压入到栈中
A方法又调用了B方法于是产生栈帧F2也被压入栈
B方法又调用了C方法于是产生栈帧F3也被压入栈
…
执行完毕后先弹出F3栈帧再弹出F2栈帧再弹出F1栈帧……
遵循“先进后出” / “后进先出”原则。
每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息每一个方法从调用直至执行完毕的过程就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关通常在256K~756之间等于1MB左右。
递归未中断的话报错误Error不是异常栈溢出错误一直不断放m1方法到栈
java栈有局部变量表、操作数栈、动态链接方法出口。
操作数栈存放值临时内存存放值后赋值给局部变量表。
动态链接是一个将符号引用解析为直接引用的过程那么虚拟机就必须解析这个符号引用。在解析时虚拟机执行两个基本任务
1.查找被引用的类如果必要的话就装载它
2.将符号引用替换为直接引用这样当它以后再次遇到相同的引用时它就可以立即使用这个直接引用而不必花时间再次解析这个符号引用了。
方法出口指方法中执行完其他方法后可以回来从刚离开的指令处继续执行。
栈+堆+方法区的交互关系
HotSpot(jdk名字)是使用指针的方式来访问对象。
Java堆中会存放访问类元数据的地址。
reference存储的就直接是对象的地址。
堆heap
一个JVM实例只存在一个堆内存堆内存的大小是可以调节的。类加载器读取了类文件后需要把类、方法、常变量放到堆内存中保存所有引用类型的真实信息以方便执行器执行堆内存分为三部分
- Young Generation Space 新生区 Young/New
- Tenure generation Space 养老区 old/Tenure
- Permanent Space 永久区/元空间 Perm
java7之前
一个JVM实例只存在一个堆内存堆内存的大小是可以调节的。类加载器读取了类文件后需要把类、方法、常变量放到堆内存中保存所有引用类型的真实信息以方便执行器执行。
物理上新生+养老
简单流程
java8
新生区是类的诞生、成长、消亡的区域一个类在这里产生应用最后被垃圾回收器收集结束生命。新生区又分为两部分伊甸区Eden Space和幸存者区Survivor Space所有的类都是在伊甸区被new出来的。幸存区有两个0区Survivor 0 space和1区Survivor 1 space。当伊甸园的空间用完时程序又需要创建对象JVM的垃圾回收器将对伊甸园区进行垃圾回收Minor GC)将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了执行引擎执行gc对该区进行垃圾回收然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了那么这个时候将产生MajorGC(FullGC进行养老区的内存清理。若养老区执行了FullGC之后发现依然无法进行对象的保存就会产生OOM异常“OutOfMemoryError”叫堆溢出异常。
如果出现iava,lang.OutofMemoryError:Java heap space异常说明lava虚拟机的堆内存不够。原因有二
- java出拟机的堆内存设置不够可以通过参数-Xms、-Xmx来调整。
- 代码中创建了大量大对象并且长时间不能被垃圾收集器收集存在被引用
实际而言方法区Method Area)和堆一样是各个线程共享的内在区域它用于存储虚拟机加载的类信息+普通常量+静态常量+编译器编译后的代码等等虽然JVM规范将方法区描述为堆的一个逻辑部分但它却还一个别名叫做Non-Heap(非堆目的就是要和堆分开。对于HotSpot虚拟机很多开发者习惯将方法区称之为“永久代Parmanent Gen)”但严格本质上说两者不同或者说使用永久代来实现方法区而己永久代是方法区相当于是一个接口interface)的一个实现jdk1.7的版本中已经将原本放在永久代的宁符串常量池移走。永久存储都是存储rt等永久的类数据。
永久存储区是一个常驻内存区域用于存放JDK自身所携带的Class,Interface的元数据也就是说它存储的是运行环境必须的类信息被装载进此区域的数据是不会被垃圾回收器回收掉的关闭JVM会释放此区域所占用的内存。
java7
java8
在Java8中永久代已经被移除被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间与永久代之间最大的区别在于
永久带使用的VM的堆内存但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
因此默认情况下元空间的大小仅受本地内存限制。类的元数据放入native memory字符串池和类的静态变量放入java堆中这样可以加载多少类的元数据就不再由MaxPermSize控制而由系统的实际可用空间来控制。
常见的垃圾回收算法
GC分代收集算法、
JVM在进行GC时并非每次都对新旧区元空间内存区域一起回收的大部分时候都是指新生区。
因此GC按照回收的区域又分了两种类型。一种是普通GCminor GC一种是全局GCmajor GC for Full GC
minor GC和major GC for Full GC
-
普通GCminor GC只针对新生代区域的GC指发生在新生代的垃圾收集动作因为大多数java对象存活率都不高所以普通GC非常频繁一般回收速度也比较快。
-
全局GCmajor GC for Full GC指发生在老年代的垃圾收集动作出现了全局GC经常会伴随至少一次的全局GC不是绝对全局GC的速度要比普通GC慢上10倍以上。
-
引用计数法
在双端循环互相引用的时候容易报错目前很少使用这种方式了。缺点计数耗费内存无法解决循环引用。
循环引用
objectA.instance = objectB;
objectB.instance = objectA;
System.gc();//手动gc
- 复制Copying
新生代使用minor GC老年代使用major GC for Full GC
复制算法在年轻代的时候进行使用复制时候有交换
幸存区0、幸存区1位置和名分没有区分不是固定的只是名字而已 都可以是From和To区
谁空谁是To区GC第一次交换找TO区TO区有数据变From区TO和From交换区名动态交换每次GC交换区。新创建的对象都会被分配到Eden区这些对象经过第一次Minor GC后如果仍然存活将会移到Survivor区。对象在Survivor区每熬过一次Minor GC年龄就会增加1岁默认增加到15岁移动到老年代中。由于年轻代基本90%都是未重复的类所以年轻代的垃圾回收算法使用的是复制算法将内存分为两块每次用其中一块当这一块内存用完就将还活着的对象复制到另外一块上复制算法不会产生内存碎片。
过程
在GC开始的时候对象只会存在于Eden区和名为“From”的Survivor区,Survivor“To”是空的。紧接着进行GCEden区中所有存活的对象都会被复制词“To”而在“From"中扔存活的对象会根据他们的年龄值来决定去向。年龄达到一定值年龄阈值可以通过-XX:MaxTenuringThreshod来设置的对象会被移动到年老代中没有达到阈值的对象会被复制到“To”区域。经过这次GC后Eden区和From区已经被清空这个时候“From”到“to”会交换他们的角色也就是新的“To”就是上次GC前的“From”新的“From”就是上次GC前的“To”。不管怎样都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程直到"To”区被填满“To"区被填满之后会将所有对象移动到年老代。
优点没有产生内存碎片
缺点浪费幸存区一半的内存如果对象存活率很高那需要将所有对象复制一遍并将所有引用地址重置一遍耗时。
- 标记清除Mark-Sweep
老年代一般是由标记清除和标记整理混合实现的
先标记后清除缺点是会产生内存碎片用于老年代多一些
缺点是两次扫描耗时严重会产生内存碎片
当程序运行期间若可以使用的内存被耗尽的时候GC线程就会被触发并将程序暂停随后将要回收的对象标记一遍最终统一回收这些对象完成标记清理工作接下来便让应用程序恢复运行。
- 标记整理Mark-Compact
标记然后整理对象不存在内存碎片
但是需要付出代价因为移动对象需要成本
新生代复制算法对象存活率低
老年代用标记清除与标记整理混合实现对象存活率高
内存效率复制算法=标记清除算法>标记整理算法。
内存整齐率复制算法=标记整理算法>标记清除算法。
内存利用率标记整理算法=标记清除算法>复制算法。
调优例子
对象动态年龄判断机制一批对象大于From幸存者内存50%直接存入老年代。
减少Full GC调优
java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar java.jar
Xms堆起始大小-Xmx堆最大大小-Xmn年轻代大小-Xss一个线程java栈大小XX:MetaspaceSize元空间即方法区大小-XX:MaxMetaspaceSize最大方法区大小。