JVM笔记
JVM先看的章节:1,2,3,4,5,12,13
JVM在运行时,将运行时的内存划分为多个区域,用于不同用途,划分区域包括:方法区,虚拟机栈,本地方法栈,堆,程序计数器
1、程序计数器:可看做当前线程所执行的字节码行号指示器,存储当前线程执行到的指令地址。是线程独有的,线程中断,异常等都依赖它。
2、虚拟机栈:每个Java方法执行时,会创建一个栈帧,用于保存该方法的局部变量,操作数栈等,一个方法从调用到执行完成的过程, 就是一个栈帧在栈中入栈和出栈的过程。局部变量表包含了基本数据类型,对象引用,returnAddress类型(用于指定下一条该执行的字节码指令地址)。 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;虚拟机栈内存大小可以动态扩展,如果扩展不能申请到内存时,会抛出OutOfMemoryError异常。
3、本地方法栈:和虚拟机栈类似,只是为Native方法服务。它也会抛出StackOverflowError和OutOfMemoryError。
4、Java堆:对于大多数应用来说,堆是虚拟机管理内存中最大的一块。堆是线程共享的,用于存放对象实例。Java堆是虚拟机垃圾回收最主要的区域。 Java堆中分为新生代和老年代,新生代又分为:Eden区,From Survivor和To Survivor。堆内存也可以动态扩展,如果无法申请到内存时,会抛出OutOfMemoryError。 5、方法区:用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等。方法区是线程共享的。也被称为“永久代”,JVM垃圾管理器会管理该部分内存。 也会抛出OutOfMemoryError。这块区域的回收类型包含常量池回收和对类型的卸载。但往往回收效果很小。
6、运行时常量池:它属于方法区的一部分。Class文件中除了有类的版本,字段,方法,接口,等描述信息外,还有常量池信息(与运行时常量池不同), 用于存放编译期生成的各种字面量和符号引用,需要符合虚拟机的规范。但运行时常量池虚拟机没有做要求,不同虚拟机可自行实现,它不要求只有在编译期的常量才能放进去, 在运行期间也可以将新的常量放入池中,比较多的是String的intern方法。
java对象创建的过程:
1、虚拟机遇到new时,虚拟机先去检查该类是否被初始化,如果没有,则先执行类加载过程。
2、检查完成后,为新的对象分配内存。对象所需大小在类加载完成后,就可以完全确定。
3、初始化内存和对象信息。
对象在对内存中存储的布局分为:对象头,实例数据,对齐填充。 对象头分为两部分,一部分用于存储对象自身的运行时数据,包括hashcode,GC分代的年龄,锁状态,线程持有的锁,偏向线程的ID,偏向时间戳等。官方称为“Mark Word”。 对象头的另一部分是类型指针,指向对象的class类。 如果对象时一个数组,还需一部分,用于存储数组的长度。 实例数据用于存储对象的实例信息。 填充部分,由于jvm要求对象的额其实地址必须是8的倍数,而对象头时,如果实力数据不是8的倍数时,就需要填充部分将少的补齐。
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory; 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。 memory leak会最终会导致out of memory! 1、静态集合类引起内存泄露: 像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10); for (int i = 1; i<100; i++) { Object o = new Object(); v.add(o); o = null; }
JVM虚拟机垃圾回收时,需要判断对象是否已死(没有被任何的使用),以下有几种算法
1、引用计数法,使用了计数加一,引用失效计数减一,该方法不能解决互相引用的问题,所以,主流java虚拟机都没有采用。
2、可达性分析算法:判断对象和“GC root”是否存在引用链,没有就表示没有引用,标识该对象可以回收。(即以下几个地方都没有调用链可以指向该对象,则该对象被标记为回收。)
GC ROOT的对象包括以下几种:
1、虚拟机栈中引用的对象
2、方法区静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中引用的对象
Java中将引用细分为四种:强引用,软引用,弱引用,虚引用。 强引用:最常用的引用,String s = new String("s")这种,如果存在强引用的对象,在GC的时候,就算抛出OOM也不会被回收; 软引用:在GC时,如果内存不够,则会对软引用对象进行回收。常用于作为缓存。 弱引用:在GC时,不管内存够不够都会被回收。 虚引用:在程序运行的任何时候都可能被回收。
垃圾回收主要涉及两部分:堆和方法区,堆中主要是对对象的回收,方法区主要是对常量和无用类的回收(方法区主要存储类信息和常量,常量池就在方法区)。 常量池中回收原则:假如一个String abc进入了常量池,如果没有任何对象引用该常量地址,也没有其他String引用了“abc”这个字面量时,才会被回收。(也就是说没有任何地方有“abc”的引用存在)。 类需要满足以下条件才会可能被回收:
1、类的对象都已被回收,即不存在该类的任何实例。
2、加载该类的ClassLoader已被回收。
3、该类对应的Class对象,没有在任何地方被引用,无法在任何地方通过反射访问到该类。
垃圾回收算法 标记清除算法: 被分为标记和清除两步,需要被回收的对象(对象是否已死)会被标记,在标记完成后,统一回收被标记的对象。它是最标准的垃圾收集算法。 它存在以下问题:效率问题,标记和清除的效率都不高;空间问题,由于清除的对象内存地址不连续,清除后,会产生很多内存碎片,空间碎片太多,可能引起大对象分配内存时,内存不够而频繁触发GC。 复制算法: 准备两块相同大小的内存,一份先用,一份备份,当使用的那份内存用完后,将还存活的对象复制到另一备份内存,然后将已使用的那份内存一次清除。 问题:内存占用多,实际存储对象的内存只有分配内存的一半。基本上所有的商业虚拟机,在新生代中的垃圾回收算法就是使用复制算法。 标记整理算法 根据老年代的特点产生,原理是将所有存活的对象移向一端,将末端之后的所有内存清理。 分代收集算法: 实际就是将堆内存分为新生代和老年代,根据其特点,采用不同的收集算法。新生代每次GC会出现大量对象死亡,只有少量存活,所以采用复制算法。老年代对象存活率较高,并没有额外空间给他分配,所以采用标记清除或标记整理算法。
内存分配及回收策略:新生代GC(Minor GC),老年代GC(Major GC、Full GC)
1、对象优先在Eden区分配,当Eden区没有足够内存时,会触发一次Minor GC,它会回收正在使用的Eden区和一个survivor区,将存活对象复制到另一个survivor区,同时清空Eden区和Survivor。 回收时,如果复制到Survivor的对象大于单个Survivor的大小,则将通过分配担保机制提前将对象转移到老年代。 运行参数设置:-Xms20M,-Xmx20M,-Xmn10M,分配20M作为堆内存,且不可扩展,10M分配给新生代,剩余10M分配给老年代。-XX:SurvivorRatio=8,决定了Eden区占用8份8M,剩余平均分配给两个Survivor区各1M。
2、大对象直接进入老年代,大对象一般指很长的字符串和数组等,虚拟机提供-XX:PretenureSizeThreshold参数来设置大于这个值的对象直接在老年代分配。
3、长期存活的对象将进入老年代。对象在Eden区被分配,且在Minor GC后,能正常放入Survivor区,则年龄增加1,当这样熬过15岁后,就会被放入老年代。提供-XX:MaxTenuringThreshold=15,来设置存活年龄的最大值。
4、动态年龄的判定。为了适应不同的内存状况,虚拟机不一定在对象满足最大年龄后,才放入老年代。如果在Survivor区中相同年龄的对象大小总和大于Survivor区空间的一半后,大于或等于该年龄的对象就会在回收时直接进入老年代。
5、空间担保分配。在发生Minor GC的时候,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于,则确保了新生代的所有对象都可以正常进入老年代,此时Minor GC是安全的,则可以正常进行Minor GC。 如果不满足,则代表可能有对象在Minor GC后,不能被正确分配空间。此时,虚拟机会查看HandlePromotionFailure是否允许担保失败,如果允许,虚拟机会检查老年代最大的连续空间是否大于历次晋升老年代的对象大小的平均值, 如果大于,则会进行一次Minor GC,如果Minor GC过程中,对象晋升时,老年代空间不够,则会触发一次Full GC。如果检查小于平均大小或HandlePromotionFailure不允许担保失败,则会进行一次Full GC。 -XX:HandlePromotionFailure=true/false,来设置是否允许担保失败。JDK 6之后,这个参数设置不启用,规则变为只要老年代连续空间大于新生代所有对象大小或大于历次进入老年代对象的平均值,就会进行Minor GC,否则将会进行Full GC。
总结一下触发Minor GC和Full GC 的时机。
JAVA内存模型与线程 内存模型中主要运用了高速缓存和指令重排来优化执行效率。这两种情况也是导致线程冲突的主要原因。 Java内存模型JMM,是用来屏蔽掉各种硬件和操作系统的内存访问差异的,一实现不同平台下,能达到一致的内存访问效果。
JDK6以后对synchronized进行了优化,使用了偏向锁,轻量级锁,重量级锁的切换机制来保证优化,具体可参考:https://www.jianshu.com/p/e7aa0a5083fb 实际就是在单个线程时,没有线程竞争,则直接使用偏向锁,省去了加锁,解锁等耗费性能的步骤,线程直接访问同步块。在另外的线程来了之后,锁变成了轻量级锁,当某个线程占用锁时,其余线程采用自适应自旋来获取锁, 这种情况是默认为线程竞争不激烈的情况。如果自旋获取锁失败,则会升级为重量级锁。
JVM性能监控工具
1、jps 虚拟机进程状况工具 直接用显示java进程id和进程主类名 jps -m ,输出启动时传给main函数的参数。 jps -l,输出主类路径或jar包路径 jps -v,输出虚拟机进程启动时JVM参数
2、jstat,虚拟机统计信息监视工具,可以显示本地货远程虚拟机进程中类加载、内存、垃圾收集、JIT编译等运行数据。在没有GUI的界面时,是定位性能问题的首选工具。 jstat -gc 进程ID 250 20 ,查询gc详情,250统计一次,共统计20次。 选项有:-class,类加载信息。-gcnew,-gcold等,只是查询部分gc情况。
3、jmap,用于生成dump文件,主要用堆的dump文件,可用来分析内存溢出等情况。 linux下切换到JDK_HOME/bin/,执行以下命令:./jmap -dump:format=b,file=heap.hprof 2576 这样就会在当前目录下生成heap.hprof文件,这就是heap dump文件。
4、jstack,java堆栈跟踪工具,用于生成虚拟机当前时刻的线程快照。线程快照主要用来分析线程停顿原因,死锁,死循环等。jstack -l pid.
虚拟机类加载机制 类的整个生命周期: 加载-验证-准备-解析-初始化-使用-卸载 其中,加载,验证,准备,初始化和卸载这五个阶段是确定的,解析阶段不一定,在某些情况下可以在初始化之后再开始。 虚拟机规范规定了有且只有以下五种情况才会立即对类进行初始化: 1、遇到new,getstatic,putstatic,invokestatic这四条字节码指令时,如果类没有初始化,需要先触发其初始化。四条语句的场景:使用new关键字创建对象;读取或设置一个类的静态字段(final修饰的除外);以及调用一个类的静态方法。 2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,先触发其初始化。 3、初始化一个类时。如果其父类没有初始化,需要先触发其父类的初始化。 4、启动虚拟机时,虚拟机会先初始化包含用户指定main方法的类。 5、当使用动态代理语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且所对应的类没有进行初始化,则需要先触发其初始化。 这五种情况被称为主动引用,其余不会触发初始化的引用被称为被动引用,下面是例子: 1、通过子类引用父类的静态字段,不会导致子类的初始化。 2、通过数组定义来引用类,不会触发此类的初始化,如:SuperClass[] aa = new SuperClass[10]。 3、常量在编译时,会存入调用类的常量池中,本质上并没有引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载过程具体描述: 1、加载:在加载阶段,虚拟机主要做以下三件事,通过一个类的全限定名类获取此类的二进制字节流;将此字节流锁代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。 2、验证:这一阶段主要是验证class文件的字节流包含信息是否符合虚拟机要求。 3、准备:准备阶段正式为类变量分配内存并设置初始化的值。其中初始化值的举例:int-0,long-0l,boolean-false,reference-null,char-'\u0000',byte-0 4、解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用:以一组符号来描述锁引用的目标;直接引用是直接指向目标的指针等。 5、初始化阶段是根据程序制定的主管计划去初始化变量和资源。初始化时执行类构造器<clinit>()方法的过程。 <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。静态语句块只能访问定义在它之前的变量,定义在它之后的变量,只能赋值,但不能访问。 由于其顺序决定,最先执行的是父类的<clinit>()方法,所以最先执行的是Object对象的方法,这也意味着,父类的静态语句块要优先于子类的。