jvm内存

Posted wwmiert

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm内存相关的知识,希望对你有一定的参考价值。

【阅读模式答题】

答题:对于JVM的理解,在我看来有两个重要部分:

 技术图片

简单介绍一下,整个JVM占用的内存可分为两个大区,分别是线程共享区和线程私有区,线程共享区和JVM同生共死,所有线程均可访问此区域;而线程私有区顾名思义每个线程各自占有,与各自线程同生共死。这两个大区内部根据JVM规范定义又分为以下几个区:

方法区(Method Area)
方法区主要是放一下类似类定义、常量、编译后的代码、静态变量等,在JDK1.7中,HotSpot VM的实现就是将其放在永久代中,这样的好处就是可以直接使用堆中的GC算法来进行管理,但坏处就是经常会出现内存溢出,即PermGen Space异常,所以在JDK1.8中,HotSpot VM取消了永久代,用元空间取而代之,元空间直接使用本地内存,理论上电脑有多少内存它就可以使用多少内存,所以不会再出现PermGen Space异常。

堆(Heap)
几乎所有对象、数组等都是在此分配内存的,在JVM内存中占的比例也是极大的,也是GC垃圾回收的主要阵地,平时我们说的什么新生代、老年代、永久代也是指的这片区域,至于为什么要进行分代后面会解释。

虚拟机栈(Java Stack)
当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等,方法开始执行前就先创建栈帧入栈,执行完后就出栈。

本地方法栈(Native Method Stack)
和虚拟机栈类似,不过区别是专门提供给Native方法用的。

程序计数器(Program Counter Register)
占用很小的一片区域,我们知道JVM执行代码是一行一行执行字节码,所以需要一个计数器来记录当前执行的行数。

2、堆内存
堆内存是JVM内存中占用较大的一块区域,对象都在此地分配内存。在堆中,又分为新生代及老年代,新生代中又分三个区域,分别是Eden,Survivor To,Survivor From。堆内存是JVM调优的重点区域,也是这篇博客重点讨论的内容。

3、提出问题
可能看到这里我们都会产生这样一个经典问题

为何堆内存要进行分代?

最简单的回收方式(标记-回收算法)
假设堆内存不进行分代,那么垃圾回收应该如何进行呢?我们可以大胆想象一下,在一大片内存空间中我们分配了若干个对象(假设图中一个黑色方块代表一个字节,分配的对象均占用两个字节)
技术图片

 


此时堆内存满了,是时候来一波垃圾回收操作了,通过某种分析算法,我们分析到某几个对象是需要进行回收(下述此类对象称之为回收对象),我们让无用对象就地清除,回收后的结果如下:
技术图片

 


我们可以看到,回收后的内存支离破碎的,虽然现在还有八个字节的内存空间,但只要有三个字节或以上的对象需要申请内存,那么这片支离破碎依旧无法为其分配内存,因为没有连续的空间。

第一次演化(复制算法)
既然我们没法回收出连续的空间,那我们可以从一开始就把内存分两个大区,平时只用其中一个区,如下图
技术图片

 


当左边内存区满时,就开始一波回收操作,找到那些无需回收的对象(下述称此类对象为存活对象),将它们工工整整地复制到右边的区域中,接着将左边的区域来次大清理,清理后的结果如下:
技术图片

 


这样当需要再分配内存给对象时,就使用右边的区域,而右边的区域此时也有8个字节的连续空间供分配,当右边满了,再如法炮制,将存活对象复制到左边再将右边回收。貌似这样就解决了问题了,但是总感觉有什么地方不对,是的没错,每次只使用一半的内存,未免也太浪费了!

第二次演化(标记-压缩算法)
我们依旧不分区,将整个内存用满, 开始回收垃圾时,我们将存活对象全部移动到左边,然后对边界外的内存进行清理,如下图
技术图片

 


这算是一种折中的做法,起码比第一次演化中的做法更加充分使用内存,也比最开始的做法的空闲内存更加连续。
在这里我们可以再往下思考,在第一次演化中,如果每次回收对象特别多,而存活对象特别少,那么只需要通过少数的复制操作和一次清除就可以实现回收,此时效率会特别高。而在第二次演化中,如果每次回收对象较少,而存活对象较多,则可以采取此策略进行回收确保最终剩余的空间是连续的空间。
到这里其实并不足够完善,毕竟上述几种演化都有缺点和优点,有没有办法可以取长补短呢?
在开发中,其实我们可以发现,大多数对象都是在方法体中new出来,new完使用后就不再使用了,此时该对象即可进行回收。所以这一类的对象有个特点就是朝生夕死。假如在方法执行完,该对象的引用还被持有着,证明该对象是比较重要的对象,越到后面要回收则越来越困难。这个情况不就刚好符合上述两种演化的情况,当对象刚出生时,我们可以将其使用演化一的方式进行回收,当使用演化一的方式回收不了的对象,则证明该对象为比较重要的对象,我们就可以采用演化二的方式进行回收。这样我们可以对我们的内存进行分区

第一次分区

当new对象时,内存均从上图右上方的区域申请,当右上方的内存区域满时,则进行一次复制算法进行垃圾回收。从上面的思考我们知道,绝大多数新对象都有朝生夕死的特点,所以在这次的垃圾回收中,存活的对象寥寥无几,然后存活的对象全部塞到右下方区域。在下一次垃圾回收到来时,根据上述分析,之前存活的对象绝大多数还会继续存活,我们将经历过一次垃圾回收的对象年龄+1,可见大多数的对象都熬不过两岁,一般在一岁时就被回收了。而当对象经历了多次垃圾回收仍然存活,此时它很难被回收了,我们可以将其移到左边的区域,另外右边上下俩区域都满了时,则通过垃圾回收将存活对象的那一边区域也移动到左边区域中。当左边区域满时,可通过标记-压缩算法进行垃圾回收。在这种分区方式中,左侧区域称之为老年代,而右侧区域则为新生代。新生代使用复制算法进行一次垃圾回收,称之为Minor GC,而复制完后如果老年代区域不够,也会触发老年代使用标记-压缩算法进行垃圾回收,称之为Major GC,一般Major GC会伴随着Minor GC,所以也称为Full GC。
在上述分区中,新生代仍然只有一半的区域可以用,之前使用一半区域的原因是考虑到有可能所有对象都是存活对象,这样才足够完全复制,但现在有老年代的存在,再考虑到此区域每一次回收时仅有少数对象需要复制,分区方式是否还有优化的空间呢?

第二次分区

这个分区是在第一次分区的基础上,将新生代分为三部分,分别是伊甸园、幸存区S0,幸存区S1,伊甸园内存占比为8:1:1,S0与S1大小相同。对象的一生如下:
①所有对象都在伊甸园出生,当伊甸园占满时,开始进行一次Minor GC,此次GC会将已存活的对象复制到S0区中
②伊甸园区又被占满,此时又进行一次Minor GC,伊甸园存活的对象又复制到S0区。
③在若干次GC后,幸存区S0也满了,此时Minor GC会对伊甸园和幸存区S0的
做一次垃圾回收,将两个区存活的对象复制到幸存区S1中,再把伊甸园和S0清空,最后把S1的内存与S0交换,此时S1又腾空了,S0剩下一些老对象。
④又经历若干次GC,幸存区S0已经放满了经历过N次GC都回收不了的老对象,此时会将老对象复制到老年代中,腾空幸存区。
⑤并非当幸存区被老对象占满才复制到老年代中,当老对象年龄达到15岁,即经历过15次GC都还活着的,也会复制到老年代中,另外伊甸园中如果诞生了一个比幸存区还大的对象,那么该对象回收不了时,也会直接送入到老年代中。
⑥又经历过若干次GC后,老年代也满了,那么此时它会进行一次Major GC。

动图演示
上述过程使用文字描述可能比较抽象,下面用动图简单来演示一次Minor GC。


二、垃圾回收涉及的算法
在垃圾回收中,涉及的算法主要有以下五个

引用计数算法
可达性分析
标记-回收算法
标记-压缩算法
复制算法
前两个算法用于判断对象是否需要回收,其原理简单讲,引用计数算法就是计算对象被谁引用,一旦有其它对象引用此对象,引用次数加一,而GC时引用次数大于零的对象则判断为存活对象,但此算法无法解决循环引用问题,如A引用B,B引用A,此时A与B均无法回收,所以现在JVM不采用此算法;而可达性算法则从GCRoot出发,若A引用B,B引用C,则通过A可以到达C,此时ABC三个对象均不进行回收。后面的三个算法为回收策略,其思路在第一章有提及,在这里就不加赘述,下面总结一下三个算法优缺点:
算法 优点 缺点
标记-回收算法 暂无 标记和清除效率低、可用空间不连续
标记-压缩算法 实现简单,运行高效 内存空间利用不充分
复制算法 内存空间利用率高 性能较低
三、JVM常用参数
Xss:每个线程的栈大小
Xms:堆空间的初始值
Xmx:堆空间最大值、默认为物理内存的1/4,一般Xms与Xmx最好一样
Xmn:年轻代的大小
XX:NewRatio :新生代和年老代的比例
XX:SurvivorRatio :伊甸园区和幸存区的占用比例
XX:PermSize:设定内存的永久保存区域(1.8已废除)
XX:MetaspaceSize:1.8使用此参数替代上述参数
XX:MaxPermSize:设定最大内存的永久保存区域(1.8已废除)
XX:MaxMetaspaceSize:1.8使用此参数替代上述参数
四、附录-演示代码
此代码为第一章中动图所使用的测试代码

public class OutOfMemoryErrorTest
public static void main(String[] args) throws Throwable
Random r = new Random();
List<TestObject[]> testObjectList = new ArrayList<TestObject[]>();
while (true)
try
TestObject[] testObjects = new TestObject[2048];
// 模拟30%左右的对象为有用对象
if (r.nextInt(10) > 4)
testObjectList.add(testObjects);

Thread.sleep(1);
catch (Throwable t)
throw t;



class TestObject

private String name;

private int age;

public String getName()
return name;

public void setName(String name)
this.name = name;

public int getAge()
return age;

public void setAge(int age)
this.age = age;


---------------------

原文:https://blog.csdn.net/qq_22152261/article/details/79491536

第一是Java代码编译和执行的整个过程:

开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

Java代码编译和执行的整个过程包含了以下三个重要的机制:

1)Java源码编译机制:Java代码编译是由Java源码编译器来完成,也就是Java代码到JVM字节码(.class文件)的过程

2)类加载机制:JVM的类加载是通过ClassLoader及其子类来完成的

3)类执行机制:JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。

JVM执行class字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,

局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。

 

第二是JVM内存管理及垃圾回收机制:

JVM内存结构分为:方法区(method),栈内存(stack),堆内存(heap),本地方法栈(java中的jni调用)。

方法区——存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(Permanet Generation)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。

栈内存——在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。  

堆内存——堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。】

堆里聚集了所有由应用程序创建的对象,JVM也有对应的指令比如 new, newarray, anewarray和multianewarray,然并没有向 C++ 的 delete,free 等释放空间的指令,

回答这部分的点可以有:

Java的所有释放都由 GC 来做,GC除了做回收内存之外,另外一个重要的工作就是内存的压缩。

4)GC怎么判断要回收的垃圾对象

5)如何减少GC开销

6)垃圾回收算法

(见文末)

 

==============================20170728更新===================================

1)Java是一种技术,它由四方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。

 

2)运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后java源码编译器将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。

 

3)Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 

就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。 

 

4)JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。

 

5)Java代码的编译和执行的整个过程大概是:开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行(jvm的执行引擎执行)。


技术图片
 

 

 虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:

Java代码  技术图片
  1. public class HelloApp   
  2.   
  3.     public static void main(String[] args)   
  4.         System.out.println("Hello World");  
  5.       
  6.   

 jvm一开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。

类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。

类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下:


技术图片
 

 

=========================== 分割线================================= 

【JVM优化】

jvm 优化问题

 

JVM堆内存分为2块:Permanent Space 和 Heap Space。

 

    Permanent 即 持久代(Permanent Generation),主要存放的是Java类定义信息,与垃圾收集器要收集的Java对象关系不大。

    Heap = Old + NEW = Eden, from, to ,Old 即 年老代(Old Generation),New 即 年轻代(Young Generation)。年老代和年轻代的划分对垃圾收集影响比较大。

 

我们知道Java和C++的区别主要是,Java不需要像c++那样,由程序员主动的释放内存。而是由JVM里的GC(GarbageCollection)来,在适当的时候替我们释放内存。JVM GC调整优化的内部工作,即JVM GC的算法有很多种,如:标记清除收集器,压缩收集器,分代收集器等等。现在比较常用的是分代收集(也是SUNVM使用的),即将内存分为几个区域,将不同生命周期的对象放在不同区域里(新的对象会先生成在Youngarea,在几次GC以后,如果没有收集到,就会逐渐升级到Tenuredarea)。在JVM GC收集的时候,频繁收集生命周期短的区域(Youngarea),因为这个区域内的对象生命周期比较短,GC效率也会比较高。而比较少的收集生命周期比较长的区域(OldareaorTenuredarea),以及基本不收集的永久区(Permarea)。

 

优化堆大小的设置。如果堆设置较大,可能导致 GC 的次数变少,但每次 GC 所花的时间很长,从而导致系统的处理能力抖动很大。此外如果堆设置过大,会占用过多的内存,使内存资源耗尽,从而会频繁的进行 IO 操作来使用虚拟内存。 如果堆设置较小,可能导致 GC 变的频繁,但每次 GC 所花的时间不会太长,每次 GC 对系统的性能影响相对也会小些。但是如果堆设置过小, 会使得对象可分配空间变小,从而会频繁的 GC 来释放内存空间,而每次 GC,都会耗用一定的系统资源。因此,要通过试验和监控数据,设法使的我们所设置的堆大小能够使得系统运行最优化。

 

众所周知,Java是从C++的基础上发展而来的,而C++程序的很大的一个问题就是内存泄露难以解决,尽管Java的JVM有一套自己的垃圾回收机制来回收内存,在许多情况下并不需要java程序开发人员操太多的心,但也是存在泄露问题的,只是比C++小一点。比如说,程序中存在被引用但无用的对象:程序引用了该对象,但后续不会或者不能再使用它,那么它占用的内存空间就浪费了。

 

我们先来看看GC是如何工作的:监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,当该对象不再被引用时,释放对象(GC本文的重点,不做过多阐述)。很多Java程序员过分依赖GC,但问题的关键是无论JVM的垃圾回收机制做得多好,内存总归是有限的资源,因此就算GC会为我们完成了大部分的垃圾回收,但适当地注意编码过程中的内存优化还是很必要的。这样可以有效的减少GC次数,同时提升内存利用率,最大限度地提高程序的效率。

 

优化程序代码:

 

1.尽早释放无用对象的引用(XX = null;)   

2.谨慎使用集合数据类型,如数组,树,图,链表等数据结构,这些数据结构对GC来说回收更复杂。

3.避免显式申请数组空间,不得不显式申请时,尽量准确估计其合理值。

4.尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其自类的构造器时造成不必要的内存资源浪费

5.尽量避免强制系统做垃圾内存的回收,增长系统做垃圾回收的最终时间

6.尽量做远程方法调用类应用开发时使用瞬间值变量,除非远程调用端需要获取该瞬间值变量的值。

7.尽量在合适的场景下使用对象池技术以提高系统性能

 

总体而言,Java虚拟机的内存优化应从两方面着手:Java虚拟机和Java应用程序。前者指根据应用程序的设计通过虚拟机参数控制虚拟机逻辑内存分区的大小以使虚拟机的内存与程序对内存的需求相得益彰;后者指优化程序算法,降低GC负担,提高GC回收成功率。

 =========================== 分割线================================= 

 

这部分也是比较常考查的~ 上文有讲过一点,这篇将会以比较全面地以代码运行的角度去理解类加载器加载类的顺序~

 

1、先用代码来看结果

先单独看一个类:

 

Java代码  技术图片
  1. class Parent   
  2.       
  3.         System.out.println("Parent的普通代码块");  
  4.       
  5.       
  6.     static   
  7.         System.out.println("Parent的静态代码块");  
  8.       
  9.       
  10.     public Parent()   
  11.         System.out.println("Parent的构造代码块");  
  12.       
  13.       
  14.     public static void staticMethod1()   
  15.         System.out.println("Parent的静态代方法");  
  16.       
  17.       
  18.     public static void staticMethod2()   
  19.         System.out.println("Parent的静态代方法2");  
  20.       
  21.   
  22.     @Override  
  23.     protected void finalize() throws Throwable   
  24.         super.finalize();  
  25.         System.out.println("销毁Parent类");  
  26.       
  27.       
  28.   

 在另一个类里调用:

 

 

Java代码  技术图片
  1. Parent.staticMethod1();  
  2. Parent.staticMethod2();  
  3. System.out.println();  
  4. Parent parent  = new Parent();  

 调用结果:

 

Parent的静态代码块

Parent的静态代方法

Parent的静态代方法2

 

Parent的普通代码块

Parent的构造代码块

 

说明:类中static 代码块在第一次调用时加载,类中static成员按在类中出现的顺序加载。普通代码块和构造代码块是在类初始化以后才会被执行。

 

下面再结合继承来看~

 

Java代码  技术图片
  1. class Child extends Parent   
  2.       
  3.         System.out.println("Child的普通代码块");  
  4.       
  5.       
  6.     static   
  7.         System.out.println("Child的静态代码块");  
  8.       
  9.       
  10.     public Child()   
  11.         System.out.println("Child的构造代码块");  
  12.       
  13.       
  14.     public static void staticMethod1()   
  15.         System.out.println("Child的静态代方法");  
  16.       
  17.       
  18.     public static void staticMethod2()   
  19.         System.out.println("Child的静态代方法2");  
  20.       
  21.   
  22.     @Override  
  23.     protected void finalize() throws Throwable   
  24.         super.finalize();  
  25.         System.out.println("销毁Child类");  
  26.       
  27.       
  28.   

 在另一个类调用:

 

 

Java代码  技术图片
  1. Child child = new Child();  

 调用结果:

 

Parent的静态代码块

Child的静态代码块

Parent的普通代码块

Parent的构造代码块

Child的普通代码块

Child的构造代码块

 

说明:如果一个类继承自一个父类,那么初始化它的时候,将会:父类的静态代码块--> 子类的静态代码块 -->父类的普通代码块 -->父类的默认构造器 -->子类的普通代码块 -->子类的构造器

 

如果承接上面,初始化子类后再调用销毁方法:

 

Java代码  技术图片
  1. try   
  2.             child.finalize();  
  3.          catch (Throwable e)   
  4.             e.printStackTrace();  
  5.           

 那么打印结果将会多上:

 

销毁Parent类

销毁Child类

 

说明:销毁的时候,将先调用父类的销毁方法finalize()再调用子类的finalize()。

 

2、再用理论来看原因——JVM工作原理

一个Java类的生命周期:

加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载 

 

(1)加载 

  首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。 

(2)链接: 

  验证:确保被加载类的正确性; 

  准备:为类的静态变量分配内存,并将其初始化为默认值; 

  解析:把类中的符号引用转换为直接引用; 

(3)为类的静态变量赋予正确的初始值 

3、类的初始化 

(1)类什么时候才被初始化 (类初始化不一定是通过new)

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

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

  3)调用类的静态方法 

  4)反射(Class.forName(“com.lyj.load”)) 

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

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

(2)类的初始化顺序 

  1)如果这个类还没有被加载和链接,那先进行加载和链接 

  2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口) 

  3)假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。 

  4)总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法 

4、类的加载 

 

  类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。如: 



 类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。加载类的方式有以下几种: 

  1)从本地系统直接加载 

  2)通过网络下载.class文件 

  3)从zip,jar等归档文件中加载.class文件 

  4)从专有数据库中提取.class文件 

  5)将Java源文件动态编译为.class文件(服务器) 

5、加载器 

  JVM的类加载是通过ClassLoader及其子类来完成的

(2)类加载器的顺序 

1)加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。 

2)在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。 

3)Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。

 

3、Java堆栈和方法区

1)栈

   在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。  

   当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

   每个线程包含一个栈区,每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

   栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享(指的是线程共享,而给进程共享)。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

   栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。

 

2)堆

   堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或对象后,在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。  引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是java比较占内存的原因。实际上,栈中的变量指向堆内存中的变量,这就是java中的指针! 

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

   jvm只有一个堆区(heap)被所有线程共享。

 

3)方法区

   方法区跟堆一样,被所有的线程共享。用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

 

4)常量池

   常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名; 字段的名称和描述符; 方法和名称和描述符。虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在方法区(Method Area),而不是堆中。

 

 4、JVM回收

 

1)GC什么时候回回收?

 对象没有引用

 

 作用域发生未捕获异常

 

 程序在作用域正常执行完毕

 

 程序执行了System.exit()

 

 

 程序发生意外终止(被杀进程等)

 

2)如何减少GC开销?

不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。

 

尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

 

对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

 

尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

 

能用基本类型如Int,long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

 

尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

 

分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

 

3)JVM垃圾回收算法

标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,将复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。
标记-整理算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让索引端移动,然后直接清理掉端边界以外的内存。
分代收集算法:一般是把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。新生代都发现有大批对象死去,选用复制算法。老年代中因为对象存活率高,必须使用“标记-清理”或“标记-整理”算法来进行回收。
内存地址考查题:
Java代码  技术图片
  1. public class B   
  2.   
  3.     public static void main(String[] args)   
  4.         B b = new B();  
  5.         int date = 9;// 局部变量,基础类型,引用和值都存在栈中,值传递不改变值。  
  6.         b.change1(date);  
  7.         System.out.println(date);  
  8.         String str = "gbk";//值传递不改变值,引用传递才改变  
  9.         int[] a = 1,2,3;  
  10.         b.change4(str, a);  
  11.         System.out.println(str + "," + Arrays.toString(a));  
  12.         System.out.println();  
  13.         BirthDate d1 = new BirthDate(10, 2, 1991);  
  14.         b.change2(d1);  
  15.         System.out.println(d1.toString2());  
  16.         System.out.println();  
  17.         BirthDate d2 = new BirthDate(10, 12, 1992);  
  18.         b.change3(d2);  
  19.         System.out.println(d2.toString2());  
  20.       
  21.       
  22.     public void change1(int i)   
  23.         i = 1234;  
  24.       
  25.       
  26.     public void change2(BirthDate d)   
  27.         System.out.println("change前内存地址:" + d);  
  28.         d = new BirthDate(22,2,2004);  
  29.         //因为有new,局部变量d的内存地址会发生改变,但因为局部变量在方法执行完毕后,会从栈中消失  
  30.         System.out.println("change后内存地址:" + d);  
  31.       
  32.       
  33.     public void change3(BirthDate d)   
  34.         System.out.println("change前内存地址:" + d);  
  35.         //没有new, d的内存地址就没变  
  36.         d.setDay(22);  
  37.         System.out.println("change后内存地址:" + d);  
  38.       
  39.     public void change4(String str, int[] a)   
  40.         str += "ok";  
  41.         a[0]=9;  
  42. //      a = new int[]10,9,8;  
  43.       
  44.   
  45.   
  46. class BirthDate   
  47.     private int day;  
  48.     private int month;  
  49.     private int year;  
  50.     public BirthDate(int day, int month, int year)   
  51.         this.day = day;  
  52.         this.month = month;  
  53.         this.year = year;  
  54.       
  55.     public int getDay()   
  56.         return day;  
  57.       
  58.     public void setDay(int day)   
  59.         this.day = day;  
  60.       
  61.     public int getMonth()   
  62.         return month;  
  63.       
  64.     public void setMonth(int month)   
  65.         this.month = month;  
  66.       
  67.     public int getYear()   
  68.         return year;  
  69.       
  70.     public void setYear(int year)   
  71.         this.year = year;  
  72.       
  73.     public String toString2()   
  74.         return "BirthDate [day=" + day + ", month=" + month + ", year=" + year  
  75.                 + "]";  
  76.       
  77.   
 运行结果:
9
gbk,[9, 2, 3]
 
change前内存地址:[email protected]
change后内存地址:[email protected]
BirthDate [day=10, month=2, year=1991]
 
change前内存地址:[email protected]
change后内存地址:[email protected]
BirthDate [day=22, month=12, year=1992]

以上是关于jvm内存的主要内容,如果未能解决你的问题,请参考以下文章

JVM内存管理和JVM垃圾回收机制

深入理解JVM之JVM内存区域与内存分配

深入理解JVM之JVM内存区域与内存分配

jvm学习一|jvm内存区域以及内存溢出

JVM:JVM内存模型

java jvm内存可以设置多少