jvm相关

1:内存结构

JVM的内存主要包括直接内存和虚拟机运行时的数据区

直接内存:

直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制

配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常

直接内存(堆外内存)与堆内存比较

直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显

直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

运行时的数据区(就是经常问的和背的):

内存区域与jvm调优和异常的定位处理是相关联的。即抛出一个常见异常,让你分析原因并提供解决方案。

操作数栈和局部变量表的关系(c=a+b)

  在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。

内存泄漏(memory leak):程序在申请内存后,无法释放已申请的空间,自己无法使用,且也不可以被系统回收进行再分配,首先这些对象是可达的,其次这些对象是无用的。一次泄漏影响不大,但是累积过后就有可能造成内存溢出。

内存泄漏的常见例子:

变量不合理的作用域。一般而言,如果一个变量定义的作用域大于其使用范围,很有可能会造成内存泄漏, 另一方面如果没有及时地把对象设置为 Null,很有可能会导致内存泄漏的发生。

这里的object实例,其实我们期望它只作用于method1()方法中,且其他地方不会再用到它,但是,当method1()方法执行完成后,object对象所分配的内存不会马上被认为是可以被释放的对象,只有在Simple类创建的对象被释放后才会被释放,严格的说,这就是一种内存泄露。解决方法就是将object作为method1()方法中的局部变量。当然,如果一定要这么写,可以改为这样:

静态集合类:例如 HashMap 和 Vector。如果这些容器为静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。

各种连接: 比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,以及使用其他框架的时候,除非其显式的调用了其close()方法(或类似方法)将其连接关闭,否则是不会自动被GC回收的。其实原因依然是长生命周期对象持有短生命周期对象的引用,导致无用的短生命周期对象也无法被回收。

 SessionFactory就是一个长生命周期的对象,而session相对是个短生命周期的对象,但是框架这么设计是合理的:它并不清楚我们要使用session到多久,于是只能提供一个方法让我们自己决定何时不再使用(要是不显示close可能造成内存泄漏,推荐关于close的方法写在finally中)。

单例模式导致的内存泄露:很多时候我们可以把它的生命周期与整个程序的生命周期看做差不多的,所以是一个长生命周期的对象。如果这个对象持有其他对象的引用,也很容易发生内存泄露。

内存溢出(oom):程序在申请内存的时候没有足够的内存供申请者使用。

OOM常见原因:

分配的内存空间较少

申请的太多,但是却不释放,造成内存的不断泄漏

资源耗尽,不断的创建线程和不断的发起网络连接

OOM常规解决:

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

重点排查以下几点:

1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

2.检查代码中是否有死循环或递归调用。

3.检查是否有大循环重复产生新对象实体。

4.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

第四步,使用内存查看工具动态查看内存使用情况


2:类加载

  在Java中,一个对象在可以被使用之前必须要被正确地实例化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。

虚拟机把描述类的数据从 Class 文件(二进制字节流)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

具体的加载流程

编译:将java的源文件编译成字节码文件

加载: 通过类加载器将字节码文件载入到内存,同时在方法区创建Class对象

验证:这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:在方法区中为类的静态变量分配内存并将其初始化为0。

解析:将常量池中的符号引用替换为直接引用,但是替换的时机可以是在类被加载的时候就替换,也可以在一个符号引用被使用的时候才去解析

初始化:执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的

有且仅有种情况会触发初始化

PutStatic,getStatic和invokeStatic,new时

子类初始化时,父类尚未初始化,触发父类初始化。一个类在初始化的时候要求其父类全部都已经被初始化,但是一个接口在初始化的时候,并不要求其父接口全部都完成初始化,只有在真正用到父接口的时候才会初始化。

指定的main

反射调用

注意:分配数组,调用static final修饰的变量不会触发初始化,子类调用父类的静态字段只会触发父类初始化,子类不会进行初始化的。

经过类加载之后开始进行实例化,具体包含以下步骤

首先分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间等同于把一块确定大小的内存从 Java 堆中划分出来,在新生代区使用的是”指针碰撞”的方式,在老年代采用的是”空闲列表”的方式。在多线程的情况下,有两种方式来分配内存。一种是对分配内存空间的动作进行同步处理,jvm 采用 CAS 配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一下块内存,称为本地线程分配缓冲(TLAB)。每次分配内存只在当前线程的缓冲区中通过指针碰撞来分配内存。只有当 TLAB 用完需要重新分配 TLAB 时才需要使用 CAS 同步锁定。

其次将分配到的内存空间的实例变量赋予默认值0(类似于准备阶段为静态变量赋予默认值一样),

然后对象进行必要的设置,例如对象属于哪个类,对象的哈希码、对象的 GC 分代年龄等信息,这些信息存放在对象头中。

最后执行<init>方法,按照程序员的意愿进行赋值(声明实例变量的赋值操作,实例代码块的复制操作,构造函数的赋值操作)。

使用:

卸载:对方法区的无用类进行回收有三个前提:第一该类的类加载器已经卸载,第二该类不存在任何实例对象,第三该类的Class不存在任何引用,即无法通过反射的方式获取该类的任一方法。


3:GC发生在什么时候?对谁GC?具体干什么?

第一问:

GC是由系统自身决定的,程序员不可指定。但是发生GC一定是在安全点或者是安全区里。我们可以通过进行优化对GC进行调整。首先GC分为2种

Minor GC:对象优先分配在新生代的eden区,当eden分配不下时会发生Minor GC。可以调整-Xmn指定新生代的大小,-Xms和-Xmx指定堆的大小(这两个参数一般设置一样,避免发生内存抖动),进而调整了新生代和老年代的比例(默认1:2);可以调整-XX:survivorRadio指定新生代中eden和一个survivor的比例(默认8:1)

Full GC:老生代的空间不足以分配对象时发生Full GC。有以下四种情况会使对象转移到老生代

1:分配担保机制:当发生Minor GC时,采用复制算法将存活对象复制到其中一个空闲的survivor,若此时survivor不够存放,就会通过内存担保机制将存活对象复制到老生代中。若此时的老生代空间不足,则看参数-XX:HandlePromotionFailure是否允许担保失败,当允许的时候查看老生代的大小是否大于历次进入老年代对象的平均大小,若大于冒着风险进行一次Minor GC(风险在于平均值不能代表当前值)。若小于或者不允许担保失败,则直接进行Full GC。

2:大对象直接进入老年代。这样可以避免在新生代发生大量的复制。可以通过调节参数-XX:PretenureSizeThreshold,指定超过此范围大小的对象直接在老年代分配。默认0,即无论大小都优先在新生代的eden分配。

3:长期存活的对象进入老年代:每一次Minor GC存活下的对象年龄加1,超过一定年限就进入老年代。可以通过调节参数-XX:MaxTenuringThreshold指定进入老年代的年龄界限(默认是15)

4:动态对象年龄判断:当新生代存活相同年龄的对象占用的空间超过survivor的一半时,将该年龄及以上的对象复制到老生代。

OOM:当老生代Full GC后空间仍然不满足,抛出OOM异常。

第二问:主要是考对象存活的判定

对象存活判定:

引用计数算法:当一个对象被引用则加1,引用失效则减1.引用次数为0说明该对象不再被使用。实现较为简单,但是无法解决对象之间的相互循环引用问题。

可达性分析算法:当一个对象到GC Roots不可达,即不存在任何一条引用链使对象与GC Roots相连。认为该对象不再使用。作为GC Roots的包含:虚拟机栈引用的对象,方法区常量引用对象,方法区类静态属性引用对象,Native引用的对象。

对象死亡的两次判定:

当可达性分析后认为其不可达,该对象会进行第一次标记和筛选。筛选的条件是该对象是否有必要执行finalize()方法。若已经调用过finalize()或者没有重写finalize()方法,则认为没有必要,直接就会被回收。

当第一次筛选通过,则会将该对象置于F-Queue队列。重写的finalize()方法该对象有且仅有一次逃脱死亡机会,就是将自己重新和GC Roots相关联。若没有关联,进行二次标记,等待执行相应的回收算法进行回收。

第三问:采用分代收集算法进行回收,列出常见收集器的对比

GC算法:

标记-清除:通过对象存活判定的流程,系统就可以标记出需要回收的对象和不需要回收的对象。然后将需要回收的对象所占存储空间直接进行清除。这样的缺点是回收的内存不连续,产生大量碎片,以至于下次为较大对象分配内存时空间不足。另外就是标记清除的效率不高。

复制:堆中的新生代采用的回收算法。堆中老生代和新生代的比例为2:1,新生代中Eden:Survivor:Survivor=8:1:1。每次使用Eden和一个Survivor,当回收时将这两个区域存活的对象复制到另外一个空闲的Survivor,然后将这两个区域清空。若出现剩下的一个Survivor存储不了两个区域的存活对象时,将由老年代进行分配担保,将存活对象直接存储到老年代。缺点是若存活对象较多则效率降低,因为不仅需要老年代进行担保。且有一个Survivor一直处于空闲状态。

标记-整理:堆中老生代采用的回收算法。和标记清除类似,只是不是将不用对象直接清理,而是将存活对象向一端移动,然后删除端边界以外的对象。

分代收集:堆中的新生代采用复制回收算法。堆中老生代采用标记整理或者标记清除回收算法。

常见收集器:

CMS:是追求最短回收停顿时间为目标的收集器,是基于标记-清除算法实现。具体包括初始标记(仅仅标记出与GC Roots直接关联的对象),并发标记(进行可达性分析,找出存活对象),重复标记(主要是用于修正在并发标记阶段因为用户线程的执行导致标记产生变动的对象),标记清除。最耗时的是并发标记和标记清除阶段,但是这两个阶段可以和用户线程同时执行,因此停顿时间较短。但是存在三处不足:

1:虽然耗时的并发标记和并发清理可以和用户线程并行执行,不会造成用户的停顿,但是却因为占用一定的cpu资源导致应用程序变慢,总吞吐量降低。解决的办法是引入了增量式并发收集器,利用抢占式的方式让GC线程和用户线程抢占交替执行,避免GC线程独占cpu资源。但是这样延长了整个GC的时间,所以不理想。

2:无法及时处理浮动垃圾:因为用户线程和GC线程是同是执行的,所以在重复标记结束后执行标记清理的过程,用户又产生了垃圾,这些在清理过程中产生的浮动垃圾,本次GC线程不会进行回收,只能等到下一次。同时由于GC和用户线程同时执行,在标记清理的过程中还需要预留足够的内存空间给用户执行,因此CMS不能等到老年代快满的时候进行GC。可以调节参数-XX:CMSInitiatingOccupancyFraction来设置触发老年代进行GC的阈值,即当老年代的已用空间达到CMSInitiatingOccupancyFraction的时候就进行一次Full GC。

3:因采用的是标记-清除算法所以使老年代容易产生内存碎片,当为大对象分配存储空间时,往往会出现老年代有很多空闲的空间,但由于不足够大而导致无法分配,进而提前触发一次Full GC。

G1:是一款面向服务端的收集器。通过引入region的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。和CMS收集器类似,也会经历初始标记,并发标记,最终标记(CMS是重复标记),筛选回收(CMS是标记清除)四个步骤。其耗时较长也是第二和第四阶段。但具有以下明显特性:

1:并行和并发:初始标记和最终标记可以与用户线程并发执行,并发标记和筛选回收可以和用户线程并行执行。

2:分代收集:CMS是采用标记-清除算法实现,G1采用的是分代收集算法。整体上采用标记-整理,局部(两个region之间)采用复制。

3:避免了内存碎片的产生。从而减少Full GC的次数

4:可以预测的停顿。用户可以设置参数标明垃圾收集器的运行时间。为此要想实现在有限的时间里达到较高GC效率的话,就优先回收价值较高的region。G1将整个堆分为大小相同的独立region。维护一个回收价值的优先列表。这种Region的划分和具有优先级的回收方式保证了G1收集器的GC效率。


4:类加载器

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器

双亲委派模型的流程:

如果一个类接受到类加载请求,它自己不会去加载这个请求,而是将这个类加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器(Bootstrap ClassLoader)。 
只有当父类加载器无法加载这个请求时,子加载器才会尝试自己去加载。

双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中。

1)首先检查类是否被加载,没有则调用父类加载器的loadClass()方法;

2)若父类加载器为空,则默认使用启动类加载器作为父加载器;

3)若父类加载失败,抛ClassNotFoundException 异常,再调用自己的findClass() 方法。

双亲委派模型的优势:

解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载)可以保证我们的类有一个合适的优先级,例如Object类,它是我们系统中所有类的根类,采用双亲委派模型以后,不管是哪个类加载器来加载Object类,哪怕这个加载器是自定义类加载器,通过双亲委派模型,最终都是由启动类加载器去加载的,下次再次请求时若已经加载则直接返回,这样就可以保证Object这个类在程序的各个类加载器环境中都是同一个类。

双亲委派模型的破坏:

1:如果不想打破双亲委派模型,那么只需要重写findClass方法即可

2:如果想打破双亲委派模型,那么就重写整个loadClass方法。由于基础类要回调用户的代码造成

思考:假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类?

答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载

因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类,加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理, AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader, 但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap,父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。

定义自已的ClassLoader

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。

(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。

(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。

定义自已的类加载器分为两步:

1、继承java.lang.ClassLoader

2、重写父类的findClass方法

读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findClass方法?

因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。


5:JVM调优

1 选择合适的堆大小 设置参数如下:

 -Xmx:设置 JVM 最大可用内存

 -Xms:设置 JVM 最小可用内存,一般-Xmx 和-Xms 相同,以避免每次垃圾回收完后 JVM 重新分配内存

 -Xmn:设置年轻代的大小。

 -Xss:设置每个线程的堆栈大小

 -XX:SurvivorRatio:设置年轻代中 Eden 和 Survivor 区的大小比值。    -XX:MaxTenuringThreshold:设置年轻代经过多少次 GC 进入老年代。

 2 选择合适的回收器

吞吐量优先的应用使用并行收集器:

  -XX:UseParellelGC:选择垃圾收集器为并行收集器

-XX:ParallelGCThreads=n:配置并行收集器的线程数

-XX:UseParallelOldGC:配置老年代垃圾收集方式为并行收集

  -XX:MaxGCPauseMillis=n:设置每次年轻代垃圾回收的最长时间

 响应时间优先的应用使用并发收集器:

-XX:UseParNewGC:设置年轻代为并发收集。可与CMS收集同时使用 -XX:UseConcMarkSweepGC:设置老年代为并发收集。

-XX:UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

  -XX:CMSFullGCsBeforeCompaction:设置运行多少次 GC 以后对内存空间进行压缩、整理。

3:jvm问题排查命令:

Jps:显示系统中所有的虚拟机进程

Jstat:收集虚拟机各方面的运行数据

Jstat -gc pid ms count:每ms查询一次进程pid的垃圾收集情况,共查询count次

Jinfo:显示虚拟机配置信息

Jmap:生成虚拟机的内存转储快照(headdump文件)

Jmap -heap pid:显示堆详细信息

Jmap -dump:format=b,file=aaa.bin pid:生成pid的dump文件aaa.bin

Jhat:分析headdump文件,让用户可以在浏览器上查看分析结果

Jstack:显示虚拟机的线程快照


6:基本概念

对象的定位:java程序通过栈上的reference来操作堆上的具体对象。

1 使用句柄:Java 栈的 reference 中存储的是对象的句柄地址,Java 堆中划分出一块内存来作为句柄池。而句柄中包含了对象实例数据(存放在堆中)与类型数据(存放在方法区)各自的具体地址信息。

 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象)时只会改变句柄中的实例数据指针,reference 本身不需要修改。

2 直接指针:Java 栈的 reference 中存放的直接就是对象地址。

 优点:节省了一次指针定位的时间开销,速度快。

对象在内存的布局:

对象头(4 字节):用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,称为 Mark word。在 32 位的 HotSpot 虚拟机中,如果对象处于为被锁定的状态下,那么 MarkWord 的 32bit 空间中 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0。

 实例数据:包括了对象的所有成员变量,其大小由各个成员变量的大小决定。

 对齐填充:对象的大小必须是 8 字节的整数倍

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值