- JVM理论
#JVM内存模型#
Java的内存模型决定了线程间的通信方式,JMM的模型是由主存和工作内存构成,两个线程想要正常通信需要将工作内存中的变量刷到主存中,另一个线程才能正确读取得到,这个也是volitile关键词的原理所在。
JVM内存结构和Java内存模型的参考资料:JVM内存结构和Java内存模型 - 知乎
JVM主要由堆(JVM heap)、方法区(Method Area)、虚拟机栈(JVM stack)、本地方法区(Native Method area)、程序计数器(PCR),其中堆、方法区是线程共享的,和JVM的启动停止同步,虚拟机栈、本地方法栈、程序计数器(PCR)是线程不共享的,是线程私有的,和线程的创建停止同步。每个结构的用途如下:
- 堆主要存储的是对象和数组,他是JVM内存模型中最大的一个区域,其中又能分为年轻代和老年代,年轻代又能分为Eden区和Survivor区,Survivor区又能分为S0,S1区,这是JVM中发生GC最频繁的区域。
- 虚拟机栈是Java方法执行的内存区域,他的基本单位是栈帧,每发生一次方法调用,就将一个栈桢入栈,方法的执行都在这个帧里面。这个帧里面包括:局部数据表,操作栈,动态链接,方法返回地址,局部变量表主要存储局部变量和方法参数,操作栈是方法执行的各种指令就放在操作栈里面的,返回地址是记录方法调用的返回地址。
- 本地方法区类似于虚拟机栈,只是他是为了保存通过JNI方式操作本地方法的相关信息。
- 方法区是保存类Class元信息、静态变量、常量等信息,其中还包括运行时常量池,其中会保存字符串常量等信息。
- 程序计数器则保存了每个线程执行到的字节码的指令地址。
运行时内存结构:
JVM的堆中包括新生代(Young)和老年代(Old)区域,其中新生代包括Eden和Survivor区域,其中Survivor区域包括S0和S1区域。其中新创建的对象会首先发到新生代的Eden区,但是如果对象太大也会直接放到老年代,其中新生代的GC是Minor GC,Minor GC后存活对象会被移至Survivor区,在这里会进行Major GC,每一次GC都会在S0和S1中移动复制对象,每次移动都会将该对象的年龄+1,等到满足一定的阈值就会移动到老年代中,这个阈值的默认值是15。
- Minor GC:是在新生代发生的GC,准确来说是在Eden区发生的GC。触发条件是Eden的空间不够,会触发异常Minor GC。
- Major GC:是老年代空间不够触发的GC。或者从新生代进入老年代的对象空间大于空闲空间了。
- Full GC:是新生代和老年代共同GC。
对象内存分配的流程图:
JVM堆在创建对象时候过程:
- 如果JIT分析判断该对象是逃逸对象,就直接在栈上分配,就不存在线程安全问题;
- 如果判断需要在JVM栈上分配,会针对该线程在TLAB上申请一块区域,该线程分配的对象登记在该区域,用以解决多线程下可能发生的线程不安全问题;
- 如果该对象被判断为大对象,这直接进入老年代,不会在年轻代分配空间;
#GC垃圾回收器#
判断对象是否可回收的方法:
- 引用计数法:每个对象有一个引用计数器,每次被引用就+1,释放引用就-1,等到为0的时候就可以被回收;他的缺陷是可能存在循环引用的问题。
- 可达性分析:就是利用GC root到达对象的可达性来分析对象是否被引用,如果被引用就在这条引用链中,就不能回收;
GC root可以由一下对象充当:
- 虚拟机栈(方法)中引用的对象;
- 类变量(静态变量)引用的对象;
- 常量引用的对象;
- JNI引用的对象;
GC Roots 是什么?哪些对象可以作为 GC Root?看完秒懂!:
GC Roots 是什么?哪些对象可以作为 GC Root?看完秒懂!_一直Tom猫的博客-CSDN博客
GC算法分为分代算法和分区算法,以下1,2,3是分区回收算法,4为分代回收算法:
- 标记-清除(Mark-Sweep)算法:就是将失效的对象做标记,等要回收的时候再清空这个对象,这个算法是分代算法的基础。缺点是标记和清除的效率都不高,并且可能存在较多的内存碎片。
- 标记-整理(Mark-Compact)算法:为了更充分的利用内存区域,就是将失效的对象先标记,再整理到内存区域的一侧,这样整理下来的也就是一块完整的区域,可以存放大对象,这个是在老年代中使用的GC算法,原因是老年代中对象需要回收的少,触发GC的频率低。
- 复制(Copying)算法:为了弥补标记-清除算法可能造成内存碎片的问题,就是将内存区域划分为两块,每次就只用一块,等到一块要满了,再将有效的复制到另一块,这样就能保证一块永远是空的,这个在新生代中使用的GC算法,原因是新生代中对象需要回收的多,并且触发GC的频率高。
- 现代的GC都分为老年代和新生代了,针对各自的特征,选中不同的GC算法。针对新生代,他们的对象生存周期短,可以选择复制这样的GC算法,这样可以留出一大片空闲空间,而老年代的内存中对象由于更新的慢,可以选择标记-整理这样的单点清理GC算法,同时也能避免内存碎片的产生。
JVM之GC算法:https://www.cnblogs.com/BlueStarWei/p/9577388.html
GC回收器的种类:(1)serial回收器:就是在GC回收的时候停掉工作线程,他是一个串行的回收器;(2)parallel回收器:就是GC回收器是并发执行的;(3)CMS,他的全程是concurrent mark sweep,他的主要优势是在GC回收的时候不需要stop the world;(4)G1:这个是从JDK7后推出的新的GC;
CMS GC垃圾回收器:
- CMS是并发标记清除GC回收器件,其目标是获取最短GC停顿时间,比较符合大型web server项目的使用场景。其GC阶段经过4个阶段:
- 初始标记:初始标记又GC root引用的对象,该过程会引发STW;
- 并发标记:这个是不需要STW,并发标记要回收的对象;
- 重新标记:这个节点重新标记并发阶段产生的新的要回收的对象;
- 并发清除;并发清除标记的要回收的对象;
CMS总结:
- 优点:(1)并发处理效率高;(2)GC的时候不会整体停顿STW,有效降低处理时延。
- 缺点:(1)并发清理时会降低CPU性能;(2)标记-清理可能会造成大量内存碎片;(3)并发清理阶段还会产生垃圾,这种垃圾称为浮动垃圾,需要下一次GC时才能清理掉。
G1 GC垃圾回收器:
G1回收器会将区域划分为region,每个region可以是新生代也可以是老年代,通过控制对region的回收,做到对垃圾回收导致的STW可控。垃圾回收的阶段前3个阶段和CMS一致,只是最后一个节点需要通过混合清除来回收新生代和老年代所有的对象:
- 初始标记;标记GC root对象,需要暂停所有用户线程,该过程会引发STW;
- 并发标记;标记GC root可达的对象。
- 最终标记;标记在并发标记阶段产生的需回收对象。
- 筛选回收:对各个Region的回收成本和价值进行排序,根据用心要求的GC停顿时间来选择需要GC的Region。
G1总结:
- 优点:(1)并发处理效率高;(2)整体停顿STW的时间可控;(3)新生掉和老年代都分为逻辑上的region,通过GC的复制算法解决内存碎片的问题;
- 缺点:引入了Remembered Set来保存内存引用信息,所以增加了内存占用,所以G1一般在大内存的服务端环境使用,起步内存大小为6G。
GC回收器总结:
- 选择GC主要考虑的是使用场景,一般嵌入式、内存较小的选择串行GC回收器;
- 对于需求吞吐量大的常见可以选择并行GC回收器;
- 对于需要时延少的场景可以选择CMS或者G1回收器;
#JMV加载机制#
双亲委派模型的定义:
当前ClassLoader加载的class,先调用双亲ClassLoader,如果他们加载不了,当前ClassLoader才加载,所以所有的类都会被请求到Bootstrap ClassLoader,简单理解就是能双亲做的事,就双亲来做,双亲做不了的事情就自己来做;
双亲委派模型的作用:
- 保障类的唯一性:ClassLoader的双亲委派模型保障一个类在类加载器的唯一性,父类已经加载了该类,子类就不再加载。
- 保障按需加载:由于ClassLoader只有在需要某个类的时候才会加载某个类,这样避免一次性将类全部加载进入内存引发OOM。并且当ClassLoader加载一个类时,该类依赖的其他类也会由这个ClassLoader加载进来。
- ClassLoader是做什么的?他是通过类的全限定名将该类的字节码转化为内存中Class对象;
- 为什么会有ClassLoader这种东西?这是由于JVM对类的加载是按需延迟加载的,并不是一次将所有类加载到内存空间里,所以需要类加载器在内存中等待需要加载的类;
- ClassLoader的常用组件?最顶层的Bootstrap ClassLoader,主要负责加载rt.jar包里面的class,Extension ClassLoader主要负责加载javax里面的java ext等类,Application ClassLoader负责加载classpath里面的class文件,还有Customer ClassLoader,这个可以加载用户自定义的ClassLoader;
老大难的 Java ClassLoader 再不理解就老了:
老大难的 Java ClassLoader 再不理解就老了 - 掘金
类加载的过程是将类的字节码加载到内存中的过程,主要包括:加载-->链接-->初始化,其中链接还包括验证、准备、解析3个步骤。
- 加载:将class文件加载到内存,在方法区生成一个java.lang.Class对象放到方法区;
- 验证:验证这个class文件是否合法,包括文件格式的校验,元数据类型的校验等;
- 准备:为类变量分配内存空间,但此时只是初始化为默认值而非真实值,但对于final变量此时会初始化为真实值;
- 解析:将符号引用(相对引用)转换为直接引用,符号引用是class文件的相对表达方式,直接引用就是在该系统里地址指针,比如hello()方法为符号引用,0x12345678为直接引用;
- 初始化:初始化类变量成真实值,初始化静态代码段,实际执行的是类构造器方法。
注意:类的初始化过程是懒加载的策略,只有当该类被使用了才会被初始化,实际就是调用方法执行的过程;会触发类的初始化操作条件为:(1)需要创建新的对象,执行了new操作;(2)调用了类的静态变量或静态方法;(3)通过反射机制来获取某个类的时候;
Java的对象实例化的过程是调用方法,在进行new操作的时候会执行实例化操作,实例化的过程主要是调用构造方法的过程。在进行对象实例化之前,会初始化静态变量和静态代码段,然后初始化变量和代码块,最后调用构造方法进行实例化。对象实例化过程:
有父子关系的初始化过程:先初始化父类,再初始化子类。
同一个类的class文件被同一个classloader加载,在JVM中才能判断这两个类对象相等。
这个是指保存在JVM堆区的的java.lang.Object对象,主要包括对象头,实例数据和对齐填充数据组成。
- 对象头:包括(1)Mark Word,这个字段包括GC标志位,锁状态,hashcode等数据,其中锁状态包括能表征对象加锁是无锁、偏性锁、轻量级锁、重量级锁。(2)Klass Pointer(类型指针),指向方法区的类信息的指针。还有Array Length,这个当对象是数组时会存在。
- 实例数据:这个保存实例变量,存储对象真正的数据。
- 对齐填充数据:由于JVM规定对象数据必须为8字节的整数,这个部分用来做数据填充。
Java对象在内存的结构:Java对象在内存的结构 - 掘金
Java 对象结构:Java 对象结构 - 掘金
- JVM应用
JVM常用参数:
- -Xms设置堆的最小空间大小。
- -Xmx设置堆的最大空间大小。
- -XX:NewSize设置新生代最小空间大小。
- -XX:MaxNewSize设置新生代最大空间大小。
- -XX:PermSize设置永久代最小空间大小。
- -XX:MaxPermSize设置永久代最大空间大小。
- -Xss设置每个线程的堆栈大小。
JVM常用命令和工具:
- jps:java线程查看工具
- jstack:java线程栈的查看工具
- jmap:java中堆的查看工具
- jstat:java内置的资源和性能监控工具
- jinfo:实时查看和调整jvm参数
【JVM进阶之路】十:JVM调优总结:【JVM进阶之路】十:JVM调优总结 - 知乎
- GC优化的重要指标看GC频次和GC时长,可以通过命令:jstat查看;
- GC优化的重要指标看CPU和内存的占用率,一般在不高于70%的利用率比较合适;
- 可以调整young/old分区大小和分区比例;
- 可以调整进入永久代的年龄阈值,默认是15次;
- 可以调整是否使用偏向锁;
一次简单的 JVM 调优,拿去写到简历里:
一次简单的 JVM 调优,拿去写到简历里
- top查询得到哪个进程id出现问题,进而得到哪个线程id出问题,jps查询哪个线程id出现了问题;
- jmap查看堆内存,可以dump下来;
- jstack查看线程堆栈问题;
- jstat进行资源监控,可以查看GC数据;
- 其他
- 对于堆中的OOM,只需要一直创建对象;
- 对于栈中的OOM,只需要一直创建线程或者进行递归调用就好;
- 对于方法区的OOM,只需要一直通过反射的方法创建类就好了;
注意:创建之前要设置堆栈的大小;
- 强引用:只要引用存在,在gc root的引用链中,即可一直存在,gc不掉。最常见的一种形式:Object obj = new Object();
- 软引用:gc不掉,但是当oom时,再次gc就会被gc掉,常用来做内存敏感的缓存;
- 弱引用:gc一次就会被gc掉,常用来做weakHashMap来做缓存;
- 虚引用:是无法使用的引用,一般用来通知某对象被gc了。
引用类型:强软弱虚
剖析 JDK:强引用、软引用、弱引用、虚引用有何区别?:
剖析JDK:强引用、软引用、弱引用、虚引用有何区别?_28天写作_后台技术汇_InfoQ写作社区
这道题考察的关键是:如果不想要发生Full GC,则让对象只在新生代中进行GC,尽量不要转移至老年代,所以应该调整的是新生代的空间大小和比例。
- 编译型语言是通过编译器将源码编译成机器能识别的机器码,在机器上能直接执行,比如C/C++;
- 解释型语言是不需要进过编译,直接通过解释器就能执行,比如JavaScript;
- Java的执行过程是先通过javac编译器,编译成class字节码文件,再在不同平台的JVM上解释执行,针对其中的热点代码,还会使用JIT技术直接编译成机器码,所以客观来说Java既不是编译型也不是解释型语言,是结合了他们两者的混合执行方式。
- 逃逸分析:逃逸分析的基本行为是分析对象的作用于,一个对象如果在方法中创建,但是在方法外也会被引用,这就称为该对象方法逃逸,反之如果该对象只在该方法内使用则该对象未发生方法逃逸,针对对象的逃逸分析后续可以进行一系列的优化策略。
- 栈上分配:一般对象存放在JVM的堆上,针对未发生方法逃逸的对象,即他的作用域只在该方法内,所以可以在JVM栈上分配对象。
标量替换:标量在程序中是指无法再分解成更小数据单元的对象,比如Java中的基础数据类型就是标量,而聚合量是指可以再分解成更小数据单元的对象,比如Java中的类对象,其至少能分解成成员变量和方法。针对未发生方法逃逸的对象,则可以直接在方法内进行标量替换,这样就不用分配到JVM的堆上。