JVM总结与思考

1,运行时数据区内存模型

运行时数据区是JVM把自己管理的内存部分抽象出来的模型,抽象出来的不同的数据区域,以便于管理,具体有如下几个区域。

1 程序计数器 program counter register

在线程中,记录每个线程当前执行的语句行数,不会发生OOM,每个线程都有。

2) Java虚拟机栈 jvm stack

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

 

局部变量表:存放了编译期可知的各种基本类型(booleanbytecharshortintfloatlongdouble)、对象引用(reference 类型)returnAddress 类型(指向了一条字节码指令的地址)

 

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。

OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

 

3本地方法栈 native method stack

本地方法栈是在JVM调用本地的native方法的时候会使用的栈,在JDK源代码中有很多方法都是本地的native修饰的方法,这种方法jdk是不实现的,直接调用操作系统的一些源语,JVM只是获取返回值。比如arraycopy方法等。这里也有程序计数器,但是没行号。

4 java heap

堆中用来分配new创建的对象,堆分为新生代和老年代,长时间存在的对象或者较大的对象会进入老年代,刚生成的对象会进入新生代,新生代分为两类区域,一部分为Eden此部分较大占据百分之80的空间,另一部分为Survivor空间,一般会有两个Survivor空间各占百分之10左右。用来存放新生代的对象。

5)方法区method area

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

理论上方法区的位置在堆上,但是方法区和堆存放的数据却天差地别。方法区中存放类的信息,当类加载进来的时候会把字节码的信息存在方法区中,而且类文件里的类变量也存放在方法区中。

1.8开始,方法区不再作为JVM内存中的一个部分而是将方法区的数据放在本地内存上,作为元空间,这样的好处在于降低了方法区OOM的可能性,因为方法区主要加载的就是类的信息,如果类的信息过大可能导致方法区OOM,而这种OOM通常不是我们操作带来的问题,不是很好处理。

6)直接内存

JVM中有一块直接存储数据的区域,在JDK1.4之后,这块区域用给NIO做数据的缓冲,通过一个native的方法来分配出一块区域,通过一个在堆中的对象直接操作者部分内存。避免了在堆和系统内存中不断的复制空间的问题。

JMMjava内存模型

JMM内存模型的存在是为了屏蔽不同的操作系统和机器对一些内存,缓存的划分。JVM用一种主内存和工作内存关联的样子去刻画自己的这个内存模型。实际上,可以把主内存和堆内存做类比,工作内存和栈做类比。每个线程在工作内存中工作,通过读写和主内存交换数据。

2,垃圾回收

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

3.1垃圾标记

1)引用计数法

引用计数法非常简单,如果有引用指向了这个对象则把这个对象计数加一,如果一个对象的计数为0说明这个对象没有引用可以回收。但是这种方法存在明显的问题,1.无法判断四种不同类型的引用情况;2.如果发生对象之间的循环引用无法清理,导致内存泄漏。

2)可达性分析

可达性分析就是从一系列的ROOT对象出发,能够达到的对象我们就认为是不应该被回收的,其他对象无论是成环还是其他形状的对象我们都会进行回收操作。ROOT包括所有的栈上的对象以及在方法区中的静态变量能够联系到的堆中的对象。

3.2垃圾回收算法

1) 标记-清除

标记清除指现现内存堆区域标记一遍,发现所有需要回收的数据,将其打上标记,然后从头开始清理垃圾。这样的问题在于,有可能清理之后空间中有很多不连续的可用空间,碎片化的情况比较严重。这种算法的优势是简单。

2)复制算法

将堆空间分成两边,每次只用其中的一部分,需要进行清理的时候,将所有存活下来的数据放到另外一块区域上,之后统一把这半边的数据全部刷掉。这种方法的好处在于清理简单,劣势在于会造成空间的浪费,每次只用一半。

解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

3)标记-整理(老年代用)

标记整理在标记清除的基础上进行了优化,先把标记后存活的对象都放在堆空间的一侧,之后从结尾刷掉其他所有数据。

4)商业虚拟机回收算法(新生代用)

在商业中,新生代的虚拟机运用复制算法,把空间分成811的三个部分,每次用其中的9块,把剩余的存货对象放入1块中,因为新生代的对象普遍寿命短。在老年代中因为寿命比较长久,使用标记整理算法。

分代收集算法

 

  对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对象也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三部分。

3.3如何触发回收

新生代和老年代的回收相比,新生代比老年代快10倍,两者触发回收都是在空间(新生代是Eden)满了的情况下。

3.4,垃圾收集器

1)基础垃圾收集器

1 Serial 收集器

这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。收集垃圾时,必须stop the world,使用复制算法。

2 ParNew 收集器

可以认为是 Serial 收集器的多线程版本,也需要stop the world,复制算法

并行:Parallel

指多条垃圾收集线程并行工作,此时用户线程处于等待状态

并发:Concurrent

指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。

 

3 Parallel Scavenge 收集器

新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。

 

CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))

 

作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)

4 Serial Old 收集器

 Serial收集器的老年代版本,单线程收集器,使用标记整理算法。

5 Parallel Old 收集器

Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。

 

 

 

垃圾收集器有很多种,比较基本的垃圾收集器会在某个时刻进行Stop The World”的方式进行回收,这样的GC堆程序的响应非常差。

3)常用垃圾收集器——CMS

 

是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。

4)常用垃圾收集器——G1

 

标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿。

CMS收集器和G1收集器的区别:

 

CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;

G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;

CMS收集器以最小的停顿时间为目标的收集器;

G1收集器可预测垃圾回收的停顿时间

CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片

G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

 

类加载机制

3.1 类文件结构

类文件结构开始会有魔数表示这是一个class文件,之后有使用的java的版本号,之后是一些类的信息,变量,方法,以及方法变量的名称参数等。具体的名字会存放在运行时常量池中。

3.2类加载机制

1)加载

1)通过一个类的全限定名来获取这个类的二进制字节流(class文件或者其他形式的二进制字节流);

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

3)在内存中生成一个代表这个类的java.lang.Class类型的对象实例,作为方法区这个类的各种数据的访问入口。

java虚拟机规范没有规定一定放到堆区,HotSpot VM会把Class类的对象实例放到方法区)

接下来程序在运行过程中所有对该类的访问都通过这个类对象实例,也就是这个Class类型的类对象是提供给外界访问该类的接口。

2)链接——验证

验证是为了保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。

编译器和虚拟机是两个独立的东西,所以JVM不确定加载进的二进制流编译完是否被修改。

3)链接——准备

为类变量(即静态成员变量即被static修饰的变量)在方法区分配内存并设置初始值。

1public static int value =123;

在准备阶段为value设置的初始值为0而不是123;初始化阶段才会赋值123

特殊情况:被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段。

2public static final  int value =123;

 

4)链接——解析

在一些方法的参数部分会有一些引用变量,引用了其他的类,在解析这一步会将引用变量全部替换成直接变量,java的多态特性也是建立在这个的基础上,值得一提的是解析这一步并不一定在此处进行,可能在初始化之后进行也可。

5)初始化

到初始化阶段,才真正的开始执行类中定义的java程序代码(或者说字节码);

初始化阶段就是执行类构造器clinit()的过程。

进行初始化过程:先查看父类的初始化情况,如果父类尚未初始化则重复进行上述结构,然后在堆上为new的对象分配空间清并空,然后执行static语句块、静态成员赋值、初始化语句块、对象成员赋值、构造方法等。

3.3什么时候会触发初始化?

1)通过new创建对象;读取、设置一个类的静态成员变量(不包括final修饰的静态变量);调用一个类的静态成员函数。

2)使用java.lang.reflect进行反射调用的时候,如果类没有初始化,那就需要初始化;

3)当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类;

4)当虚拟机启动时,虚拟机会首先初始化带有main方法的类,即主类;

3.4类加载器

类加载器是用来对class文件进行加载的工具

3.5双亲委托

委托过程和加载过程:

1) 请求加载某个Class,类加载器AppClassLoader先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器ExtClassLoader

2)如果ExtClassLoader也没有加载过(缓存中没有),则委托给其父加载器Bootstrap ClassLoader,它也去查找自己的缓存。到这就是委托过程

3) 如果Bootstrap ClassLoader也没有加载过(缓存中没有),就去找自己的规定的路径下查找,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器ExtClassLoade自己去找。

4) 则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器AppClassLoader找。

5AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。到这是加载过程。

 

我们可以发现委托是从下向上,然后具体查找过程却是自上至下。

3,OOM异常

1OutOfMemoryError堆溢出

如果我们在堆上创建了一些不是很大的对象,但是创建数量很多就有可能造成堆溢出。这个时候我们通常可以设置参数HeapDumpOnOutOfMemory来打印溢出瞬间的堆空间快照。一般的情况下堆溢出分为两类,一类是堆空间溢出,空间不够大,我们可以设置参数增大的大小和自动扩展范围。还有一种是内存泄漏导致空间比实际的空间小,比如ThreadLocal配合线程池的情况,这种情况下我们通过工具进行可达性分析,分析出有问题的部分想办法进行回收即可。

2StackOverflowError栈溢出

栈溢出是我们在一个线程之中调用的方法过多导致的,这种情况一般是递归的逻辑有问题造成的,我们在测试的时候就能测试出这种错误。除此之外一般不会发生StackOverflow的问题。

3OutOfMemoryError栈溢出

栈还可能因为扩展的时候内存不够而溢出,这种情况比较特殊,是线程过多,而且每个线程中的调用链特别长的时候可能出现。此时为了解决这个问题,我们可以采用降低栈深度的方法,因为每个栈的深度降低了,总体可用的栈个数就能增加了。

4)方法区溢出

方法区在1.8中已经不存在了,因为随着CGLibgroovyJSP热部署等技术的出现,动态修

改字节码生成类和类加载器替换生成类的操作越来越多,如果我们将方法区放在堆中已经难以满足现状的类数量,把方法区放本地内存中是一个不错的选择。新特性也不能神奇地消除类和类加载器导致的内存泄漏。

5)直接内存溢出

直接内存溢出是我们在使用NIO的时候声明了一块区域,但这块区域的大小使得整个直接内存部分放不下,从而OOM。一般Thread Dump文件不大,但是其中有NIO操作的话可能是直接内存溢出导致的。直接内存溢出是NIO的底层通过Unsafe类来分配空间,分配的大小超过限额导致的。

 

4,JVM调优

1)减少Full GC

因为Full GC要整理整个堆区,会很慢(几秒甚至数十秒),因此应该让尽量少的对象进入老年代。

 

1)确保对象都是“朝生夕死”的

一个对象使用完后应尽快让他失效,然后尽快在新生代中被Minor GC回收掉,尽量避免对象在新生代中停留太长时间。

2)提高大对象直接进入老年代的门槛

通过设置参数-XX:PretrnureSizeThreshold来提高大对象的门槛,尽量让对象都先进入新生代,然后尽快被Minor GC回收掉,而不要直接进入老年代。

 

2)当高并发时

应该适当增加S区的大小,因为发生YGC时,大量对象(高并发时)被引用而无法被GC掉,S区太小则只能到老年代;

当然增加s区自然会减小Eden区,从而增加YGC young gc)的频率,但是换来的却是高吞吐量。

 

默认:

新生代占jvm内存 3/8;

新生代中Eden区:s区:s=811

5,JVM命令行

1)jps unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号。

2)jmap 查看堆内存空间,可以查看堆空间的数据分布情况,新生代和老年代。

3)javac 编译java文件

4)jstack  查看栈内存空间,可以查看多线程死锁的问题,查看各个进程的状态快照。

5)jstat:一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。 

6)jinfo:用来查看JVM参数和动态修改部分JVM参数的命令

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

6,JVM启动参数

在哪设置JVM的启动参数:

1)eclipse需要修改根目录文件eclipse.ini

2)tomcat bin catalina.sh 文件内添加

 

常用的设置参数;

-Xmx  指定JVM最大 堆内存  例:-Xmx3550m:设置JVM最大堆内存为3550M

-Xms  指定JVM初始 堆内存  例:-Xms3550m:设置JVM初始堆内存为3550M

 

-Xmn  (默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?) 用于设置新生代大小

-XX:NewSize=1024m:设置年轻代初始值为1024M

-XX:MaxNewSize=1024m:设置年轻代最大值为1024M

 

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1Eden区的比值为2:4,即1Survivor区占整个年轻代大小的1/6

 

-Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M

 

-Xmx3550m: 最大堆大小为3550m

 

-Xms3550m: 设置初始堆大小为3550m

 

-Xmn2g: 设置年轻代大小为2g

 

-Xss128k: 每个线程的堆栈大小为128k

 

-XX:MaxPermSize: 设置持久代大小为16m

 

-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。

 

-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

 

-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。

https://www.cnblogs.com/marcotan/p/4256885.html

 

8. 再谈引用

 

前面的两种方式判断存活时都与‘引用’有关。但是 JDK 1.2 之后,引用概念进行了扩充,下面具体介绍。

 

下面四种引用强度一次逐渐减弱

 

强引用

 

类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。

 

软引用

 

SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

用处: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建

2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出

弱引用

 

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

 

虚引用

 

PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

 

 生存还是死亡

 

即使在可达性分析算法中不可达的对象,也并非是facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

 

如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象建立关联即可。

 

finalize() 方法只会被系统自动调用一次。

 

2.2.5 回收方法区

 

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

 

永久代垃圾回收主要两部分内容:废弃的常量和无用的类。

 

判断废弃常量:一般是判断没有该常量的引用。

 

判断无用的类:要以下三个条件都满足

 

该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例

加载该类的 ClassLoader 已经被回收

该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

9.什么情况下会发生栈内存溢出。

思路: 描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK的话可以给面试官手写是一个栈溢出的demo

 

我的答案:

 

栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。

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

参数 -Xss 去调整JVM栈的大小

10.JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为EdenSurvivor

思路: 先讲一下JAVA堆,新生代的划分,再谈谈它们之间的转化,相互之间一些参数的配置(如: –XX:NewRatio,–XX:SurvivorRatio等),再解释为什么要这样划分,最好加一点自己的理解。

 

我的答案:

 

1)共享内存区划分

 

共享内存区 = 持久带 +

持久带 = 方法区 + 其他

Java= 老年代 + 新生代

新生代 = Eden + S0 + S1

2)一些参数的配置

 

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。

默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)

Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)

3)为什么要分为EdenSurvivor?为什么要设置两个Survivor区?

 

如果没有SurvivorEden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为EdenSurvivor

Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16Minor GC还能在新生代中存活的对象,才会被送到老年代。

设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GCEden中的存活对象就会被移动到第一块survivor space S0Eden被清空;等Eden区再满了,就再触发一次Minor GCEdenS0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

11.JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代

思路: 先描述一下Java堆内存划分,再解释Minor GCMajor GCfull GC,描述它们之间转化流程。

 

我的答案:

 

Java= 老年代 + 新生代

新生代 = Eden + S0 + S1

Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。

大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;

如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。

老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GCFull GC 清理整个内存堆 – 包括年轻代和年老代。

Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC10倍以上。

12.JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存。

思路: 先画出Java内存模型图,结合例子volatile ,说明什么是重排序,内存屏障,最好能给面试官写以下demo说明。

 

我的答案:

1Java内存模型图:

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

2)指令重排序。

在这里,先看一段代码

 

public class PossibleReordering {

static int x = 0, y = 0;

static int a = 0, b = 0;

 

public static void main(String[] args) throws InterruptedException {

    Thread one = new Thread(new Runnable() {

        public void run() {

            a = 1;

            x = b;

        }

    });

 

    Thread other = new Thread(new Runnable() {

        public void run() {

            b = 1;

            y = a;

        }

    });

    one.start();other.start();

    one.join();other.join();

    System.out.println((+ x + ,+ y + ));

}

运行结果可能为(1,0)(0,1)(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOEOOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排。

 

3)内存屏障

 

内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。

 

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

4happen-before原则

 

单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。

volatilehappen-before原则:对一个volatile变量的写操作happen-before对此变量的任意读操作(当然也包括写操作了)

happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。

线程中断的happen-before原则 :对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

线程终结的happen-before原则: 线程中的所有操作都happen-before线程的终止检测。

对象创建的happen-before原则: 一个对象的初始化完成先于他的finalize方法调用。

13.怎么打出线程栈信息。

思路: 可以说一下jpstop jstack这几个命令,再配合一次排查线上问题进行解答。

 

我的答案:

 

输入jps,获得进程号。

top -Hp pid 获取本进程中所有线程的CPU耗时性能

jstack pid命令查看当前java进程的堆栈状态

或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。

可以使用fastthread 堆栈定位,fastthread.io/

14.简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。

思路: 先说明一下什么是类加载器,可以给面试官画个图,再说一下类加载器存在的意义,说一下双亲委派模型,最后阐述怎么打破双亲委派模型。

 

我的答案:

 

1) 什么是类加载器?

 

类加载器 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。

 

启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot,负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。

其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:

扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。

应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

2)双亲委派模型

 

双亲委派模型工作过程是:

 

如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

 

双亲委派模型图:

 

 

 

 

 

3)为什么需要双亲委派模型?

 

在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码

 

4)怎么打破双亲委派模型?

 

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClassfindClass方法。

 

 

  当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

 

                                                    

 

 

 

 

 

一、类加载过程

1.加载    

    加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

 

    类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

 

    通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

 

从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。

JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。

通过网络加载class文件。

把一个Java源文件动态编译,并执行加载。

    类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

 

2.链接

    当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

 

    1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

 

    四种验证做进一步说明:

 

    文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

 

    元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

 

    字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

 

    符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

 

   2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

 

   3)解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

 

3.初始化

    初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量astatic的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10

 

二、类加载时机

创建类的实例,也就是new一个对象

访问某个类或接口的静态变量,或者对该静态变量赋值

调用类的静态方法

反射(Class.forName("com.lyj.load")

初始化一个类的子类(会首先初始化子类的父类)

JVM启动时标明的启动类,即文件名和类名相同的那个类    

     除此之外,下面几种情形需要特别指出:

 

     对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

 

三、类加载器

    类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

 

   JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

 

 1)根类加载器(bootstrap class loader:它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOMEjre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

 

下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:

 

public class ClassLoaderTest {

 

public static void main(String[] args) {

 

URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();

for(URL url : urls){

System.out.println(url.toExternalForm());

}

}

}

运行结果:

 

 

 

  2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null

 

  3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader

 

类加载器加载Class大致要经过如下8个步骤:

 

检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。

如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。

请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。

请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。

当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。

从文件中载入Class,成功后跳至第8步。

抛出ClassNotFountException异常。

返回对应的java.lang.Class对象。

四、类加载机制:

1.JVM的类加载机制主要有如下3种。

 

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

2.这里说明一下双亲委派机制:

 

 

 

       双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

 

      双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改

     

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系, 比如类java.lang.object类,无论哪一个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器进行加载,因此object类在程序的各种类加载器环境中都是同一个类。如果没有双亲委派模型,有各个类加载器自行去加载的话,那么系统中会出现多个不同 的object类,应用程序变的混乱。

  应用程序类加载器 扩展类加载器 启动类加载器

15如果我两个类是同名的,但是处于不同的包下面,在类加载的时候会发生什么?

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

16 堆和栈的区别

Java把内存划分成两种:一种是栈内存,一种是堆内存。  
   
  在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。  
   
  当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。  
   
  堆内存用来存放由new创建的对象和数组。  
   
在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。  
   
  在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。  
   
引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。 

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;而通过new关键字和构造器创建的对象放在堆空间;程序中的字面量(literal)如直接书写的100"hello"和常量都是放在静态区中。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,理论上整个内存没有被其他进程使用的空间甚至硬盘上的虚拟内存都可以被当成堆空间来使用。
String str = new String("hello");
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而"hello"这个字面量放在静态区。

17 JAVA中的域,静态域,实例域

1.java中的域

所谓的域,翻译成英文就是field, 也就是我们常说的字段,或者说是属性。 比如类的字段(属性),局部的,全局的。所谓域,其实是“field”的翻译

然后实例域,就是 实例(“object” )的"field"。包括实例域和静态域,静态域又叫类域。

java中对象中的数据称为实例域(instance field)。

2.静态域

如果将域定义为static,那么每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。例如,假定需要给每一个雇员赋予唯一的表示码。这里给Employee类添加一个实例域id和一个静态域nextld:

class Employee

{

 private int id;

 

 private static int nextId=1;

现在,每一个雇员对象都有一个自己的id域,但这个类的所有实例将共享一个nextid域,换句话说,如果有1000个Employee类的对象,则有1000个实例域id,但是只有一个静态域nextid,即使没有一个雇员对象,静态域nextil也存在,他属于类,而不属于任何独立的对象。

18 Java内存泄漏的排查总结

一、内存溢出和内存泄露

一种通俗的说法。

1、内存溢出:你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,出现溢出。

2、内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。

 

下面具体介绍。

 

1.1 内存溢出

java.lang.OutOfMemoryError,是指程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError

产生原因

产生该错误的原因主要包括:

 

JVM内存过小。

程序不严密,产生了过多的垃圾。

程序体现

一般情况下,在程序上的体现为:

 

内存中加载的数据量过于庞大,如一次从数据库取出过多数据。

集合类中有对对象的引用,使用完后未清空,使得JVM不能回收。

代码中存在死循环或循环产生过多重复的对象实体。

使用的第三方软件中的BUG

启动参数内存值设定的过小。

错误提示

此错误常见的错误提示:

tomcat:java.lang.OutOfMemoryError: PermGen space

tomcat:java.lang.OutOfMemoryError: Java heap space

weblogic:Root cause of ServletException java.lang.OutOfMemoryError

resin:java.lang.OutOfMemoryError

java:java.lang.OutOfMemoryError

 

解决方法

 

增加JVM的内存大小

对于tomcat容器,找到tomcat在电脑中的安装目录,进入这个目录,然后进入bin目录中,在window环境下找到bin目录中的catalina.bat,在linux环境下找到catalina.sh

编辑catalina.bat文件,找到JAVA_OPTS(具体来说是 set "JAVA_OPTS=%JAVA_OPTS% %LOGGING_MANAGER%")这个选项的位置,这个参数是Java启动的时候,需要的启动参数。

也可以在操作系统的环境变量中对JAVA_OPTS进行设置,因为tomcat在启动的时候,也会读取操作系统中的环境变量的值,进行加载。

如果是修改了操作系统的环境变量,需要重启机器,再重启tomcat,如果修改的是tomcat配置文件,需要将配置文件保存,然后重启tomcat,设置就能生效了。

优化程序,释放垃圾

主要思路就是避免程序体现上出现的情况。避免死循环,防止一次载入太多的数据,提高程序健壮型及时释放。因此,从根本上解决Java内存溢出的唯一方法就是修改程序,及时地释放没用的对象,释放内存空间。

1.2 内存泄露

Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:

1)首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;

2)其次,这些对象是无用的,即程序以后不会再使用这些对象。

如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

 

关于内存泄露的处理页就是提高程序的健壮型,因为内存泄露是纯代码层面的问题。

 

1.3 内存溢出和内存泄露的联系

内存泄露会最终会导致内存溢出。

相同点:都会导致应用程序运行出现问题,性能下降或挂起。

不同点:1) 内存泄露是导致内存溢出的原因之一,内存泄露积累起来将导致内存溢出。2) 内存泄露可以通过完善代码来避免,内存溢出可以通过调整配置来减少发生频率,但无法彻底避免。

 

二、一个Java内存泄漏的排查案例

某个业务系统在一段时间突然变慢,我们怀疑是因为出现内存泄露问题导致的,于是踏上排查之路。

 

2.1 确定频繁Full GC现象

首先通过“虚拟机进程状况工具:jps”找出正在运行的虚拟机进程,最主要是找出这个进程在本地虚拟机的唯一IDLVMIDLocal Virtual Machine Identifier),因为在后面的排查过程中都是需要这个LVMID来确定要监控的是哪一个虚拟机进程。

同时,对于本地虚拟机进程来说,LVMID与操作系统的进程IDPIDProcess Identifier)是一致的,使用Windows的任务管理器或Unixps命令也可以查询到虚拟机进程的LVMID

jps命令格式为:

jps [ options ] [ hostid ]

使用命令如下:

使用jpsjps -l

使用psps aux | grep tomat

 

找到你需要监控的ID(假设为20954),再利用“虚拟机统计信息监视工具:jstat”监视虚拟机各种运行状态信息。

jstat命令格式为:

jstat [ option vmid [interval[s|ms] [count]] ]

使用命令如下:

jstat -gcutil 20954 1000

意思是每1000毫秒查询一次,一直查。gcutil的意思是已使用空间站总空间的百分比。

结果如下图:

 

jstat执行结果

查询结果表明:这台服务器的新生代Eden区(E,表示Eden)使用了28.30%(最后)的空间,两个Survivor区(S0S1,表示Survivor0Survivor1)分别是08.93%,老年代(O,表示Old)使用了87.33%。程序运行以来共发生Minor GCYGC,表示Young GC101次,总耗时1.961秒,发生Full GCFGC,表示Full GC7次,Full GC总耗时3.022秒,总的耗时(GCT,表示GC Time)为4.983秒。

 

2.2 找出导致频繁Full GC的原因

分析方法通常有两种:

1)把堆dump下来再用MAT等工具进行分析,但dump堆要花较长的时间,并且文件巨大,再从服务器上拖回本地导入工具,这个过程有些折腾,不到万不得已最好别这么干。

2)更轻量级的在线分析,使用“Java内存影像工具:jmap”生成堆转储快照(一般称为headdumpdump文件)。

jmap命令格式:

jmap [ option ] vmid

使用命令如下:

jmap -histo:live 20954

查看存活的对象情况,如下图所示:

 

存活对象

按照一位IT友的说法,数据不正常,十有八九就是泄露的。在我这个图上对象还是挺正常的。

 

我在网上找了一位博友的不正常数据,如下:

 

image.png

可以看出HashTable中的元素有5000多万,占用内存大约1.5G的样子。这肯定不正常。

 

2.3 定位到代码

定位带代码,有很多种方法,比如前面提到的通过MAT查看Histogram即可找出是哪块代码。——我以前是使用这个方法。 也可以使用BTrace,我没有使用过。

 

举例:

 

一台生产环境机器每次运行几天之后就会莫名其妙的宕机,分析日志之后发现在tomcat刚启动的时候内存占用比较少,但是运行个几天之后内存占用越来越大,通过jmap命令可以查询到一些大对象引用没有被及时GC,这里就要求解决内存泄露的问题。

 

 

 

Java的内存泄露多半是因为对象存在无效的引用,对象得不到释放,如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析:

2.3 定位到代码

定位带代码,有很多种方法,比如前面提到的通过MAT查看Histogram即可找出是哪块代码。——我以前是使用这个方法。 也可以使用BTrace,我没有使用过。

 

举例:

 

一台生产环境机器每次运行几天之后就会莫名其妙的宕机,分析日志之后发现在tomcat刚启动的时候内存占用比较少,但是运行个几天之后内存占用越来越大,通过jmap命令可以查询到一些大对象引用没有被及时GC,这里就要求解决内存泄露的问题。

 

 

 

Java的内存泄露多半是因为对象存在无效的引用,对象得不到释放,如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析:

1. 用工具生成java应用程序的heap dump(如jmap

2. 使用Java heap分析工具(如MAT),找出内存占用超出预期的嫌疑对象

3. 根据情况,分析嫌疑对象和其他对象的引用关系。

4. 分析程序的源代码,找出嫌疑对象数量过多的原因。

19 class文件的编码格式

Java开发中,常常会遇到乱码的问题,一旦遇到这种问题,常常就很扯蛋,每个人都不愿意承认是自己的代码有问题。其实编码问题并没有那么神秘,那么不可捉摸,搞清Java的编码本质过程就真相大白了。

其实,编码问题存在两个方面:JVM之内和JVM之外。

1、Java文件编译后形成class

这里Java文件的编码可能有多种多样,但Java编译器会自动将这些编码按照Java文件的编码格式正确读取后产生class文件,这里的class文件编码是Unicode编码(具体说是UTF-16编码)。

 

因此,在Java代码中定义一个字符串:

String s="汉字";

不管在编译前java文件使用何种编码,在编译后成class后,他们都是一样的----Unicode编码表示。

 

2、JVM中的编码

JVM加载class文件读取时候使用Unicode编码方式正确读取class文件,那么原来定义的String s="汉字";在内存中的表现形式是Unicode编码。

 

当调用String.getBytes()的时候,其实已经为乱码买下了祸根。因为此方法使用平台默认的字符集来获取字符串对应的字节数组。在WindowsXP中文版中,使用的默认编码是GBK,不信运行下:

public class Test { 
        public static void main(String[] args) { 
                System.out.println("当前JRE:" + System.getProperty("java.version")); 
                System.out.println("当前JVM的默认字符集:" + Charset.defaultCharset()); 
        } 
}

 

当前JRE1.6.0_16 
当前JVM的默认字符集:GBK

 

当不同的系统、数据库经过多次编码后,如果对其中的原理不理解,就容易导致乱码。因此,在一个系统中,有必要对字符串的编码做一个统一,这个统一模糊点说,就是对外统一。比如方法字符串参数,IO流,在中文系统中,可以统一使用GBK、GB13080、UTF-8、UTF-16等等都可以,只是要选择有些更大字符集,以保证任何可能用到的字符都可以正常显示,避免乱码的问题。(假设对所有的文件都用ASCII码)那么就无法实现双向转换了。

 

要特别注意的是,UTF-8并非能容纳了所有的中文字符集编码,因此,在特殊情况下,UTF-8转GB18030可能会出现乱码,然而一群傻B常常在做中文系统喜欢用UTF-8编码而不说不出个所以然出来!最傻B的是,一个系统多个人做,源代码文件有的人用GBK编码,有人用UTF-8,还有人用GB18030。FK,都是中国人,也不是外包项目,用什么UTF-8啊,神经!源代码统统都用GBK18030就OK了,免得ANT脚本编译时候提示不可认的字符编码。

 

因此,对于中文系统来说,最好选择GBK或GB18030编码(其实GBK是GB18030的子集),以便最大限度的避免乱码现象。

 

3、内存中字符串的编码

内存中的字符串不仅仅局限于从class代码中直接加载而来的字符串,还有一些字符串是从文本文件中读取的,还有的是通过数据库读取的,还有可能是从字节数组构建的,然而他们基本上都不是Unicode编码的,原因很简单,存储优化。

 

因此就需要处理各种各样的编码问题,在处理之前,必须明确“源”的编码,然后用指定的编码方式正确读取到内存中。如果是一个方法的参数,实际上必须明确该字符串参数的编码,因为这个参数可能是另外一个日文系统传递过来的。当明确了字符串编码时候,就可以按照要求正确处理字符串,以避免乱码。

在对字符串进行解码编码的时候,应该调用下面的方法:

getBytes(String charsetName)    
String(byte[] bytes, String charsetName)

 

而不要使用那些不带字符集名称的方法签名,通过上面两个方法,可以对内存中的字符进行重新编码。

 

20 cpu占用率高怎么排查

1先用top命令,找到cpu占用最高的进程 PID  

2再用ps -mp pid -o THREAD,tid,time   查询进程中,那个线程的cpu占用率高 记住TID

3jstack 29099 >> xxx.log   打印出该进程下线程日志

  1. sz xxx.log 将日志文件下载到本地
  2. 将查找到的 线程占用最高的 tid  上上上图中 29108   转成16进制  --- 71b4
  3. 打开下载好的 xxx.log  通过 查找方式 找到 对应线程 进行排查

CPU占用过高

1、现象重现

CPU占用过高一般情况是代码中出现了循环调用,最容易出现的情况有几种:

a)递归调用,退出机制设计的不够合理;

b)定时器启动过频繁;

c)代码出现死循环

 GC频繁也可能导致CPU占用过高

我用最简单的死循环来举例:

while (true){

        ...

}

21 触发fullGC的原因

当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时,会触发Full GC

当老年代的空间使用率超过某阈值时,会触发Full GC

当元空间不足时(JDK1.7永久代不足),也会触发Full GC

当调用System.gc()也会安排一次Full GC

系统高峰期fullGC频繁,优化后恢复正常。 
导致原因: 
系统中有一个调用频繁的接口会调用下面这个方法,目的是获取图片的宽高信息,但是Image这个对象用完不会自动释放,需要手动调用 flush()方法;以前没有调用这个方法,就导致一有请求就会有大对象进入old区,在业务高峰期old区一会就被打满,所以一直进行fgc。

 

解决办法: 
其实不管是用Image还是BufferedImage,读取图片的宽高不用把图片全部加载到内存,在图片的宽高信息其实是存储在文件头中的,只 要按不同的格式读取文件的头信息就可以拿到宽高信息 
使用ImageReader代码如下

posted @ 2020-07-17 11:09  太阳恒  阅读(207)  评论(0)    收藏  举报