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按照回收的区域又分了两种类型。一种是普通GC(minor GC),一种是全局GC(major GC for Full GC)
minor GC和major GC for Full GC
-
普通GC,minor GC:只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数java对象存活率都不高,所以普通GC非常频繁,一般回收速度也比较快。
-
全局GC,major 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”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制词“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最大方法区大小。