B站 https://www.bilibili.com/video/av70549061
https://www.bilibili.com/video/av70549061
定义:Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境,将class文件通过类加载器加载到jvm中)。 好处:
1.面试。提升面试竞争力。 2.帮助理解底层的实现原理。 3.中高级程序员的必备技能。比如遇到内存溢出,响应时间缓慢,很多时候这种问题与jvm相关。
jvm是一套规范,很多公司遵循这套规范开发了自己的jvm。使用的时候,需要注意,有使用许可的,比如hotspot,需要商业许可。可以用openjdk替换掉。阿里也有自己的jdk。下文中的内容都是针对hotspot的实现来讲的。
jvm分成3部分:类加载器、java内存结构、执行引擎三部分。 一个类从java源代码编译为class文件(二进制字节码)以后,必须经过类加载才能被加载到jvm里。类都是放在方法区,类将来创建的实例是放在堆的部分,堆里面这些对象,在调用方法时,又会用到虚拟机栈、程序计数器、本地方法栈。方法执行时,是由每行代码中的解释器逐行进行执行,热点代码(频繁调用的代码)会由JIT进行一个优化后的执行。GC会对堆中一些不再被引用的对象进行垃圾回收。对于一些java代码不方便实现,必须调用底层操作系统的功能,就需要借用本地方法接口来调用操作系统提供一些功能方法。
案例引入:二进制字节码 vs java源代码
作用:用于保存JVM中下一条所要执行的指令的地址。 从物理上,实现程序计数器,是通过寄存器来实现的。上图中的jvm指令前面的数字(0,3,4…),就是指令的地址。寄存器是cpu读取最快的单元。
栈的数据结构的特点:先进后出。 虚拟机栈就是线程运行时需要的内存空间。每个线程都有自己的内存空间。 栈中的执行单元叫做栈帧。 简单理解,一个栈帧就是一次方法的调用。栈帧是每个方法运行时需要的内存。 栈帧中有什么:参数,局部变量,返回地址。
代码举例: 使用idea的debug运行。在debug界面,可以看到栈帧的情况,以及栈帧中的参数、局部变量的情况。 在栈顶部的那个栈帧叫做活动栈帧,一般是正在执行的那个方法所用的内存空间。
1.垃圾回收不需要涉及栈内存。因为栈帧内存在方法执行完毕之后会自动的弹出栈被回收掉。所以根本不需要垃圾回收来管理栈内存。垃圾回收只是回收堆内存中的无用对象。 2.通过Xss可以设置栈的大小,如下图。栈内存越大,反而会让你的线程数变少。因为物理内存的大小是一定的,单位内存越大,能分配的单位的数量越少。栈的空间大了,能支持更深的方法调用,而不会增加整体的执行效率。 总结:一般采取系统默认的栈的大小就可以了。
是否线程安全只需要考虑,这个变量是多个线程共享的还是一个线程私有的。这里的问题3答案是不会。因为一个线程对应一个栈帧,一个栈帧中有自己的局部变量,是线程私有的,不是共享的,因为局部变量的位置在栈中,而栈又是线程私有,所以间接的局部变量也是线程私有的,是线程安全的。 如果改成static的全局变量,就不是线程安全了!
m1:线程安全,因为完全是局部变量完成了操作,不与外界有任何的交集,绝对的线程安全。 m2:线程不安全,因为方法执行的时候,因为传参是一个对象,会有别的线程访问到它。参数不是线程私有的一个对象,所以线程不安全。如下图的调用方式,m2的参数在主线程被使用了。 m3:也不是线程安全的,因为对象被返回了,其他方法也可以拿到引用去修改。实际上是逃离了方法的作用范围,就管不住了,逃逸分析。
Java.lang.stackOverflowError 栈内存溢出产生的原因? 1.栈的大小固定,栈帧的数量过多,超出了栈的内存,导致栈内存溢出。一般递归的时候操作不当会触发栈内存溢出。 2.栈帧过大,也会导致栈内存溢出。 案例1:栈帧过多导致内存溢出 代码没有设置递归终止条件。通过设置jvm参数来更快的触发栈内存溢出的异常。
stackoverflow 异常不只是你的代码会产生,很多第三方的库也会引起这个异常。 案例分析:员工与部门对象,分别持有对方的引用。 运行结果如下: 原因是因为在转化为对象影射的时候,产生无穷的递归调用。 解决方案:加注解,双向关联改成单向。
java进程占用cpu高,是一个很危险的信号,如下图所示: 如何分析呢? top命令只能定位到哪个进程占用的cpu多,并不能定位到线程。使用ps命令,全部命令如下: ps H -eo pid, tid), %cpu 参数解释: H :打印进程的线程数 pid:进程id tid:线程id %cpu:cpu占用 会打印: 这时候打印的会比较多,可以使用grep 命令限制输出的进程。 ps H -eo pid, tid), %cpu | grep 进程号 结果如下,内容被减少了,留下了java进程的线程占用cpu的情况: 使用这个命令之后,再使用jstack打印一下java进程的每个线程的信息。 threadN这种进程是用户线程,系统线程都有规范的名字。刚才用ps命令已经找到了出问题的线程是哪个了。 但是在java中线程编号使用的是16进制,需要换算一下。换算出之后,对比一下nid,就可以定位到出问题的代码了。
看一下问题代码,如下,是因为空循环导致cpu占用高。
迟迟得不到结果,有一种可能是死锁了,先排除这个还是用jstack排查。先找到java进程,然后jstack 进程号。 很简单的死锁,jstack会发现。并且在最后给你输出出来。 看一下代码实现: 拿着竞争资源,然后等待对方的竞争资源,形成了死锁。
本地方法栈,类似于虚拟机栈,是虚拟机在调用本地方法的时候提供的内存空间。 举个例子,什么是本地方法,比如object类中的clone方法。
栈是线程私有的,堆是线程共享的。
堆,是有垃圾回收机制的,但是当对象一直有引用就无法被回收,当对象越来越多,空间不够,也就发生内存溢出了。报错信息如下:OOM异常。 tips:使用Xmx来限制最大堆空间 技巧:排查内存泄漏问题,可以把堆内存设置的小一点,容易早点暴露问题。
jps是可以帮你把java进程过滤出来。 jmap的限制在于只能看到某一时刻的堆内存占用情况,不能时序显示变化。 演示堆内存占用的代码:程序运行过程中增加堆中的对象占用情况,先增加后释放。 调试过程: 1.首先使用jps确认java进程号 2.使用jmap -heap 进程号命令 检测堆内存占用情况。 3.分别在三个时间点,抓取内存快照信息。分别是程序刚启动,申请完了内存,释放了内存三个节点。 第一个时间点:Eden区使用了6m,还没有申请10m的空间。 第二个时间点,申请了10m的空间。内存占用到了16m,就是新创建的数组占用的内存。 第三个节点,释放了byte数组。空间明显缩水了。
还是把刚才的程序跑起来,使用不安全的链接,监控刚才的java进程。 能明显的看到堆内存的变化情况。 jstack的检测死锁的功能,jconsole也是带的。
这种情况,怎么分析呢?使用jvirsualvm。先用他连接上java进程。 使用堆dump功能,可以抓取堆的快照,可以对堆中的详细内容进行分析。 可以查看有哪些对象占用的空间比较大,有哪些对象。使用右侧的检查功能,可以查找出大对象。
可以点击进去查看对象的细节内容。 基本也能定位出问题了。结合业务分析一下,就能降下去了。 再回看一下源代码:
基于jdk1.8,官方给的定义。java方法区是所有的java虚拟机线程共享的区域。它存储了跟类的结构相关的一些信息,比如运行时常量池、成员变量,方法数据,成员方法,构造器方法,包括特殊方法。方法区在虚拟机启动时被创建,逻辑上方法区是堆的组成部分,但是jvm厂商在实现的时候不一定遵守,这个规范并不强制方法区的位置。方法区是规范,1.8之前的永久代(堆上),1.8之后的元空间(基于直接内存)都是具体实现。方法区也会出现内存不足的情况。
jdk 1.8的常量池 就不放在方法区了,而是放在堆里面。
案例代码: 不加jvm参数,在jdk1.8中往内存里放,是看不出来效果的,因为直接内存一般都够。需要加虚拟机参数,设置元空间的大小。 加入限制之后,报错如下: 演示jdk1.8以前的方法区溢出,不同的点在于方法区的实现变成了永久代。 报错如下:
实际场景中,我们动态产生class并加载这些类的场景是非常多的! 各种框架中都用到了字节码技术,比如cglib。 在代理技术中广泛的使用了字节码的生成技术。比如cglib打开之后的,截图如下:
框架会有很多动态加载的过程。
一个最简单的hello world。二进制字节码包括了三部分:类基本信息,常量池,类方法定义。 可以将上述代码的class利用javap -v class文件 进行反编译。验证是否三部分。
首先是,类的基本信息。
然后是常量池:里面都是一些地址+一些符号。 再往下,就是方法定义了。里面有虚拟机的指令了。能看到默认的空构造。 后面的#2,#3,可以去常量池中查找具体指什么。 常量池中,也有互相跳的,就能找到具体执行了什么。 常量池的作用,简单理解为就是给虚拟机指令提供常量符号。