JVM

在这里插入图片描述

Java中内存泄漏问题

会存在内存泄漏,自己实现堆载的数据结构时有可能会出现内存泄露,可参看effective java。

64位JVM中,int的长度是多数?

Serial与Parallel GC之间的不同之处?

32位和64位的JVM,int类型变量的长度是多数?

32位和64位的JVM中,int类型变量的长度是相同的,都是32位或者4个字节。

Java中WeakReference与SoftReference的区别?

JVM选项-XX:+UseCompressedOops有什么作用?为什么要使用?

怎样通过Java程序来判断JVM是32位还是64位?

32位JVM和64位JVM的最大堆内存分别是多数?

JRE、JDK、JVM及JIT之间有什么不同?

解释Java堆空间及GC?

JVM内存区域

在这里插入图片描述
JVM内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA堆、方法区】、直接内存。

线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在Hotspot VM内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。

线程共享区域随虚拟机的启动/关闭而创建/销毁。

直接内存并不是JVM运行时数据区的一部分, 但也会被频繁的使用:在JDK1.4引入的NIO提供了基于Channel与Buffer的IO方式, 它可以使用Native函数库直接分配堆外内存, 然后使用DirectByteBuffer对象作为这块内存的引用进行操作(详见:Java I/O扩展), 这样就避免了在Java堆和Native堆中来回复制数据, 因此在一些场景中可以显著提高性能。
在这里插入图片描述

程序计数器(线程私有)

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有” 的内存。

正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如果还是Native方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈(线程私有)

是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派(Dispatch Exception)。 栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
在这里插入图片描述

本地方法区(线程私有)

不能,虽然你可以调用System.gc() 或者Runtime.gc(),但是没有办法保证GC的执行。

你能保证GC执行吗?

怎么获取Java程序使用的内存?堆使用的百分比?

可以通过java.lang.Runtime类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。Runtime.freeMemory() 方法返回剩余空间的字节数,Runtime.totalMemory()方法总内存的字节数,Runtime.maxMemory() 返回最大内存的字节数。

Java中堆和栈有什么区别?

描述一下JVM加载class文件的原理机制

GC是什么?为什么要有GC?

GC是垃圾收集的意思 ,内存处理是编程人员容易出现问题的地方 ,忘记或者错误的内存回收会导致程序 或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否 超过作用域从而达到自动回收内存的目的 ,Java语言没有提供释放已分配内存的显示操作方法 。Java程序员不用担心内存管理, 因为垃圾收集器会自动进行管理 。要请求垃圾收集 ,可以调用下面的方法之一 :System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用 。

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

堆(Heap-线程共享)-运行时数据区

方法区/永久代(线程共享)

JVM运行时内存

在这里插入图片描述
Java堆从GC的角度还可以细分为:新生代(Eden 区、From Survivor区和To Survivor区)和老年代

新生代

老年代

永久代

JAVA8与元数据

引用计数法

可达性分析

标记清除算法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。

如图
在这里插入图片描述
从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

复制算法(copying)

为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。

如图:
在这里插入图片描述
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话, Copying算法的效率会大大降低。

标记整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同, 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

如图
在这里插入图片描述

分代收集算法

新生代与复制算法

目前大部分JVM 的 GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1: 1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
在这里插入图片描述

老年代与标记复制算法

分代收集算法

在新生代-复制算法

在老年代-标记整理算法

分区收集算法

GC垃圾收集器

Java堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;年老代主要使用标记-整理垃圾回收算法,因此java虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器, JDK1.6中Sun HotSpot虚拟机的垃圾收集器如下:
在这里插入图片描述

Serial垃圾收集器(单线程、 复制算法)

ParNew垃圾收集器(Serial+多线程)

Parallel Scavenge收集器(多线程复制算法、高效)

Serial Old收集器(单线程标记整理算法 )

Serial Old是Serial垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client默认的java虚拟机默认的年老代垃圾收集器。在Server模式下,主要有两个用途:

  1. 在 JDK1.5 之前版本中与新生代的Parallel Scavenge收集器搭配使用。

  2. 作为年老代中使用CMS收集器的后备垃圾收集方案。新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:
    在这里插入图片描述
    新生代Parallel Scavenge收集器与ParNew收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。新生代ParallelScavenge/ParNew与年老代Serial Old搭配垃圾收集过程图:
    在这里插入图片描述

四大引用

1、强引用:普通的变量引用
2、软引用(SoftReference):将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
//使用场景:浏览器的后退按钮
3、弱引用(WeakReference):将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
4、虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

Parallel Old收集器(多线程标记整理算法)

CMS收集器(多线程标记清除算法)

G1收集器

JVM类加载机制

加载:把.java文件编译成.class文件,生成Class对象
验证:验证字节码的准确性
准备:给类的静态变量做分配内存,并赋予默认值
解析:符号引用和动态链接都变为直接引用
初始化:给类的静态变量初始化为指定的值,执行静态代码块

类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3 种类加载器:
**根类加载器(Bootstrap classLoader):**负责加载lib下的核心类库

启动类加载器(Bootstrap ClassLoader): 负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别, 如 rt.jar)的类。

**自定义类加载器:**继承ClassLoader,重写loadClass(),findClass(),一般是只需要重写findClass

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

应用程序类加载器(Application ClassLoader):
负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载, 当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
在这里插入图片描述

双亲委派

双亲加载机制中源码有两个方法:
1、loadClass
1)先检查指定的类是否已经加载过了,若已经加载过,则直接返回加载的类
2)若没有加载,则判断有没有父类,有的话则调用父类加载器,或者调用根类加载器(Bootstrap)加载。
3)若父类加载器与Bootstrap加载器都没有找到指定的类,则调用下面的方法(findClass)来完成类加载
2、findClass

为什么要设计双亲加载机制

1、保证类的唯一性
2、沙箱安全机制

全盘委托机制

如果没有显示的使用其他类加载器,则类下的所有依赖与及引用的类都将会有加载该类的类加载器加载

Tomcat如何自定义类加载机制1、CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;

2、CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
3、SharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
4、WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,
每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;
5、模拟实现Tomcat的JasperLoader热加载
原理:后台启动线程监听jsp文件变化,如果变化了找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器
赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot引用了,下一次gc的时候会被销毁

=>总结:每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

OSGI(动态模型系统)

动态改变构造

模块化编程与热插拔

JVM内存模型

私有:
程序计时器:记录当前线程执行到字节码行号
虚拟机栈:内部有许多栈帧,每个栈帧里面包括局部变量表,操作数栈,动态链接,方法出口。
本地方法栈:执行本地的Native方法
共享:
堆:内部分为eden区,s0,s1,老年代,保存对象和数组
方法区/永久代(1.8后元空间):保存类信息、常量、静态变量、即时编译器编译后的代码;内部有个运行时常量池,用于保存类的版本、字段、方法、接口等;

扩展=>直接内存:通过unsafe,或者netty的DirectByteBuffer申请

本地方法栈

程序计数器

方法区

分代回收

分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间
在这里插入图片描述
年轻代->标记-复制

老年代->标记-清除

堆和栈的区别

什么时候会触发FullGC

什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

描述一下JVM加载class文件的原理机制?

Java对象创建与分配

创建

1、类加载检查

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表

的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等

2、分配内存

//划分内存
1、指针碰撞
内存规整,用过的内存放一边,没用过的放一边
2、空闲列表
内存不规整,使用的和空闲的相互交错,需要一个列表进行存储
//并发问题解决
1、CAS
2、本地线程分配缓冲区(TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
通过XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize指定TLAB大小。

3、初始化

为分配到的内存初始化为零值,不设置对象头,若是呀TLAB,可以提前至TLAB分配时进行,保证对象即使不赋初始值也可以直接使用

4、设置对象头

对象布局:
1、对象头(Header)
2、实例数据(Instance Data)
3、对齐填充(Padding)
在这里插入图片描述

5、执行方法

执行方法,也就是所谓的属性赋值与执行构造器

分配

1、栈上分配
通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
/**
*对象逃逸分析:分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中
*标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所 代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配
*/
‘结论:栈上分配依赖于逃逸分析和标量替换’
2、堆上分配(eden区)
1、先eden区分配,满了young GC,把存活的对象放入s0
2、再eden区分配,满了young GC,把s0存活的对象和eden区存活的对象放入s1,
3、重复1,2操作
3、大对象进入老年代
大量连续的内存空间的对象
4、长期存活对象进入老年代
在2(堆上分配)中,每次移动都会给当前对象设置个计数器,默认15,CMS默认6,则会young gc放入老年代
5、对象动态年龄判断
当一批对象的总大小大于s区内存大小的50%,则大于等于这批对象年龄最大值的对象,就可以进入老年代
6、空间担保机制
年轻代每次young gc之前JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),
就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了) 的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,
是否大于之前每一次minor gc后进入老年代的对象的平均大小。小于或者之前说的参数没有设置,那么就会触发一次Full gc,
对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"。

简述Java的对象结构

如何判断对象可以被回收

1、该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2、加载该类的 ClassLoader 已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

JVM的永久代中会发生垃圾回收么

垃圾收集算法

调优命令有哪些?

调优工具

Minor GC与Full GC分别在什么时候发生?

finalize()方法最终判定对象是否存活

  1. 第一次标记并进行一次筛选。
    筛选的条件是此对象是否有必要执行finalize()方法。
    当对象没有覆盖finalize方法,对象将直接被回收。
  2. 第二次标记
    如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救 自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第 二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

//注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。

你知道哪些JVM性能调优

设定堆内存大小-Xmx:堆内存最大限制。

设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代

-XX:NewSize:新生代大小

-XX:NewRatio 新生代和老生代占比

-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比

设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

JVM如何判断垃圾

垃圾判断算法:引用判断法,可达性分析法。

引用判断法

假设堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1。
当引用失效时,则之前被引用的对象的计数器的值就减 1。引用计数为 0 的对象,就可以称之为垃圾,可以被收集。
当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。
优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。

可达性分析法

可达性分析法也被称之为根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。一个对象只有满足下述两个条件之一,就会被判断为可达的:
对象是属于根集中的对象
对象被一个可达的对象引用
在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的常量引用的对象
方法区中的类静态属性引用的对象
本地方法栈中 JNI(Native 方法)的引用对象
活跃线程(已启动且未停止的 Java 线程)
根集中的对象称之为GC Roots,也就是根对象。可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。
如上图所示,形象的展示了可达对象与不可达对象的示例,其中灰色的对象都是不可达对象,表示可以被垃圾收集的对象。在可达性分析法中,对象有两种状态,那么是可达的、要么是不可达的,在判断一个对象的可达性的时候,就需要对对象进行标记。
优点:可以解决循环引用的问题,不需要占用额外的空间
缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用

垃圾回收算法

垃圾回收算法:标记-清除算法、标记-整理算法、复制算法、分代收集算法。

标记-清除算法

标记-清除(Tracing Collector)算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。
优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点:标记和清除过程的效率都不高,这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片,虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。

标记-整理算法

标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。
优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

复制算法

复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。
优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。

分代收集算法

分代收集算法的将堆内存划分为新生代、老年代和永久代。新生代又被进一步划分为 Eden 和 Survivor 区,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。
新生代:目的是回收那些生命周期短的对象,主要存放新产生的对象。新生代按照8:1:1分为eden区、survivor0、survivor1,大部分对象在eden区中生成,当eden满时,将存活的对象复制到survivor0,然后清空eden,当eden、survivor0都满了时,将这两个区中存活的对象复制到survivor1,然后清空eden、survivor0,当着三个区都满了时则把存货对象复制到老年代,如果老年代也满了则触发FullGC。新生代的全回收叫MinorGC,MinorGC发生频率比较高,不一定等到新生代满了时才进行。
老年代:存放对象生命周期较长,且内存大概是新生代的两倍,老年代存活对象生命周期长,因此MajorGC发生频率较低。
永久代:主要存放静态文件,如Java类,方法等。永久带对垃圾回收基本没有影响,当应用动态生成或者调用一些类的时候,例如反射、动态代理CGLib等bytecode框架时需要永久带来保存新生成的类。

JVM调优

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。

Full GC

会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。

导致Full GC的原因

年老代(Tenured)被写满

调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在年老代创建对象 。

持久代Pemanet Generation空间不足

增大Perm Gen空间,避免太多静态对象 ,控制好新生代和年老代的比例

System.gc()被显示调用

垃圾回收不要手动触发,尽量依靠JVM自身的机制
在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节,下面详细介绍对应JVM调优的方法和步骤。

JVM性能调优方法和步骤

性能优化常用命令

● top/jps -l:获取要监控的进程ID:Pid。
● jinfo Pid:输出进程的基本信息。
● jstat -gcutil Pid 5s:每5秒输出一次GC情况。
● jstack -l Pid > /data/jstack.txt:将指定进行的线程情况进行输出到指定文件中。
● jmap -histo Pid > /data/histo.txt:将堆中对象统计信息输出到指定文件中。
● jmap -heap Pid > /data/jmap_heap.txt:输出堆内存信息到指定文件中。
● jmap -J-d64 -dump:format=b,file=/data/heap_dump.bin Pid:输出JVM的堆内容到指定文件中。

1、监控GC的状态

使用各种JVM工具(Java Virtual Machine),查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。
举一个例子: 系统崩溃前的一些现象:
● 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
● FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
● 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
● 之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。

2、生成堆的dump文件

通过JMX的MBean生成当前的堆(Heap)信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

3、分析dump文件

打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件: Visual VM、IBM HeapAnalyzer、JDK 自带的Hprof工具、Mat(Eclipse专门的静态内存分析工具)推荐使用。
   注:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。

4、分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
   注:如果满足下面的指标,则一般不需要进行GC:
● Minor GC执行时间不到50ms;
● Minor GC执行不频繁,约10秒一次;
● Full GC执行时间不到1s;
● Full GC执行频率不算频繁,不低于10分钟1次;

5、调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

6、不断分析和调整

通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。
在这里插入图片描述

JVM调优参数参考

1、针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;
2、年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。
3、年轻代和年老代设置多大才算合理
● 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的耗时;小的年老代会导致更频繁的Full GC
● 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
   如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。
   在抉择时应该根据以下两点:
   (1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。
(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。
4、在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC 。
5、线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。
   理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

深拷贝和浅拷贝

浅拷贝指的是把原对象的所有属性都拷贝到新对象上去,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其新对象引用同一个对象。
深拷贝指的是把原对象的所有属性都复制一份新的再拷贝到新对象上,也就是无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
实现方式
浅拷贝通过clone()方法实现,clone属于Object里的方法,也就是说所有对象都可以拷贝,类必须实现Cloneable接口,否则会抛出CloneNotSupportedException 异常
让每个引用类型都重写clone() 方法
既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Father类有一个引用类型 Son,我们在 Son类内部也重写 clone 方法。如下:
序列化
是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里将对象序列化写到缓冲区中,然后通过反序列化从缓冲区中获取这个对象。
注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值