Weitere ähnliche Inhalte
Ähnlich wie JVM内容管理和垃圾回收 (20)
JVM内容管理和垃圾回收
- 1. 技术中心 : 罗伟峰 2009-10-22 JVM 内存管理和垃圾回收
Hinweis der Redaktion
- “ 重新排序”这个术语用于描述几种对内存操作的真实明显的重新排序的类型:
- jmm 怎么体现 可视性 (Visibility) ? 在 jmm 中 , 通过并发线程修改变量值 , 必须将线程变量同步回主存后 , 其他线程才能访问到 . jmm 怎么体现 有序性 (Ordering) ? 通过 java 提供的同步机制或 volatile 关键字 , 来保证内存的访问顺序 . 注意 : 其中 4,5 两步是同时进行的 . 这边最核心的就是第二步 , 他同步了主内存 , 即前一个线程对变量改动的结果 , 可以被当前线程获知 !( 利用了 happens-before ordering 原则 ) 对比之前的例子 如果多个线程同时执行一段未经锁保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。
- 栈有一个很重要的特殊性,就是存在栈中的数据可以共享。 假设我们同时定义: int a = 3; int b = 3 ; 编译器先处理 int a = 3 ;首先它会在栈中创建一个变量为 a 的引用,然后查找栈中是否有 3 这个值,如果没找到,就将 3 存放进来,然后将 a 指向 3 。 接着处理 int b = 3 ;在创建完 b 的引用变量后,因为在栈中已经有 3 这个值,便将 b 直接指向 3 。这样,就出现了 a 与 b 同时均指向 3 的情况。 这时,如果再令 a=4 ;那么编译器会重新搜索栈中是否有 4 值,如果没有,则将 4 存放进来,并令 a 指向 4 ; 如果已经有了,则直接将 a 指向这个地址。因此 a 值的改变不会影响到 b 的值。 要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况 a 的修改并不会影响到 b, 它是由编译器完成的,它有利于节省空间。 而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。 关于 String str = "abc" 的内部工作。 Java 内部将此语句转化为以下几个步骤: (1) 先定义一个名为 str 的对 String 类的对象引用变量: String str ; (2) 在栈中查找有没有存放值为 "abc" 的地址,如果没有,则开辟一个存放字面值为 "abc" 的地址,接着创建一个新的 String 类的对象 o ,并将 o 的字符串值指向这个地址, 而且在栈中这个地址旁边记下这个引用的对象 o 。 如果已经有了值为 "abc" 的地址,则查找对象 o ,并返回 o 的地址。 (3) 将 str 指向对象 o 的地址。 值得注意的是,一般 String 类中字符串值都是直接存值的。 但像 String str = "abc" ;这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 例子说明: String str1 = "abc"; String str2 = "abc"; System.out.println(str1==str2); //true 我们这里并不用 str1.equals(str2) ;的方式,因为这将比较两个字符串的值是否相等。 == 号,只有在两个引用都指向了同一个对象时才返回真值。 而我们在这里要看的是, str1 与 str2 是否都指向了同一个对象。 结果说明, JVM 创建了两个引用 str1 和 str2 ,但只创建了一个对象,而且两个引用都指向了这个对象。 下一个例子: String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; System.out.println(str1 + "," + str2); //bcd, abc System.out.println(str1==str2); //false 这就是说,赋值的变化导致了类对象引用的变化, str1 指向了另外一个新对象!而 str2 仍旧指向原来的对象。上例中,当我们将 str1 的值改为 "bcd" 时, JVM 发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。 事实上, String 类被设计成为不可改变 (immutable) 的类。 如果你要改变其值,可以,但 JVM 在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。 这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。 修改代码 : String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; String str3 = str1; System.out.println(str3); //bcd String str4 = "bcd"; System.out.println(str1 == str4); //true str3 这个对象的引用直接指向 str1 所指向的对象 ( 注意, str3 并没有创建新对象 ) 。当 str1 改完其值后,再创建一个 String 的引用 str4 , 并指向因 str1 修改值而创建的新的对象。 可以发现,这回 str4 也没有创建新的对象,从而再次实现栈中数据的共享。 继续修改代码 : String str1 = new String("abc"); String str2 = "abc"; System.out.println(str1==str2); //false 创建了两个引用。 创建了两个对象。两个引用分别指向不同的两个对象。 以上两段代码说明,只要是用 new() 来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。 总结 : (1) 我们在使用诸如 String str = "abc" ;的格式定义类时, String 类的 str 对象可能并没有被创建!唯一可以肯定的是,指向 String 类的引用被创建了。 因此,更为准确的说法是,我们创建了一个指向 String 类的对象的引用变量 str ,这个对象引用变量指向了某个值为 "abc" 的 String 类。 至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过 new() 方法来显要地创建一个新的对象。 (2) 使用 String str = "abc" ;的方式,可以在一定程度上提高程序的运行速度, 因为 JVM 会自动根据栈中数据的实际情况来决定是否有必要创建新对象。 而对于 String str = new String("abc") ;的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。
- 过程如下: 当一个 class 文件被 ClassLoader load 进入 JVM 后,方法指令保存在 stack 中,此时 heap 区没有数据。然后程序计数器开始执行指令, 如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问 heap 数据区的; 如果是非静态方法,由于隐含参数没有值,会报错。 因此在非静态方法执行前,要先 new 对象,在 heap 中分配数据,并把 stack 中的地址指针交给非静态方法,这样程序计数器依次执行指令, 而指令代码此时能够访问到 heap 数据区了。
- Java 程序中使用的绝大多数资源都是对象,垃圾收集在清理对象方面做得很好。因此,您可以使用任意多的 String 。垃圾收集器最终无需您的干预就会算出它们何时失效,并收回它们使用的内存。
- 垃圾收集为我们做了大量可怕的资源清除工作,但是有些资源仍然需要显式的释放,比如文件句柄、套接字句柄、线程、数据库连接和信号量许可证。当资源的生命周期被绑定到特定调用帧的生命周期时,我们通常可以使用 finally 块来释放该资源,但是长期存活的资源需要一种策略来确保它们最终被释放。对于任何一个这样的对象,即它直接或间接拥有一个需要显式释放的对象,您必须提供生命周期方法 —— 比如 close() 、 release() 、 destroy() 等 —— 来确保可靠的清除。
- 应用组合 对于应用来说,可分配的内存受到 OS 的限制,不同的 OS 对进程所能访问虚拟内存地址区间直接影响对于应用内存的分配, 32 位的操作系统通常最大支持 4G 的内 存寻址,而 Linux 一般为 3G , Windows 为 2G 。 然而这些大小的内存并不会全部给 JVM 的 Java Heap 使用,它主要会分成三部分: Java Heap , Native Heap ,载入资源和类库等所占用的内存。 那么由此可见, Native Heap 和 Java Heap 大小配置是相互制约的,哪一部分分配多了都可能会影响到另外一部分的正常工作,因此如果通过命令行去配置, 那么需要确切的了解应用使用情况,否则 采用默认配置自动监测会更好的优化应用使用情况。 同样要注意的就是进程的虚拟内存和机器的实际内存还是有区别的,对于机器来说实际内存以及硬盘提供的虚拟内存都是提供给机器上所有进程使用的, 因此在设置 JVM 参数时,它的虚拟内存绝对不应该超过实际内存的大小。
- sun 公司的性能优化白皮书中提到的几个例子: 1 .对于吞吐量的调优。机器配置: 4G 的内存, 32 个线程并发能力。 java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -Xmx3800m -Xms3800m 配置了最大 Java Heap 来充分利用系统内存。 -Xmn2g 创建足够大的青年代(可以并行被回收)充分利用系统内存,防止将短期对象复制到老年代。 -Xss128 减少默认最大的线程栈大小,提供更多的处理虚拟内存地址空间被进程使用。 -XX:+UseParallelGC 采用并行垃圾收集器对年青代的内存进行收集,提高效率。 -XX:ParallelGCThreads=20 减少垃圾收集线程,默认是和服务器可支持的线程最大并发数相同,往往不需要配置到最大值。 2 .尝试采用对老年代并行收集 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -Xmx3550m -Xms3550m 内存分配被减小,因为 ParallelOldGC 会增加对于 Native Heap 的需求,因此需要减小 Java Heap 来满足需求。 -XX:+UseParallelOldGC 采用对于老年代并发收集的策略,可以提高收集效率。 3 .提高吞吐量,减少应用停顿时间 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=90 -XX:MaxTenuringThreshold=31 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 选择了并发标记交换收集器,它可以并发执行收集操作,降低应用停止时间,同时它也是并行处理模式,可以有效地利用多处理器的系统的多进程处理。 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=31 表示在青年代中 Eden 和 Survivor 比例,设置增加了 Survivor 的大小,越大的 survivor 空间可以允许短期对象尽量在年青代消亡。 -XX:TargetSurvivorRatio=90 允许 90% 的空间被占用,超过默认的 50% ,提高对于 survivor 的使用率。