jvm相关

jvm

jvm内存区域

image-20221008190858854

程序计数器:记住下一条jvm指令的执行地址 , 物理上就是一个寄存器

虚拟机栈:线程运行需要的内存空间,在栈中可以将内存区域划分为栈帧,一个栈帧对应一个方法调用时需要的内存空间,每个线程只可以有一个活动栈帧(线程当前正在执行的方法,也可理解为栈顶的栈帧)

  • 垃圾回收不涉及栈内存,方法出栈时,栈帧会自动回收
  • 栈内存溢出:①栈帧过多,比如递归调用没有合理的结束条件 ② 栈帧过大,理论可能。

本地方法栈::java虚拟机调用本地方法时,需要给本地方法提供的内存空间

:存放对象实例的空间,是线程共享的,但是其中仍然有线程私有的区域,称为TLAB,它可以提高对象分配的效率。

方法区:储存一些已经被虚拟机加载的类型信息,常量,静态变量等。

  • jdk1.6时,是永久代(物理上占用堆空间),包含字符串常量池,静态变量,运行时常量池都是在永久代。
  • 1.7之后字符串常量池和静态变量被移动到了堆里,但是运行时常量池还是存在在永久代。
  • 1.8 字符串常量池和静态变量仍然在堆中,其他的都已经被移动到元空间里面。元空间是用本地内存实现的

常量池:可以理解为一张表,虚拟机指令根据这张常量表找到要执行的类名 ,方法名, 参数类型 , 字面量信息等。

运行时常量池:当该类被加载的时候 , 常量池里的信息就是放入运行时常量池 , 并且将里面的符号地址变为真实地址。

StringTable(字符串池)

  • 底层数据结构是HashTable , 每个元素都是key-value结构,采用了数组+单向链表的实现方式。
  • java的字符串采用延迟加载的策略 (就是程序运行到具体某一行的时候再去加载)
  • 所以在类加载时 , 字符串"a"仅仅是当作符号被加载进了运行时常量池中, 还没有成为字符串对象
  • 当程序运行到这一行时, 字符串"a"会从一个符号变成一个字符串对象
  • 然后去StringTable中找有没有相同的字符串对象,如果有的话就返回对应的地址给变量s1
  • 如果没有的话就把“a”放入StringTable中,然后再把地址给变量s1。
  • jdk1.6 StringTable在永久代中
  • jdk1.8 StringTable在堆中

字符串拼接原理

  • 变量拼接原理是StringBuilder
  • 常量拼接原理是编译期优化

intern()实现

jdk1.6: 将这个字符串对象尝试放入串池,如果有则并不会放入, 如果没有会把此对象复制一份放入串池(堆里和串池里都有), 最终是把串池中的对象返回

jdk1.8: 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则把堆中的对象转移放入串池(堆里就没有了), 最终是把串池中的对象返回

一般用来判断两个字符串是否是同一个对象。

对象相关

创建对象

指针碰撞: 假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump ThePointer)。

空闲列表:如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

本地线程分配缓冲区(TLAB):除了分配空间外,还有频繁的指针指向,多线程的情况下,这种情况也是不安全的所以要保证操作的原子性,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local AllocationBuffer,TLAB)哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

如何分配是由垃圾收集器的种类来决定的

对象内容

对象头

整体分为两个部分,Mark Word:对象自身运行时的数据和Klass Word:类型指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

如果是数组对象的话,还要多出一个32bit的数组长度。

image-20221009092830890

Mark Word:

image-20221009093356958

实例数据

是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

对齐填充:这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

定位对象

句柄池(引用传递,java没有)

img

直接指向:速度快,只需要一次定位

img

判断对象死亡

引用计数法:当一个对象被一个变量引用就加1 , 当计数器为0时,就可回收了。弊端:无法解决循环引用的问题。

可达性分析算法:首先**根对象(root)**是肯定不可以被垃圾回收的对象,我们沿着根节点的引用链往下扫描,如果某个对象没有被任何的引用链相连接,那么这个对象就可以被回收。

不可达之后也不是非死不可。当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

引用级别

  • 强引用: GC Root直接引用对象: 只有所有 GC Roots 对象都不通过【强引用】直接引用该对象,该对象才能被垃圾回收。

    • 方法区中类静态属性引用的对象
    • 所有被同步锁(synchronized关键字)持有的对象
    • 在方法区中常量引用的对象,譬如字符串常量池(StringTable)里的引用
  • 软引用:在垃圾回收之后,内存仍不足时会再次触发垃圾回收,回收软引用对象。

  • 弱引用:强度比软引用更弱,垃圾回收时,无论内存是否充足,都会回收弱引用对象。

  • 软引用和弱引用本身也是对象 , 当他们指向的对象被回收后,他们需要被放入引用队列中以释放引用自身

  • 虚引用:虚引用: 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

对象分配原则

  • 对于绝大多数对象,new出来之后是分配在eden区域的,当eden没有足够的空间时,虚拟机就会发起一次Minor GC(只回收新生代的)

  • 如果是一些大对象,比如很长的字符串,元素很多的数组等大对象就会直接被已分配到老年代。

  • 我们知道java对象头中有一个对象年龄计数器,如果该对象经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且把它的年龄设置为1岁,随后只要它可以熬过一次Minor GC,就把它的年龄加一,当增大到15时(默认的,对象头4bit存储),就把该对象晋升到老年代中。

  • 还有一种情况就是eden空间不足了,然后触发了Minor GC,回收之后eden空间依然不足,那么此时一些年龄相对大的对象就会提前进入老年代。

内存溢出(oom):是指程序在申请内存时,没有足够的内存空间供其使用。

内存泄露:是指程序在申请内存后,无法释放已申请的内存空间。长期的内存泄露最终会导致内存溢出。

垃圾回收算法

标记清除算法

标记: 找到没有被GC root直接或间接引用的 ,然后标记这些对象为可回收对象。

清除:将其起始地址放入空闲列表

优点:回收速度快 缺点:产生内存碎片

标记整理算法(老年代,减少内存碎片,避免内存溢出)

标记过程和上述一致,整理的过程是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

标记清除属于非移动回收算法,整理属于移动回收算法,虽然不会产生内存碎片,但是整理时的资源开销很大。

复制算法(新生代)

新生代中有两块大小相同的survivor区域,分别叫from区和to区

扫描后将from里不是垃圾的对象复制到to里并整理,然后将from清空

最后交换from和to的位置,保证to永远为空

优点:没有内存碎片,缺点:需要占用双倍空间

分代收集理论

浅显的说用完就可以丢弃的对象放在新生代当中,而那些长时间存活的对象可以放在老年代。不同的代执行不同的垃圾回收策略。

垃圾收集分类

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

垃圾收集器

image-20221009185458295

Serial垃圾收集器:单线程工作的收集器,新生代和老年代都有,老年代叫SerialOld。多核cpu运行到安全点停下,垃圾回收线程运行,其他线程被阻塞,垃圾回收成功之后,所有线程再继续运行。会stop the world。但是开销很小。

Parallel收集器:也是分为新生代和老年代的不同收集器,是一个关注吞吐量的垃圾收集器,吞吐量= 运行用户代码时间 / JVM总运行时间。

CMS垃圾收集器老年代垃圾收集器,基于标记-清除,它的目标是为了尽可能缩短单次stw的时间。

image-20221009161825293

cms运行的四个阶段:

  • 初始标记:标记一下GC Roots能直接关联到的对象,速度很快,但需要stw
  • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图,这个过程耗时长但是不需要stw,用户线程可以与垃圾收集线程并发运行。
  • 重新标记:在并发标记时,用户线程在工作,会影响一些垃圾对象的地址,所以此时需要重新标记,需要stw,时间稍长
  • 并发清除:清理掉标记阶段被判定为死亡的对象,不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

cms的缺点:

  • 并发清理的时候会产生浮动垃圾,如果这些垃圾超过了cms设定的预留空间,那么就会退化成serial old垃圾回收器。
  • 而且因为是基于标记清除算法的,所以会产生很多的内存碎片。

Garbage First收集器( 高并发低停顿 )

  • 把java堆分为大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

  • 整体用的标记-整理算法,每个region间用的标记-清除算法。

  • 每个Region都维护有自己的记忆集,来解决跨代引用。(卡表)

  • 还有一个概念叫Humongous Regions,如果一个对象超过了Region大小的50%,我们就称它为巨型对象,这样的对象会占一个或者多个Region,这个连续空间就叫H–R。巨型对象会被看成是直接在老年代分配的。

运行的四个阶段:

  1. 初始标记:仅仅标记GC Roots能直接关联的对象,然后修改TAMS指针,这个是为了在下一个的并发标记阶段,用户线程可以在可用的Region中分配新对象。这个阶段需要stw,不过耗时很短。
  2. 并发标记:从GC root对对象进行可达性分析,找出要回收的对象,耗时长,可与用户线程并发执行。扫描完之后,还需要重新处理一下在此时有引用变动的对象。
  3. 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
  4. 筛选回收:首先根据各个区的回收价值和成本排序,然后根据期望的停顿时间制定回收计划。一般是把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。因为这里涉及到存活对象的移动,是需要stw的。

类加载

类加载就是把字节码文件,也就是.class文件加载到内存

以下情况触发类加载

  • new 一个实例
  • 访问static变量方法
  • new 子类会加载父类
  • class.forname(反射)
  • 虚拟机启动时,定义main方法的类会首先触发类加载

类加载的过程

image-20221009193007194

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。(序列化)
  2. 把这个字节流文件存储到方法区中,称为instanceKlass,也叫元数据。
  3. 加载的同时会在java堆内存生成一个java_mirror对象(类.class),镜像对象的作用是作为方法区类信息的访问入口。方法区的元数据和mirror对象互相持有访问地址。

验证

验证类(字节码)是否符合 JVM规范,安全性检查 , 有效阻止不合法的类被继续运行。比如文件格式验证、元数据验证、字节码验证、符号引用验证等

准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾(在方法区),从 JDK 7 开始,存储于 _java_mirror 末尾(在堆中)
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

解析

将常量池中的符号引用解析为直接引用

  • 符号引用:仅仅是一串符号,并不知道该引用对应的内存地址;不知道对应的是方法还是对象
  • 直接引用:此时该引用已经有对应的内存地址了

初始化

初始化就是类加载的最后一个阶段了。准备阶段变量可能已经被赋过初值了,初始化的时候会根据程序代码的要求来对类里的一些变量和数据进行操作。也可以说就是在调用**<clinit()>**方法

会触发初始化的场景:

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会优先引发父类的初始化
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会触发初始化的场景

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

    • 访问这些数据是在类链接的准备阶段就完成了
  • 访问类对象.class 不会触发初始化(加载阶段完成)

  • 创建该类的数组不会触发初始化

  • 类加载器的 loadclass 方法

  • Class.forName 的参数2设置为 false 时

类加载器

通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

image-20221009195044414

双亲委派机制

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

一是可以避免类的重复加载,二就是安全性问题,一些核心API中定义类型不会被随意替换掉。

破坏双亲委派

如何打破双亲委派机制:重写 loadclass 方法

jdbc破坏双亲委派

原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。

tomcat打破双亲委派机制

  • 一个 web 容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  • 部署在同一个 web 容器中相同的类库相同的版本可以共享。否则,如果服务器有 5 个应用程序,那么要有 5 份相同的类库加载进虚拟机,这肯定不行的。
  • web 容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  • web 容器要支持 jsp 的修改,jsp 文件最终也是要编译成 class 文件才能在虚拟机中运行,但程序运行后修改 jsp 常见的事情, web 容器需要支持 jsp 修改后不用重启。

调优命令工具

jps:与linux上的ps类似,用于查看有权访问的虚拟机的进程,可以查看本地运行着几个java程序,并显示他们的进程号。当未指定hostid时,默认查看本机jvm进程。

jinfo:可以输出并修改运行时的java 进程的一些参数。

jstat:可以用来监视jvm内存内的各种堆和非堆的大小及其内存使用量。

jstack:堆栈跟踪工具,一般用于查看某个进程包含线程的情况。

**jmap:**打印出某个java进程(使用pid)内存内的所有对象的情况。一般用于查看内存占用情况。

jconsole:一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视

JVM参数

-Xms:JVM启动时申请的初始Heap值,默认为操作系统物理内存的1/64但小于1G。默认当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过-XX:MaxHeapFreeRation=来指定这个比列。Server端JVM最好将-Xms和-Xmx设为相同值,避免每次垃圾回收完成后JVM重新分配内存;开发测试机JVM可以保留默认值。(例如:-Xms4g)

-Xmx:JVM可申请的最大Heap值,默认值为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列。最佳设值应该视物理内存大小及计算机内其他内存开销而定。(例如:-Xmx4g)

-Xmn:Java Heap Young区大小。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小(相对于HotSpot 类型的虚拟机来说)。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。(例如:-Xmn2g)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值