JVM原理与深度调优,Java互联网架构师系统vip

JVM中的ClassLoader

================

JVM本身包含了一个ClassLoader称为Bootstrap ClassLoader,和JVM一样,BootstrapClassLoader是用本地代码实现的,它负责加载核心JavaClass(即所有java.*开头的类)。

另外JVM还会提供两个ClassLoader,它们都是用Java语言编写的,由BootstrapClassLoader加载;其中Extension ClassLoader负责加载扩展的Javaclass(例如所有javax.*开头的类和存放在JRE的ext目录下的类)ApplicationClassLoader负责加载应用程序自身的类。

当运行一个程序的时候,JVM启动,运行bootstrapclassloader,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。

第一个Class文件、通过javac编译成字节码、字节码之后有个ClassLoader叫类加载器,因为java.class文件到JVM内部运行起来需要有个装载过程、从物理的文件到内存的结构、比如加载、连接、初始化。

linux应用程序有个进程地址空间,对进程地址空间的解释:

linux采用虚拟内存管理技术,每一个进程都有一个3G大小的独立的进程地址空间,这个地址空间就是用户空间。每个进程的用户空间都是完全独立、互不相干的。进程访问内核空间的方式:系统调用和中断。

创建进程等进程相关操作都需要分配内存给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址。

实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际页框的程序。该异常是虚拟内存机制赖以存在的基本保证,它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在的映射到了物理地址上。

Linux操作系统采用虚拟内存技术,所有进程之间以虚拟方式共享内存。进程地址空间由每个进程中的线性地址区组成,而且更为重要的特点是内核允许进程使用该空间中的地址。通常情况况下,每个进程都有唯一的地址空间,而且进程地址空间之间彼此互不相干。但是进程之间也可以选择共享地址空间,这样的进程就叫做线程。

基本上所有linux应用程序都会遵循这个规泛、有栈、有堆、对于JVM来说、也是遵循这个规则、只不过在这个规则上做了一些改进

通过类加载器把Class文件装载进内存空间、装进来以后只是你的字节码,然后你需要去运行、怎么去运行呢 ?图中类加载器子系统下面都是运行区

内存空间里有:

1.方法区:被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。

2.Heap(堆):一个Java虚拟实例中只存在一个堆空间。

3.JavaStack(java的栈):虚拟机只会直接对栈执行两种操作:以帧为单位的压栈或出栈,java栈有个核心的数据、先进后出

4.Nativemethodstack(本地方法栈):通过字面意思、基本是调用系统本地的一些方法、一般在底层封装好了、直接调用

5.地址、在这里边是一个指针的概念、比如从变量到对象怎么做引用、就是地址

6.计数器:主要做字节码解析的时候要记住它的位置、可以理解为一个标记

7.执行引擎:数据、字节码做一些业务处理、最终达到想要的结果

8.本地方法接口:基本是底层系统、比如IO网络、调用操作系统本身

9.本地方法库:为了兼容、实现跨平台有不同的库 、兼容平台性

额外数据信息指的是本地方法接口和本地方法库

JMM

===

java的内存模型

大家可能听过一个词、叫线程安全、在写高并发的时候就会有线程安全问题、java里边为什么会出现线程安全问题呢、因为有JMM的存在、它会把内存分为两个区域(一个主内存、一个是工作内存)工作内存是每个java栈所私有的

因为要运行速度快、需要把主内存的数据放到本地内存中、然后进行计算、计算完以后再把数据回显回去

JVM原理与深度调优

JMM有两个区域、主内存和栈内存、

java线程可能不止一个、可能有多个栈、现在需要三个线程同时做个运算、主内存初始值x=0 需要把x=0都要装载在自己的内存里边去、相当于有一个

副本、现在初始值和三个栈都是x=0

现在需要做运算

x=x+1

x=x-1

x=0

我们的期望值是x=0,如果是单个线程跑没问题 、取回x=0、运算x=+1、回显进来主内存就是1 、栈1是1,运算x=-1、回显进来主内存就是0、栈1是0

如果多个线程同时执行、结果是不可预期的、正因为有这种结构的存在、当执行x=+1、栈1是x=1 、栈2来不及执行、栈1就已经把x=1写到主内存了 、栈2跟栈3拿过去之后初始值就不是0、可能就是1了 、这样程序就写乱了

所以在java中就出现了很多锁、来确保线程安全

运行时数据区

=======

PC寄存器----线程私有

=============

PC寄存器也叫程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。

每一条JVM线程都有自己的PC寄存器

在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)

如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址

如果该方法是native,那PC寄存器的值是undefined。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈 ----线程私有

=================

与PC寄存器一样,java虚拟机栈(Java Virtual Machine Stack)也是线程私有的。每一个JVM线程都有自己的java虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

JVM stack 可以被实现成固定大小,也可以根据计算动态扩展。

如果采用固定大小的JVM stack设计,那么每一条线程的JVM Stack容量应该在线程创建时独立地选定。JVM实现应该提供调节JVM Stack初始容量的手段。

如果采用动态扩展和收缩的JVM Stack方式,应该提供调节最大、最小容量的手段。

JVM Stack 异常情况:

StackOverflowError:当线程请求分配的栈容量超过JVM允许的最大容量时抛出

OutOfMemoryError:如果JVM Stack可以动态扩展,但是在尝试扩展时无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈时抛出。

本地方法栈----线程私有

==============

Java虚拟机可能会使用到传统的栈来支持native方法(使用Java语言以外的其它语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack)

如果JVM不支持native方法,也不依赖与传统方法栈的话,可以无需支持本地方法栈。

如果支持本地方法栈,则这个栈一般会在线程创建的时候按线程分配。

异常情况:

StackOverflowError:如果线程请求分配的栈容量超过本地方法栈允许的最大容量时抛出

OutOfMemoryError:如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

Jave堆----线程公用

=============

平时所说的java调优就是它

在JVM中,堆(heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。

Java堆载虚拟机启动的时候就被创建,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显示地被销毁。

Java堆的容量可以是固定大小,也可以随着需求动态扩展,并在不需要过多空间时自动收缩。

Java堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。

JVM实现应当提供给程序员调节Java 堆初始容量的手段,对于可动态扩展和收缩的堆来说,则应当提供调节其最大和最小容量的手段。

Java 堆异常:

OutOfMemoryError:如果实际所需的堆超过了自动内存管理系统能提供的最大容量时抛出。

方法区----线程公用

===========

方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

方法区在虚拟机启动的时候创建。

方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。

方法区在实际内存空间中可以是不连续的。

Java虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。

Java 方法区异常:

OutOfMemoryError: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常。

JVM内存分配

=======

内存分配其实真正来讲是有三种的、但对于JVM来说只有两种

  • 栈内存分配:

大家在调优的过程中会发现有个参数是-Xss 默认是1m,这个内存是栈内存分配, 在工作中会发现栈OutOfMemory Error内存溢出、就是因为它的内存空间不够了 一般情况下没有那么大的栈、除非你的一个方法里边有几十万行代码、一直往那压、不出,所以导致栈的溢出、栈的内存分配直接决定了你的线程数 、比如说你默认情况下是1m 、系统一共给你512m、那最高可以分配512个线程,再多系统分配不了啦、因为没有那么多的内存 、像tomcat、resin、jboss等、有个最大线程数、要根据这个来调、调个100万没有意义、分配不了那么大、调太少整个性能发挥不出来 ,调这个 、跟你的cpu有关系、需要找一个折中位置 、根据应用 、是IO密集型的还是CPU密集型的来调-Xss的值、它这里边主要保存了一些参数 、还有局部变量 、就比如说写代码、有开始有结束、这里边肯定定义了很多变量、比如:int x=1 y=0 只要在这方法内的都属于局部变量 、因为你要做运算、要把这东西存住、只有等程序结束的时候才能销毁,对于这种参数是不会产生线程安全问题、因为线程是私有的

  • 堆内存分配:

Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢

JVM原理与深度调优

jvm堆结构

======

JVM原理与深度调优

(图一)

1.Young(年轻代)

年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Old。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。

2.Old(年老代)

年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

3.Permanent:(持久代)

也叫方法区、用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。

通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是值指MethodArea,不属于Heap。

java堆结构和垃圾回收

============

JVM原理与深度调优

图(二)

Direct Momery 严格意义来说也算堆,它是一块物理内存、可以分为操作系统内存、是比较快的、不会走JVM 在java里边实现了内存映射、这样速度更快

CodeCache 放一些字节码、类的信息会放在里边

Permanent Generation space 方法区、严格意义来说也属于堆

Eden Space 区

Survivor Space区

Tenured Generation Old区(年老代)

JVM GC 管理

调优大部分调优的是怎么回收,Minor GC 回收Eden Space和 Survivor Space , Full GC回收所有区域

不管什么GC,回收过程中会出现暂停、回收过程中用户线程是不会工作的、这样就造成程序卡了 这是无法改变不了的事实、避免不了、不过可以优化暂停时间的长短

原则上不能出现Full GC 、所有区域都要跑一遍 、出现Full GC 应用就不可用

Jvm 堆配置参数

=========

1、-Xms初始堆大小

默认物理内存的64/1(<1GB),建议小于1G、可根据应用业务调节

2、-Xmx最大堆大小

默认物理内存的4/1(<1GB)、建议小于1G、实际中建议不大于4GB(否则会出现很多问题)

3、一般建议设置 -Xms= -Xmx

好处是避免每次在gc后、调整堆的大小、减少系统内存分配开销

4、整个堆大小=年轻代大小+年老代大小+持久代大小(Permanent Generation space区、也会被Full GC回收)

jvm新生代(young generation

========================

JVM原理与深度调优

图(三)

1、新生代=1个eden区和2个Survivor区

2、-Xmn 年轻代大小

设置年轻代大小、比如-Xmn=100m那么新生代就是100m,然后共享

3、-XX:NewRatio

年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

4、-XX:SurvivorRatio

Eden区与Survivor区的大小比值,设置为8(默认是8) ,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

比如新生代=100m,设置-XX:SurvivorRatio为8,那E =80m S0 =10m S1=10m(1/10)

5、用来存放JVM刚分配的Java对象

java老年代(tenured generation)

============================

JVM原理与深度调优

图(四)

1、老年代=整个堆-年轻代大小-持久代大小

年轻代就是上面讲的-xmn配置的参数、持久代参数默认是0

2、年轻代中经过垃圾回收没有回收掉的对象被复制到年老代。

就是这个对象收集完一次、发现被引用了、某个地方使用了、回收不掉才放进去,一般是多次回收、从E区回收过程中、先进S0或者S1、S0或者S1再回收一次、回收不掉再放到年老区

3、老年代存储对象比年轻代年龄大的多,而且不乏大对象。

对互联网企业来说、最常用的是"缓存"的对象比较多、缓存一般会用弱引用、但弱引用也不会轻易被回收的、除非是在整个堆的内存不够的情况下、防止你的内存宕机、强引用是和垃圾回收机制相关的。一般的,如果一个对象可以通过一系列的强引用引用到,那么就 说明它是不会被垃圾回收机制(Garbage Collection)回收的,

刚才说了缓存对象一般是弱引用、有些数据丢了是没关系的、只是提高你的系统性能才放到缓存里边去、但是如果有一天内存不够了 、缓存占了很大一部分对象、你不回收的话、你整个系统都不可用了、整个服务都不能用了、如果回收掉、我可以从数据库去取、可 能速 度慢点、但是我的服务可用性不会降低

比如说刚开始分配的对象 、这个对象暂定是OLD区、刚开始一部分内存区域被缓存占据了、一般情况下对于一个缓存的设计都有初始值、对于java来说、比较通用的缓存是可以自动伸缩的、

如图(四)整个OLD区50M有45M是被缓存占据了、不会被回收掉、那整个OLD区只有5M可以用了 、假如E区有40M 、S0 分配10M 、S1分配也是10M 、理想情况下、经过E区到S0、S1到老年代的大小不到1M、 那5M就够了、不会出现FULL GC 、也不会出现 内存溢出、一旦你的对象大于5M、比如10M的数据、 放不进去了、就会出现FULL gc 、FULL gc会把整个缓存全都收掉、瞬间缓存数据就没了、然后把10M的数据放进去、这就是弱引用、可以理解为这是一种服务降级、如果是强引用那就直接挂了

4、新建的对象也有可能直接进入老年代

4.1、大对象,可通过启动参数设置

-XX:PretenureSizeThreshold=1024(单位为字节,默认为0、也就是说所有的默认都在新生代)来代表超过多大时就不再新生代分配,而是直接在老年代分配

4.2、大的数组对象,切数组中无引用外部对象。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

img
img

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V:vip1024b 备注Java获取(资料价值较高,非无偿)
img

总结

我们总是喜欢瞻仰大厂的大神们,但实际上大神也不过凡人,与菜鸟程序员相比,也就多花了几分心思,如果你再不努力,差距也只会越来越大。实际上,作为程序员,丰富自己的知识储备,提升自己的知识深度和广度是很有必要的。

Mybatis源码解析

开发知识点,真正体系化!**

[外链图片转存中…(img-xbAdCL9L-1711567771805)]
[外链图片转存中…(img-AJ5oO7ip-1711567771806)]

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V:vip1024b 备注Java获取(资料价值较高,非无偿)
[外链图片转存中…(img-WU2t63tm-1711567771806)]

总结

我们总是喜欢瞻仰大厂的大神们,但实际上大神也不过凡人,与菜鸟程序员相比,也就多花了几分心思,如果你再不努力,差距也只会越来越大。实际上,作为程序员,丰富自己的知识储备,提升自己的知识深度和广度是很有必要的。

Mybatis源码解析

[外链图片转存中…(img-as8SH4vf-1711567771807)]

[外链图片转存中…(img-kNgXnnR5-1711567771807)]

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值