JVM(一)

2021-08-06 14:59:15 浏览数 (1)

java内存区域

代码语言:javascript复制
public class kafka {
public static void main(String[] args) {
        ReplicaManager replicaManager = new ReplicaManager();
        replicaManager.loadReplicasFromDisk();
    }
}
public class ReplicaManager {
public void loadReplicasFromDisk() {
        Boolean hasFinishedLoad = false;
if (isLoacalDataCorrupt()) {
//....
        }
    }
public Boolean isLoacalDataCorrupt() {
        Boolean isCorrupt = false;
return isCorrupt;
    }
}

首先,你的JVM进程启动,首先会加载你的kafka类到内存中,然后有一个main线程,开始执行你的kafka中main方法

main线程是关联了一个程序计数器,那么他执行到哪一行行指令,就会记录到哪里

其次,就是main 线程在执行main方法的时候,会在main线程关联的java虚拟机栈里,压入一个main方法的帧栈,接着发现需要创建ReplicaManager类的实例对象,此时就会加载ReplicaManager类加载到内存中

然后一个Replication的对象实例分配到java堆内存,然后main方法的帧栈里的局部变量引入一个replication变量,让他引用ReplicaManager对象在java堆内存中的地址,

接着,main线程开始执行RelicaManager对象的方法,会一次执行到方法对应的帧栈中,执行完方法之后,再把方法对应的帧栈从java虚拟机栈里出栈。

双亲委派机制

扩展类加载器

Bootstrap ClassLoader,主要负责加载在安装的java目录下的核心类,在JDK安装目录下有一个lib目录,这个就是java的一些核心思路,支撑java系统的运行,

扩展类加载器

Extension ClassLoader,这个类加载器其实也是类似的,就是加载JDK安装目录下的libext目录的一些类。

应用程序类加载器

这个就是负责加载classPath环境变量所指定路径的类,就是你写的代码,

自定义类加载器

根据你的需求加载你的类

双亲委派机制

JVM的类加载器是有亲子层级结构的,就是启动类加载器是最上层,扩展类加载器第二层,应用程序类加载器,最后一层就是自定义加载器

例如,JVM加载ReplicaManager类,此时用用程序类加载器就会问自己的爸爸,就是扩展类加载器,你能加载整个类吗,然后扩展加载器直接问自己的爸爸,启动类加载器,你能加载整个类吗,启动类加载器就会在自己的加载的目录寻找,发现没有找到,就会告诉自己的儿子,你去加载,然后扩展类加载器,也没有找到,就会告诉自己的儿子,你去加载,应用程序类加载器,就会看时候是自己的负责范围,果然是自己的,然后就会加载整个类到内存中去,这就是所谓的双亲委派模型,先找父亲加载,不行就去由儿子加载,这样的话,可以避免多层级的加载器结构重复加载某些类。

Tomcat这种web容器中类加载器如何设计实现

首先Tomcat类加载体系如下图,他是自定义了许多类加载器

Tomcat自定义了Common,Catalina,Shared等类加载器,其实就是用来加载tomcat自己的一些核心基础类库,然后tomcat每个部署在的web应用都有一个对应的WebApp类加载器,负责加载我们部署的这个Web应用的类,至于Jsp类加载器,则是给每个jsp都准备了一个Jsp类加载器,切记的是Tomcat是打破了双亲委派机制,

每个WebAPP负责加载对应的那个web引用的class文件,也即是我们写好的某个系统打包好的war包中的所有class文件,不会上传给上层类加载器去加载.

tomcat如何打破双亲委派机制

common类加载器,tomcat最基本的类加载器,加载的class可以被tomcat容器本身访问以及各个WebApp访问,实现共有类库,war和tomcat可以通用这个类class

cacalina类加载器,加载的webapp不可见,加载的是tomcat容器私有的类加载器,就是

shared类加载器,各个webapp共享类加载器,对于所有的webapp可见,但是对于Tomcat容器不可见,所有的webapp可以共用加载的类库,如上图的war1和war2使用同一个的mysql5.6的类,这个mysql就是share类加载器加载

webapp类加载器,各个webapp的私有加载器,仅对webapp可见,这个就是为了不同war包可能引用不同的版本,起到隔离的作用,

jsp类加载器,加载的仅仅是这个jsp所编译出来的class文件,当web容器检测到jsp修改了,然后新建一个jsp实例,就会替换旧的jsp实例

分代模型

我们知道我们代码写的对象大部分存活的周期极短,少数对象是长期存活的,JVM使用分代模型,存储不同的对象,将java堆内存划分为年轻代和老年代,

顾名思义,年轻代存放存放存活周期短的对象,老年代存放长期存在的对象

代码语言:javascript复制
public class kafka {
    private static  ReplicaFetcher fetcher = new ReplicaFetcher();  
public static void main(String[] args) {
       loadReplicasFromDisk();        
        while (true){
           fetchReplicasFromRemote();
           Thread.sleep(2000);
       }        
    }    
private static void loadReplicasFromDisk(){
        ReplicaManager replicaManager = new ReplicaManager();
        replicaManager.load();
    }
public static void fetchReplicasFromRemote(){
        fetcher.fetch();
    }
}

例如代码,他们在java堆内存中是如何分布的,如下图

然后一旦loadReplicasFromDisk执行完毕之后,方法的帧栈就会出栈,对应的年轻代里的ReplicaManager对象也会被回收掉,如下图

永久代

JVM里的永久代其实就是我们之前说的方法区,可以理解就是所谓的永久代,你可以认为永久代就是放一些类信息的

方法区会进行垃圾回收吗,满足下面条件就可以回收引用

  1. 首先该类的所有实例对象都已经从java堆内存被回收
  2. 其次加载这个类的classLoader已经被回收
  3. 最后,对该类class对象没有任何应用

JVM内存中如何分配,如何流转

大部分的对象都是优先在新生代分配内存的,正如上面类静态变量fetcher用用的RelicaFetcher对象,会长期存活内存中,其实他刚开始的时候也是存活在新生代,最后经过多次垃圾回收之后,最后转移到老年代

那么什么时候进行垃圾回收

其实并不是java堆内存的对象没有引用,并不一定会发生垃圾回收,实际上垃圾回收也是需要条件的

常见的是当新生代预分配内存空间,几乎被对象占满了,但是还有对象创建,这个时候就要进行垃圾回收,新生代内存空间的垃圾回收称为Minor GC,有时候也叫 Young GC,这个时候就会回收没有任何引用的对象.

长期存活的对象会躲过多次垃圾回收,

比如上面说的ReplicaFetcher实例对象,他长期被静变量引用,所以他在新生代不会别回收,但是我们JVM有一条规定,当成功回收15次之后,还是没有回收,就会进入老年代,因此老年代放的就是年纪大的对象.

JVM核心参数

-Xms

java堆内存大小

-Xmx

java堆内存的最大大小

-Xmn

java堆内存新生代的大小

-X:PermSize

永久代大小

-X:MaxPermSize

永久代最大大小

-Xss

每个线程的栈内存大小

如何设置JVM堆内存

如上图一个支付系统,每天产生100万的订单,当然正常的生产环境不会在一台机器上,假设我们有三个机器,如下部署,每台机器每秒大概有30个请求

对于上面我们看看我们如何预估如何设置JVM堆内存大小

  1. 每条订单大约多少内存 我们的interger是4个字节,Long是8个字节等等,按此计算,假设我们的一个订单对象大概是500字节,不到1kb
  2. 实际估算要加10-20倍 由于我们创建订单不仅仅是订单对象,可能还有其他对象,比如我们的而此时的订单,30*500=1500,15kb,加上其他对象,每秒产生几百kb到1Mb
  3. 触发Minor GC 我就认为每秒产生1M,当过一段时间产生了几十万对象,此时占据了几百M内存,此时可能新生代快满了
  4. 此时如何设置JVM堆内存 假设我们机器是2核4G,机器本身就要占用内存空间,最后留给JVM的最多有2G,此时JVM还有方法区,栈内存,堆内存,留给堆内存可能只有1G,但是堆内存还分为新生代和老年代,留给新生代的空间假设只有500M,此时我们的系统每秒1M,不到500秒,新生代就满了,就会发生GC 这样频繁GC,是一种不好的现象,再比如我们用4核8G,再次计算半个小时到一个小时,才会发生一次Minor GC,其实我们也可以再多部署几台机器,比如5台机器,平均每天每秒也就20订单对象,这样对每台机器的请求越少,JVM压力越小

按照上面步骤合理的预估你们生产环境,将事半功倍。

如何堆内存设置不合适会出现什么情况

比如我们有一台2核4G机器,分给堆内存是1G,但是对于新生代就是500MB,正常情况下没有问题,但是在大促销的时候,我们就可以发现问题

假设每秒产生100个对象,每个对象500字节,就有50kb,此时我们要在此基础上乘以20倍,此时就是大约1M

而每秒产生1M,几百秒之后,新生代就会慢,此时就会发生Minor GC,频繁的发生GC,就会导致系统卡顿,体验不好

极端情况下,每秒1000个对象,最终系统内存每秒有10MB,甚至几十MB,同时系统的CPU,资源性能急剧下降,就会导致请求变慢,最后在Minor GC之后,但是还会有几十MB没有被回收,慢慢的就会导致进入老年代

如果进入老年代,那就会更加糟糕,因为老年代的GC,是非常慢的,老年代频繁的GC,最后有可能造成内存溢出,极大的影响性能

0 人点赞