JVM知识集

java代码执行流程

java代码加载执行过程

  1. Java源码文件经过编译,成可解释执行的class二进制文件;由JAVA虚拟机负责解释执行
  2. class类文件由类加载器classLoader负责加载(类加载过程包含加载、链接、使用、卸载的过程)
  3. calssLoader类加载器分为三个层次:
    * BootStrapClassLoader:加载系统,由C++实现;主要是加载rt.jar中的class
    * ExtClassLoad平台类加载器: 主要是加载jre\ext*.jar
    * AppClassLoader应用类app加载器 -> 可以自定义扩展类加载器,如通过网络加载,类加密等
    双亲加载机制:系统类由系统类加载器负责,而自定义类由其他的类加载器负责。(类加载器有层次关系,类加载时,先由父 类加载器加载,加载不到时,再调用本类加载器的findclass方法)
  • Java是直接通过指针进行的程序访问,所以它没有采用句柄的形式操作,这样使得程序的性能更高。 (句柄是指向指针的指针,因为windows操作系统的内存会释放在需要时再分配,导致其指针值是变化的,句柄不变)
    传统意义上来讲,JVM一共分为三种(虚拟机是一个公共标准):
     【SUN】从JDK 1.2开始使用了HotSpot虚拟机标准(2006年开源、利用C++实现、一些JNI部分使用的是系统提供的C程序实现的、JIT即时编译器);
     【BEA】使用了JRockit虚拟机标准,例如:WebLogic;
     【IBM】开发了JVM’s(J9)虚拟机;

jvm内存结构(java运行时数据区)

参考文章:link
Java运行时数据区
根据JVM规范,JVM内存一共分为方法区、堆、栈、本地方法栈、程序计数器五个部分

  1. **方法区:最重要的内存区域,多线程共享,保存了类的信息(名称、成员、接口、父类),反射机制是重要的组成部分,动态进行类操作的实现; 运行时常量池(如字符串常量)。永久代(permGen space)是hotspot虚拟机的方法区实现(1.7以前),1.8以后叫元空间(Metaspace),功能类似,都是jvm规范中方法区的实现,但是元空间被移出了堆内存,直接使用本地内存,目的可能是: * 字符串存在永久代中,容易出现性能问题和内存溢出; 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出; 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

  2. **堆内存(Heap):**保存对象的真实信息,该内存牵扯到释放问题(GC),多线成共享
    1.8以前的堆内存结构
    1.8及以后的堆内存(年轻代和老年代)
    堆内存层级结构

    当内存不足的时候就需要进行伸缩区的控制,当内存充足的时候就需要考虑将伸缩区所占用的内存释放掉(收起),一定会造成额外的计算性能的影响,导致程序的整体性能下降。
    默认情况下:
    MaxMemory:整体电脑内存的1 / 4;
    TotalMemory:整体电脑内存的1 / 64;
    伸缩区的空间:MaxMemory – TotalMemory 。默认情况下伸缩区空间较大
    程序优化执行的时候设置两个值相等,以减少伸缩区控制带来的性能下降(-Xmx10G -Xms10G):
    -Xmx:堆内存最大允许大小,一般不要大于物理内存的80%
    -Xms:堆内存初始大小

  3. **栈内存(Stack):**线程的私有空间,在每一次进行方法调用的时候都会存在有栈帧,每次方法调用都会涉及栈桢的创建和销毁,栈采用先进后出的设计原则; 栈中保存栈帧。栈帧中包含以下内容:
    * 本地变量表;局部参数或形参,允许保存有32位的插槽(Solt),如果超过了32位的长度就需要开辟两个连续性的插槽(long、double)—— volatile关键字问题;
    * 操作数栈:执行所有的方法计算操作。操作数栈可以理解为栈帧中用于计算的临时数据存储区
    * 常量池引用:String类实例、Integer类实例 (常量池在方法区)
    * 返回地址:方法执行完毕后的恢复执行的点;
    每个线程都有独立的栈空间,每个方法在执行的开始和结束都对应这栈中栈帧的压栈和出栈:
    方法调用压栈和出栈
    使用 -Xss 设置栈大小,通常几百K就够用了。由于栈是线程私有的,线程数越多,占用栈空间越大。

  4. **程序计数器:**执行指令的一个顺序编码,该区域的所占比率几乎可以忽略。可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

  5. **本地方法栈:**与栈内存功能类似,区别在于是为本地方法(Native)服务的;

  • 下图是jvm可以访问的所有内存空间,除了jvm内存外,还包含本地内存(元空间、直接内存(nio中的bytebuffer)),如下图所示:
    jvm可访问的所有内存空间

重要的jvm参数

jvm内存参数
mx /ms通常设置成一样的,避免jvm动态调整内存大小。OutOfMemeryError: java heap space
mn推荐为最大堆内存的3/8,如果使用cms垃圾回收器,一般设置成1/4。默认是1/15
ss默认是1m,一般会调成128k/256k,如果调用栈复杂可能调成512k,一般不会大于1m 。 stackOverFlowError
本地方法栈溢出。OutOfMemeryError: unable to create new native thread(32位系统等情况内存比较小的情况下创建过多的线程)
X开头的参数是jvm标准的稳定的参数,XX开头的参数是非标准参数,可能不是所有类型的jvm都支持
元空间受限于物理内存的大小,一般不会发生内存溢出,但是一般生产商还是会加上限制,一般是在512m到1g之间。OutOfMemeryError:Metaspace
代码缓存是存放jit编译器二次编译后的缓存,jdk8一般不需要调整。
直接内存溢出:OutOfMemeryError at Unsafe.allocateMemory。(nio操作)

对象实例化过程

参考文章:link
对象的实例化过程分两部分: 类的加载初始化;对象的初始化

  1. 类的加载初始化:

    • 加载:载入class(文件、jar、动态生成的字节码等)到方法区
    • 连接:验证(验证class字节流是否符合jvm规范)、准备(为类变量分配内存并赋默认值)、解析(将常量池的符号引用替换为直接引用)
    • 类初始化:执行类构造器(clinit)为类变量赋值,执行静态代码块;在子类初始化前先执行父类的构造器和静态代码块
      1.1 类初始化的条件:
    • 第一次创建类的对象时,先做类的加载初始化,再做对象的初始化
    • jvm启动时会先加载初始化带main方法的类
    • 调用类的静态方法
    • 类的静态字段的读写
    • 反射加载类
    • 初始化类时,其父类没被初始化
  2. 对象的初始化:执行类的构造函数,class中的init()方法(实例构造器),包含非静态变量\非静态代码块\构造器(可重载)组成

    • ()方法中的代码执行顺序为:父类变量初始化,父类代码块,父类构造器,子类变量初始化,子类代码块,子类构造器。

父子类的类和对象的初始化顺序
3. 类加载器和双亲委派机制
* 类加载器:通过类的全限定名来获取一个类的二进制字节流,实现这个获取动作的代码叫做加载器。jvm通过类本身和对应的加载器确定类在jvm中的唯一性。
* 同一个类由不同的类加载器加载,则被jvm识别为不同的类,java给出解决方法是下层的加载器加委托上级的加载器去加载类,如果父类无法加载(在自己负责的目录找不到对应的类),而交还下层类加载器去加载。
* 双亲委派的加载机制不是强制的,如在Java核心库中定义的spi接口,由各厂商去做对应的实现(如jdbc链接),此时spi接口由bootstrap加载,但实现只能由appclassloader加载,破坏了双亲委派的类加载模型。线程上下文类加载器,Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作

内存回收模型(java垃圾回收机制GC)

垃圾回收示意图

  1. GC处理过程:
    • 对象实例化时(new),新对象会在伊甸园区开辟内存,如果伊甸园区内存不足,也发生minorGC
    • 经过N次minorGC后,可能有些对象还会存在,这些对象会进入存活区(存活区有两个,一个负责保存存活对象,一个负责晋升,永远有一个是空内存)
    • 经过若干次minorGC后,年轻代内存还是不够用,则将存活区的对象晋升到老年代;如果老年代的空间不足,则会进行老年代的GC(fullGC)。
    • 如果minorGC失败,则继续进行fullGC,如果fullGC失败,则报OOM
    • 如果新创建的对象过大,则将直接保存到老年代中
    • 垃圾回收的对象是通过可达性分析无法搜索到的对象,会进行标记
-Xmx20M -Xms20M -XX:+PrintGCDetails 
-Xmx2M -Xms2M -Xlog:gc*
JDK 1.8的时候默认会根据系统的不同而选择不同的Gc回收策略; 
JDK 1.9 ~ JDK 1.11:使用的默认Gc操作就是G1。 
  1. 垃圾回收算法(分带的垃圾回收算法)
    jdk8垃圾回收算法
    jdk8生产级一般使用useConcMarkSweepGc,年轻代使用pernew ,老年代使用cms,cms无法回收时会降级到serial old垃圾回收器。
    pernew +cms更关注应用挂起时间,parraller更关注应用的吞吐量

    • 年轻代(minorGC):复制清理算法
      具体过程:当GC线程启动时,会通过可达性分析法把Eden区和From Space区的存活对象复制到To Space区,然后把Eden Space和From Space区的对象释放掉。当GC轮训扫描To Space区一定次数后,把依然存活的对象复制到老年代,然后释放To Space区的对象。

    • 老年代回收算法:标记算法
      对于用可达性分析法搜索不到的对象,GC并不一定会回收该对象。要完全回收一个对象,至少需要经过两次标记的过程。
      第一次标记:对于一个没有其他引用的对象,筛选该对象是否有必要执行finalize()方法,如果没有执行必要,则意味可直接回收。(筛选依据:是否复写或执行过finalize()方法;因为finalize方法只能被执行一次)。
      第二次标记:如果被筛选判定位有必要执行,则会放入FQueue队列,并自动创建一个低优先级的finalize线程来执行释放操作。如果在一个对象释放前被其他对象引用,则该对象会被移除FQueue队列。
      “标记-清除”算法:先进行对象的第一次标记,在这段时间之内会暂停程序的执行(如果标记的时间长或者对象的内容过多),这个暂停的时间就会长;就会产生串行标记、并行标记使用问题;
      “标记-压缩”算法:基于“标记-清除”算法,将零散的内存空间进行整理重新集合再分配;

    • cms算法在实现上分了两类:参考文章
      cms参数配置

      1. foreground collector 。对象内存分配不够时,立即触发,采用标记清楚算法,不压缩。
      2. background collector。后台线程不断扫描(默认2s扫描一次),满足一定条件下触发。1. System.gc();2. 动态计算(根据历史数据评估,如果进行一次cms的时间大于剩余空间内存填满的时间,则触发,第一次是根据内存的使用比例50%。仅未配置 UseCMSInitiatingOccupancyOnly 时);3. 老年带的使用空间大于配置的阈值时(CMSInitiatingOccupancyFraction,默认值是-1,根据默认的计算公式算出阈值为92%),如果配置,一般设置为70-80%;4. 通过预估young gc晋升到老年代的大小是否大于老年代的剩余空间来判断;5. 在配置了cms回收元空间时,如果元空间进行一次扩容会触发。一般建议配置-XX:+UseCMSInitiatingOccupancyOnly , -XX:CMSInitiatingOccupancyFraction,只会根据配置的阈值来触发是否background cms, 能够比较确定的执行cms,也方便排查gc的原因
        有参数控制background cms是否进行内存压缩,需要配置减少内存碎片(-XX:+UseCMSCompactAtFullCollection)
        有参数控制是否cms回收原空间(-XX:+CMSClassUnloadingEnabled)
    • 可以通过命令触发一次full gc:参考文章
      jmap -histo:hive:会触发Full GC(打印活跃对象内存占用情况,一般在jmap导出堆转储文件时,先触发一次full gc,使的导出的都是无法释放的对象)
      jmap -histo:不会触发Full GC。(打印对象内存占用情况)
      jmap -dump:live,format=b,file=output.hprof pid :dump存活对象,会触发一次fullgc

  2. G1垃圾回收算法
    Jdk8开始支持G1算法,使用虚拟机参数 -XX:+UseG1GC 使用G1算法
    JDK 11之后默认就是G1回收器,对于其他的回收算法实际上就可以忽略掉了。
    G1算法内存拆分
    STW-stop the world
    将堆内存分成若干个单元,每个单元都有年轻代、年老代,垃圾回收时可以每个单元独立并行回收

jvm常用工具

  • jps -jvm进程信息
  • jstat -jvm统计信息工具。常用的jstat -gc pid查看jvm堆内存情况,垃圾回收信息等情况
  • jmap -jvm内存镜像映射工具,生成堆转储文件。jmap -dump:format=b,file=fliename pid
  • jstack-jvm线程栈信息。生产上单个cpu使用率高,可能代码中有死循环,可以打印出栈信息,结合top -Hp打出java进程每个线程的cpu占用情况,找到对应线程的线程号(10进制转换为16进制后),在jstack文件中查找哪段代码死循环。
  • 堆转储文件分析工具:Eclipse MAT工具,导入堆转储文件。如果dump文件比较大,可以在Linux下使用mat命令行模式,以无图形化界面的方式分析,导出结果后在eclipseMat图形化界面或浏览器中查看结果。参考文章

生产上出现内存溢出时分析思路

  1. 查看监控工具,确定内存溢出发生的应用系统虚拟机
  2. jps查看应用的进程号
  3. 使用top分析应用进程占用的cpu、内存情况
  4. top -Hp pid 分析应用进程中每个线程占用的cpu、内存情况
  5. jstack到处栈信息
  6. jmap导出堆转储文件
  7. 重启应用
  8. 查看应用日志,分析jvm内存溢出的位置(堆、栈、元空间、直接内存等)
  9. 用mat分析堆转储文件,分析内存溢出的具体原因
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值