잊지 않겠습니다.

최신 Gradle 2.0으로 업데이트 한 내용이 존재합니다.


전에 정리한 적이 있는데, 조금 글을 다듬고 정리할 필요성이 있어서 다시 옮깁니다.
먼저, 개발되는 Project는 다음과 같은 구조를 갖습니다.

rootProject
-- domainSubProject (Spring Data JPA + queryDSL)
-- webAppSubProject (Spring Web MVC)

위와 같은 Project는 개발의 편의를 위해서 다음 두 조건을 만족해야지 됩니다.

  1. queryDSL을 사용하기 위한 Q-Entity를 생성이 compileJava task전에 수행되어야지 됩니다.
  2. web application 개발을 위해, tomcat을 실행시킬 수 있어야합니다.
  3. 개발된 web application을 test 환경 또는 product 환경에 배포가 가능해야지 됩니다.

root project

root project는 sub project들의 공통 설정이 필요합니다.

  1. build시에 필요한 plugin의 repository를 설정합니다.
  2. sub project들에서 사용될 공통 dependency들을 설정합니다. (sub project들의 build.gradle 이 너무 길어지는 것을 막을 수 있습니다.)
공통 plugin 추가
  • maven의 POM과 동일한 provided과 optional의 사용을 위해 spring prop plugin을 추가합니다.
  • subprojects들에 필요한 plugin들을 모두 추가합니다. (저는 java와 groovy, idea, eclipse 등을 추가했습니다.)
  • source java version과 target java version을 정해줍니다.
  • source code의 Encoding을 정해줍니다.
  • SubProject에서 기본적으로 사용될 dependency들을 모두 적어줍니다.

build.gradle (root project)

apply plugin: 'base'

// spring prop plugin 추가
buildscript {
    repositories {
        maven { url 'http://repo.springsource.org/plugins-release' }
    }
    dependencies {
        classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.5'
    }
}

subprojects {
    // Plugin 설정, 만약에 code에 대한 static analysis가 필요한 경우에 이곳에 설정.
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'
    apply plugin: 'groovy'
    apply plugin: 'propdeps'
    apply plugin: 'propdeps-maven'
    apply plugin: 'propdeps-idea'
    apply plugin: 'propdeps-eclipse'

    // 기본적으로 사용할 repository들을 정의
    repositories {
        mavenCentral()
        maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "http://192.168.13.209:8080/nexus/content/repositories/releases" }
    }

    dependencies {
        def springVersion = "4.0.1.RELEASE"

        compile 'org.slf4j:slf4j-api:1.7.5'
        compile "org.springframework:spring-context:${springVersion}"
        compile "org.springframework:spring-aspects:${springVersion}"
        compile "org.springframework:spring-jdbc:${springVersion}"
        compile "org.springframework:spring-context-support:${springVersion}"

        compile 'mysql:mysql-connector-java:5.1.27'
        compile 'com.jolbox:bonecp:0.8.0.RELEASE'
        compile 'com.google.guava:guava:15.0'
        compile 'org.aspectj:aspectjrt:1.7.4'
        compile 'org.aspectj:aspectjtools:1.7.4'
        compile 'org.aspectj:aspectjweaver:1.7.4'

        testCompile 'org.springframework:spring-test:4.0.0.RELEASE'
        testCompile "junit:junit:4.11"

        groovy "org.codehaus.groovy:groovy-all:2.1.6"
        testCompile "org.spockframework:spock-core:1.0-groovy-2.0-SNAPSHOT"
        testCompile "org.spockframework:spock-spring:1.0-groovy-2.0-SNAPSHOT"
    }

    // source, target compiler version 결정
    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    // source code encoding
    [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
}
domain project
  • domain project는 queryDsl의 Q-Entity들의 생성이 필요합니다.
  • compileJava에 대한 Q-Entity 생성 Task의 Depend가 잡혀있으면 사용하기 편합니다.

build.gradle (domain project)

dependencies {
    def springVersion = "4.0.1.RELEASE"

    compile 'org.hibernate:hibernate-core:4.3.1.Final'
    compile 'org.hibernate:hibernate-entitymanager:4.3.1.Final'
    compile "org.springframework:spring-orm:${springVersion}"

    def queryDSL = '3.2.4'
    compile("com.mysema.querydsl:querydsl-core:$queryDSL")
    compile("com.mysema.querydsl:querydsl-jpa:$queryDSL")
    compile("com.mysema.querydsl:querydsl-sql:$queryDSL")
    provided("com.mysema.querydsl:querydsl-apt:$queryDSL") {
        exclude group: 'com.google.guava'
    }
    compile 'org.springframework.data:spring-data-jpa:1.5.0.RELEASE'
}

sourceSets {
    generated {
        java {
            srcDirs = ['src/main/generated']
        }
    }
}

// QEntity 생성 task
task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
    source = sourceSets.main.java
    classpath = configurations.compile + configurations.provided
    options.compilerArgs = [
            "-proc:only",
            "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

// compileJava task에 dependency를 걸어줍니다.
compileJava {
    dependsOn generateQueryDSL
    // compile target에 generated된 QClass들의 위치를 추가.
    source sourceSets.generated.java.srcDirs.iterator().next()
}

compileGeneratedJava {
    dependsOn generateQueryDSL
    options.warnings = false
    classpath += sourceSets.main.runtimeClasspath
}

clean {
    delete sourceSets.generated.java.srcDirs
}

idea {
    module {
        sourceDirs += file('src/main/generated')
    }
}
webapplication project

마지막으로 web application project입니다. 이는 조건이 조금 더 많습니다.

  • tomcat을 실행시켜 local 개발 환경에서 사용할 수 있어야지 됩니다.
  • 외부 테스트 또는 운영환경의 tomcat에 배포가 가능해야지 됩니다.

위 조건을 만족하기 위해서 gradle의 tomcat plugin과 cargo plugin을 이용합니다.

plugin에 대한 자료들은 다음 url에서 보다 많은 자료를 볼 수 있습니다.

tomcat plugin > https://github.com/bmuschko/gradle-tomcat-plugin
cargo plugin > https://github.com/bmuschko/gradle-cargo-plugin

build.gradle (webapp project)

apply plugin: 'war'
apply plugin: 'tomcat'
apply plugin: 'cargo'

// tomcat과 cargo plugin에 대한 repository 설정입니다.
buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }

    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0'
        classpath 'org.gradle.api.plugins:gradle-cargo-plugin:1.4'
    }
}


dependencies {
    // tomcat plugin 설정입니다.
    String tomcatVersion = '7.0.47'
    tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
    tomcat "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
    tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
        exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
    }
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    providedCompile 'javax.websocket:javax.websocket-api:1.0'
    providedCompile 'javax.servlet:jsp-api:2.0'
    providedCompile "org.apache.tomcat:tomcat-servlet-api:${tomcatVersion}"

    // cargo에 대한 설정입니다.
    def cargoVersion = '1.4.5'
    cargo "org.codehaus.cargo:cargo-core-uberjar:$cargoVersion",
            "org.codehaus.cargo:cargo-ant:$cargoVersion"


    def springVersion = "4.0.1.RELEASE"
    compile "org.springframework:spring-webmvc:${springVersion}"
    compile 'jstl:jstl:1.2'
    compile 'org.apache.tiles:tiles-jsp:3.0.3'

    compile 'org.slf4j:slf4j-api:1.7.6'
    compile 'org.slf4j:jcl-over-slf4j:1.7.6'

    compile 'ch.qos.logback:logback-classic:1.0.13'
    compile 'ch.qos.logback:logback-core:1.0.13'

    compile 'org.apache.velocity:velocity:1.7'
    compile 'org.freemarker:freemarker:2.3.20'
    compile 'com.ctlok:spring-webmvc-rythm:1.4.4'
    compile project(':bookstoreHibernate')
}

// tomcarRun을 실행시키기 위해서 war에 대한 dependency를 주입합니다.
tomcatRun {
    contextPath = ""
    URIEncoding = 'UTF-8'
    dependsOn war
}

tomcatRunWar {
    dependsOn war
}

// cargo를 이용한 배포를 위해서 war에 대한 dependency를 주입합니다.
cargoRedeployRemote {
    dependsOn war
}

cargoDeployRemote {
    dependsOn war
}

cargo {
    containerId = 'tomcat7x'
    port = 8080

    deployable {
        context = "${project.name}"
    }

    // remoteDeploy 되는 target의 tomcat 정보
    remote {
        hostname = '192.168.13.209'
        username = 'ykyoon'
        password = 'qwer12#$'
    }
}

bower를 이용한 javascript dependency

web application에서의 외부 javascript dependency를 사용하는 방법입니다. bower를 이용하는 경우, 외부에서 javascript에 대한 source code를 모두 다운받고 compile된 javascript를 dist에 저장하게 됩니다.

그런데, 우리의 web application은 dist에 저장된 특정 파일만을 사용하게 됩니다. 그럼 이 dist에 있는 file을 최종적으로 배포할 webapp folder에 넣어줘야지 됩니다. 이를 위해서 개발된 것이 bower-installer 입니다. 그런데 bower-installer의 경우에는 윈도우즈에서 동작이 정상적이지 않습니다. 아니 실행이 되지 않습니다.; 그래서 bower-installer와 동일한 동작을 하는 task를 만들어봤습니다.

먼저, bower-installer는 bower.json의 install property에 설정합니다. jquery와 bootstrap에 대한 dependency를 설정한 bower.json 입니다.

bower.json

{
    "name" : "bookstore-web",
    "version" : "0.0.0.1",
    "dependencies" : {
        "jquery" : "1.11.0",
        "bootstrap" : "3.1.1"
    },
    "install" : {
        "path" : {
            "css" : "src/main/webapp/lib/css",
            "js" : "src/main/webapp/lib/js",
            "eot" : "src/main/webapp/lib/fonts",
            "svg" : "src/main/webapp/lib/fonts",
            "ttf" : "src/main/webapp/lib/fonts",
            "woff" : "src/main/webapp/lib/fonts",
            "map" : "src/main/webapp/lib/js"
        },
        "sources" : {
            "jquery" : [
                    "bower_components/jquery/dist/jquery.min.js",
                    "bower_components/jquery/dist/jquery.min.map"
                ],
            "bootstrap" : [
                    "bower_components/bootstrap/dist/css/bootstrap.min.css",
                    "bower_components/bootstrap/dist/css/bootstrap-theme.min.css",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff",
                    "bower_components/bootstrap/dist/js/bootstrap.min.js"
                ]
        }
    }
}

위의 install property에 지정된 js와 css들을 옮기는 task는 다음과 같이 설정할 수 있습니다. war task에 dependency를 주입해서 위의 tomcatRun이나 cargoRedeployRemote 등에서도 사용할 수 있습니다.

import org.apache.tools.ant.taskdefs.condition.Os
import groovy.json.JsonSlurper
task bowerInstall(type:Exec, description : 'copy js files dependencies that is defined in Bower.js') {

    if(Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', 'bower', 'install'
    } else {
        commandLine 'bower', 'install'
    }

    def jsonHandler = new JsonSlurper()
    def jsonFile = file("bower.json")
    def conf = jsonHandler.parseText(jsonFile.getText("UTF-8"))
    def pathMap = [:]

    conf.install.path.each {
        pathMap.put(it.key, it.value)
    }

    conf.install.sources.each {
        it.value.each { f ->
            def sourceFile = file(f)
            String sourceName = sourceFile.name
            int dotPos = sourceName.lastIndexOf(".")
            String ext = sourceName.substring(dotPos + 1, sourceName.length())
            if(pathMap.containsKey(ext)) {
                copy {
                    from f
                    into pathMap.get(ext)
                }
            }
        }
    }
}

war {
    dependsOn bowerInstall
}


위 정리된 내용을 github에 공유합니다. 아래 주소에서 git clone 하시면 됩니다. ^^

 

 https://github.com/xyzlast/study-spring-bookstore.git



Posted by Y2K
,

Java VM - YoungGen space의 동작

번역(http://javaeesupportpatterns.blogspot.ie/2013/11/java-vm-beware-of-younggen-space.html) 입니다. 약간의 의역과 오역이 들어가있을수 있습니다. ^^;

정상적인 JVM 상태는 application의 성능 및 안정성에 매우 중요한 목표라고 할 수 있다. 이러한 health 평가는 GC의 major collection이나 memory leak에 촛점이 맞춰져 있다. Young Generation space 또는 shot lived object의 경우에는 크기나 저장 공간이 어떻게 될까?

이 article은 우리의 client의 최신 장애 경험에 의거한 이야기이다. 샘플 application을 통해, 이 현상을 보여줄 것이며, 메모리 저장소와 minor collection이 Old Generation collection 또는 tenured space와 비슷한 정도의 성능저하를 나타낼 수 있음을 보여줄 것이다.

JVM Health diagnostic

만약에 당신이 JVM tunning의 신참이라면, 모든 application에 공통으로 사용할 수 있는 일반적인 솔루션은 존재하지 않는다는 것은 곧 알 수 있을 것이다. Web을 통한 여러 소스의 경로를 통해, 여러분들은 JVM GC pauses가 매우 섬세하며 이것을 이해하기 위해서는 매우 많은 노력을 해야지 된다는 것을 알 수 있다. (어떤 application은 JVM pause time을 1% 이하로 줄이길 요구한다.)

성능과 load testing을 같이 하는 Java profiling (메모리 leak detection을 포함)은 JVM runtime health와 당신의 application의 memory 저장공간에 대한 데이터와 시실을 모두 수집하기 위한 좋은 방법이 된다.

그럼, “Healthy” JVM 이란 무엇을 의미하는가? 이 질문에 대해서 당신의 최고의 지식을 가지고 다음 질문들을 대답해보길 바란다.

만약에 당신이 NO라고 대답한다면, 90%이상의 확신을 가진 대답을 해주면 된다. 또는 I DON’T Know 라고 해도 된다.

  • Application에서 Java heap 또는 OldGen space leaking over time이 발생하는가? (major collection이 발생한 경우)
  • Application에서 현재 크거나 잦은 JVM GC Collection이 발생하고 있는지?
  • JVM overall pause time이 5%보다 넘거나 이상적인 baseline보다 높은지?
  • Application의 response time이 JVM GC 행위에 영향을 받고 있는지?
  • 3개월 내로, java.lang.OutOfMemoryError error를 경험해본적이 있는지?
  • 3개월 내로, JVM의 crush 현상을 격어본적이 있는지?
  • 당신의 JVM 상태가 현제 불안정하거나 인위적인 개입을 받아야지 되는 상태에 있는지? (JVM reboot etc.. )

만약에 당신이 YES 또는 I don’t know라고 대답을 했다면, 당신의 production 성능을 높이기 위해서 tuning team이 현 JVM GC policy를 살펴서 해야지 될 일들이 남아있다는 것이다.

만약에 당신이 모든 질문에 NO라고 대답할 수 있다면, 당신은 매우 견고한 application과 JVM 안정성을 가지고 있는 것이다. 축하한다. 여전히 나는 당신에게 major release와 load forecasts에 대한 상황을 평가하는 것을 제안한다.

Young Generation : Stop-The-World

우리가 JVM Health 평가를 진행할 때, JVM overall pause time은 자주 언급되는 문제이다. JVM overall pause time은 JVM이 stop the world event가 발생할 때, 얼마나 많은 시간이 걸리는지를 측정하는 것이다. Event가 발생중에, application thead는 모두 중지되고, 어떠한 일도 동작하지 않으며, application의 응답시간은 증가되게 된다. 이 수치는 JVM의 불안정성 이나 예측 불가능한 응답시간에 대한 매우 중요한 지표로 사용된다.

최근 몇년을 거쳐 사람들이 많이 알고 있으나, 잘못된 정보는 YoungGen 또는 minor collection은 매우 명료하며, application response time에 영향을 주지 않는다.라는 선입관이다. 이 문장은 대부분의 경우에 참이지만, Java heap size가 작고 (YG space < 1GB), 생명주기가 매우 짧은 경우만을 한정해야지 된다. 위 시나리오는 minor collection 실행이 매우 빠르며 (< 20msec), 자주 일어나지 않으며 (every 30 seconds++), overall JVM pause 시간중 YoungGen space가 매우 작게 남아있는 경우이다(<< 1%). 그러나, YG memory 할당 빈도수는 매우 빠르게 증가되어 이러한 상황은 매우 쉽게 변화될 수 있다(traffic의 증가로 인하여 사용자가 증가하는 경우, YG의 Size 및 할당 빈도수는 매우 쉽게 증가한다).

다음 article에서 제안한 내용을 중심으로 YoungGen space와 concurrent collectors에 대한 정보를 좀 더 얻기를 바란다.

Oracle HotSpot mostly concurrent collectors: CMS vs. G1

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

Oracle HotSpot minor collections exhaustive coverage

http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html
http://blog.griddynamics.com/2011/06/understanding-gc-pauses-in-jvm-hotspots_02.html

이미 사용하고 있는 HotSpot GC 규정에 상관없이, 모든 collector들 CMS, G1, YoungGen space collection 모두는 “stop the world”를 발생시킬 수 있다. 우리의 지식대로, Azul Zing C4만이 실시간 compacting collectior인 것으로 알려지고 있다. 우리는 현 시점에서 이 collector를 사용할 수 있는 경험을 갖지 못했다. C4 tuning 경험이나 연구 결과가 있다면 많은 공유를 부탁드린다.

지금 우리는 약간의 이론을 소개하도록 한다. sample application을 통해, 성능 검사 결과는 다양한 YoungGen의 저장공간과 할당 빈도를 보여줄 것이다.

Sample application specification

YG 할당빈도수에 따른 JVM pause time %를 측정하기 위해 우리는 다음 sample application을 작성하였다.

// jvm uri를 갖는 REST WebService (JAX-RS)
@GET
@Path("/jvm")
@Produces(MediaType.APPLICATION_JSON)
public Integer jvm() {}

jvm method의 실행은 다음 로직을 따르게 된다.

이미 결정된 크기의 short-lived object를 할당한다. (fast Young GC의 요건에 적합하다.)

추가적으로, 1 GB의 CMS collector의 동작의 약간의 noise를 가할 목적의 long-lived objects의 저장소를 만들어준다(GC의 대상이 아니다.).

private final static int LONG_LIVED_OBJ_FOOTPRINT = (1024 * 1024 * 1024);
private final static int SHORT_LIVED_OBJ_FOOTPRINT = (100 * 1024 * 1024);

// 1 GB static memory footprint
private final static byte byteArrayLongLivedObj[] = new byte[LONG_LIVED_OBJ_FOOTPRINT];

// 100 MB memory allocation (waste) created per execution
public void generateShortLivedObj(String objId) {          
  byte byteArrayShortLivedObj[] = new byte[SHORT_LIVED_OBJ_FOOTPRINT];
}

마지막으로, 테스팅하는 환경 변수는 다음과 같다.

OS: Windows 7 @64-bit
Java EE container: WildFly 8 Beta1
JVM: Oracle HotSpot 1.7 @64-bit with a Java heap size of 5 GB (YG space: 1152 MB XX:NewRatio=3). GC collector: CMS
IDE: JBoss Developer Studio 7.0.0.GA
JVM monitoring: JVisualVM
JVM memory leak analyzer: Plumbr 3.0
JVM verbose:gc log analyzer: GCMV
Performance & load testing: Apache JMeter 2.9

Performance testing results and observations

Simulated된 성능 테스팅은 높은 JVM pause time을 가지고 있었으며, peak load하에서 잦은 성능저하를 보여줬다.

Baseline

  • 10개의 concurrent thread
  • JVM process당 100 MB의 short lived object.

short lived object memory 저장 공간은 매우 극적이지만, 처음에는 잘 운영되었다.

Result

Average response time: 140 ms
Throughput: 68 req / sec
JVM overall pause time: 25.8%
YG collection frequency: 7 collections per second
Rate of GC: 308,909 MB per minute

JvirtualVM에 따르면, 이 JVM은 정상적이다(memory leak이 없으며, 안정적이며, OldGen collect 횟수가 낮다). 그러나, verbose:gc log를 좀 더 깊게 바라보면, JVM pause time이 전체 runtime 시간의 25.8%가 되는 것을 알 수 있고, 모든 지연시간은 YG Collection에 의해서 발생됨을 알 수 있다. 이것은 명확하게 verbose:gc log vs JVM에 대한 검증된 기준과의 차이에 대한 적절한 분석이 필요함을 보이고 있다.

Test & tuning #1.

  • 10 concurrent threads
  • 50 MB of short lived objects / JVM process

(먼저번의 테스트와의 차이점은 short lived object의 크기가 다르다는 점이다.)

위 조건에서 application의 메모리 할당 용량은 100MB에서 50MB 사이에서 동작하고 있었다. 우리는 명확히 개선된 것을 모든 지표에서 볼 수 있었으며 request 당 할당되는 메모리 양의 감소가 처리양에 관련이 있는 것을 볼 수 있다.

Result

Average response time: 119 ms  -21
Throughput: 79 req / sec  +11
JVM overall pause time: 15.59%  -10.21
YG collection frequency: 3-4 collections per second  -3
Rate of GC: 164 950 MB per minute  -143 959

Test & tuning #2

  • 10 concurrent threads
  • 5 MB of short lived objects created per execution per JVM process

(Test 1에 비하여 request 당 생성되는 short lived object의 크기가 5M로 반으로 줄었다.)

Result

Average response time: 107 ms  -33
Throughput: 90 req / sec  +22
JVM overall pause time: 1.9%  -23.9
YG collection frequency: 1 collection every 2-3 seconds * significant reduction
Rate of GC: 15 841 MB per minute  -293 068

보는것과 같이, application footprint와 memory 할당에 대한 최종 개선은 의미있는 매우 적절한 1.9%정도인 JVM pause time을 보여주고 있다.3번의 테스트를 통해서 매우 중요한 점을 알 수 있는데, OldGen footprint와 CMS 동작은 JVM pause time에 전혀 영향을 주지 않고 있으며, 성능 문제는 과도한 동작과 YG Collection에 연결된 매우 큰 크기의 stop the world event와 연결되어 있음을 알 수 있다.

Solution & recommandations

이 문제 케이스에서 우리는 JVM pause time을 줄이기 위해서는 연관된 과도한 YG collection 활동에 대한 tuning과 application request에 의한 memory footprint를 제거시켜주는것이 중요함을 알 수 있다. 따라서, application rate와 YG GC frequency를 줄이는 것이 중요하다.

그러나, 이러한 tuning 전략은 단기간에 가능하지 않는데, 다른 솔루션들을 분석하는 것이 더욱더 가치가 있기 때문이다. 이와 유사한 결과는 어쩌면 다음과 같은 허용량 개선 전략으로도 성취할 수도 있다.

  • Horizontal & Vertical scaling : traffic을 증가된 숫자의 JVM process로 나누면 메모리 할당 빈도와 YG Collection 횟수를 줄일 수 있다. 이는 근본적인 원인을 hardware로 넘기는 방법이다. 나의 추천사항은 언제나 당신의 application memory footprint를 먼저 tunning하고, 다른 scaling option을 찾는 것이다.
  • Java heap size & YG ratio tuning : YG 영역의 크기의 증가는 틀림없이 YG Collections에 의한 frequency를 줄일 수 있다. 그러나 매우 주의해야지 된다. 절대로 OldGen space를 굶주리게 만들어서는 안된다 그렇지 않으면 당신은 매우 간단히 문제를 JVM thrashing과 OOM events쪽으로 옮겨버리게 된다.

Final words

JVM YG collections의 성능 영향에 대해서 보다 나은 이해가 있기를 바란다. 나는 이 article을 읽은 후에 다음과 같은 실습을 해보기를 권합니다.

  • 당신이 맡고 있는 가장 바쁘게 돌아가는 application을 선택합니다.
  • verbose:gc log를 살펴보고, JVM pause time과 GCMV를 결정해보세요.
  • YG collection에 의한 영향을 판단하고, tunning 기회를 알아보시길 바랍니다.

당신의 comment와 JVM tuning 경험 기회를 공유할 수 있기를 바랍니다.


Posted by Y2K
,

(원문 : http://javaeesupportpatterns.blogspot.ie/2013/02/java-8-from-permgen-to-metaspace.html)

Java8에서 가장 큰 변화라고 개인적으로 생각하고 있는 Metaspace에 대한 post를 한번 번역해서 소개합니다.

JDK7까지 사용된 Permanent General (PermGen) space가 Oracle의 주도하에 제거된 것이 큰 특징중 하나입니다. 예를 들어, 내부 문자열의 경우, JDK7에서 이미 제거되어있었으나, JDK8에서 최종적으로 폐기되게 되었습니다.

이 문서는 PermGen의 후계자라고 할 수 있는 Metaspace에 대한 내용입니다. 또한 우리는 HotSpot 1.7 vs HotSpot 1.8 간의 leaking class meta object의 동작에 대해서 알아보도록 하겠습니다.

Metaspace : 새로운 메모리 저장소의 탄생

JDK8 HostSpot VM의 경우, 재사용성을 위한 객체에 대한 metadata를 Metaspace라고 불리우는 native memory에 저장하게 됩니다. 마치 Oracle JRockit 또는 IBM JVM과 같은 동작으로 구성되게 됩니다.

좋은 뉴스는 이제는 더이상 java.lang.OutOfMemoryError : PermGen space와 같은 문제를 더이상 야기하지 않는다는 것과 더이상 tunning과 monitoring을 통해서 이러한 memory space를 조절할 필요가 없다는 것입니다. 그렇지만, 이러한 변화는 기본적으로 안보이게 설정이 되기 때문에 우리는 이 새로운 meta memory에 대한 족적을 여전히 살펴볼 필요성은 존재하게 됩니다. 이러한 변화가 마법과 같이 class의 메모리 공간을 줄여주거나 class loader의 memory leak을 줄여주지는 못하는 것을 인지해야지 됩니다. 우리는 새로운 naming convention하에서 다른 시각으로 이러한 문제를 바라보는 것이 필요합니다.

Summary

PermGen space situation

  • memory space는 완벽하게 제거됩니다.
  • PermSize와 MaxPermSize JVM argument는 무시되며, 시작될때 경고를 표시합니다.

Metaspace memory allocation model

  • class의 metadata는 거의 대부분 native memory에 저장됩니다.
  • class의 metadata를 사용하기 위한 klasses들은 모두 제거됩니다.

Metaspace capacity

  • 기본적으로 metaspace는 native memory를 사용하기 때문에, OS에서 사용 가능한 memory 영역 모두를 사용 가능합니다.
  • 새로운 Flag(MaxMetaSpaceSize)가 가능합니다. 이는 class metadata를 저장하기 위한 최대한의 memory size를 정하는 것이 가능합니다. 만약에 Flag를 설정하지 않는다면, Metaspace는 application의 동작에 따라 가변적으로 동작하게 됩니다.

Metaspace garbage collection

  • dead class와 classloader의 GC는 MaxMetaspaceSize에 도달하면 발생합니다.
  • Metaspace의 tuning과 monitoring은 GC의 횟수와 지역을 제한하기 위해 필요한 작업입니다. 과도한 Metaspace GC는 classloader의 memory leak 또는 적합하지 않은 Application의 memory size를 초래합니다.

Java heap space impact

  • 여러 다양한 데이터들은 Java Heap space로 저장 영역이 이동되었습니다. 결국 기존 JDK7에서 JDK8로 업그레이드를 하는 경우, Java Heap memory를 증가시킬 필요가 있습니다.

Metaspace monitoring

  • metaspace 사용량은 GC log output을 통해서 확인 가능합니다.
  • Jstat * JVirtualVM 은 아직 metaspace를 monitoring 하지 못합니다. 아직 예전 PermGen space reference를 확인하도록 되어 있습니다.

PremGen vs. Metaspace runtime comparison

새로운 Metaspace memory space의 동작을 이해하기 위해서, 우리는 metadata leaking java program을 준비했습니다.

주어진 시나리오에 따라 다음과 같이 테스트를 진행했습니다.

  • JDK 1.7에서 PermGen memory space를 128M로 설정 후, PermGen memory의 감소를 관찰합니다
  • JDK 1.8에서 Metaspace memory space의 GC와 dynamic increase를 monitoring 합니다.
  • JDK 1.8에서 MaxMetaspaceSize를 128M로 설정 후, Memory의 감소를 관찰합니다.

JDK 17 - PermGen depletion

  • Java program with 50K configured iterations
  • Java heap space는 1024 MB
  • Java PermGen space 는 128MB

JVirtualVM의 경우, PermGen depletion은 약 30K+개의 class가 class loader에 로딩되었을 때, 발생합니다. GC output을 통해 다음 log를 확인할 수 있습니다.

Class metadata leak simulator
Author: Pierre-Hugues Charbonneau
http://javaeesupportpatterns.blogspot.com
ERROR: java.lang.OutOfMemoryError: PermGen space

JDK 1.8 - Metaspace dynamic re-size

  • Java program with 50K configured iterations
  • Java heap space는 1024 MB
  • Java metaspace space : 지정하지 않음 (default)


GC output에서 볼 수 있듯이, JVM Metaspace는 20MB에서 320MB로 dynamic resizing이 이루어진 것을 볼 수 있습니다. 또한 JVM의 GC 시도를 GC output을 통해서 확인 할 수 있습니다. 우리는 memory leak이 발생하는 application을 구성하고 있기 때문에, JVM은 dynamic expand 밖에 할 수 없습니다.

이 프로그램은 50K 정도의 OOM event와 50K+ 정도의 classes들을 load 시킬 수 있습니다.

JDK 1.8 - Metaspace depletion

  • Java program with 50K configured iterations
  • Java heap space는 1024 MB
  • Java metaspace space : 128M (-XX : MaxMetasapceSize=128M)

JVirtualVM에서 보듯이, metaspace 감소는 약 30K+ 개의 class들이 감소한 후 부터 발생하고 있습니다. 마치 JDK 1.7과 동작이 비슷합니다. 이러한 현상을 GC output을 통해서 확인할 수 있습니다. 또다른 흥미로운 현상은 native memory footprint를 보면 알 수 있듯이, native memory는 MaxMetaspaceSize의 2배를 할당하고 있다는 점입니다. 이것은 native memory 낭비를 줄일, Metaspace의 재할당 정책에 대한 좋은 tunning point입니다.

이제 java program output을 통해 Exception을 확인해보도록 하겠습니다.

Class metadata leak simulator
Author: Pierre-Hugues Charbonneau
http://javaeesupportpatterns.blogspot.com
ERROR: java.lang.OutOfMemoryError: Metadata space
Done!

예상과 같이, Metaspace가 128MB가 되었을 때, 마치 JDK 1.7과 거의 유사한 base line을 갖는 50 K 정도의 객체를 생성할 때 Exception이 발생합니다. 새로운 OOM(Out Of Memory) error는 JVM에 의해서 발생되게 됩니다. OOM event는 memory allocation failuire에 의해 발생되게 됩니다.

metaspace.cpp 

Final words

새로운 Java 8 Metaspace에 대해서, 빠른 분석과 설명을 알아보길 바랍니다. 현 분석대로라면 마지막 실험에서 보셨던 것과 같이, Metaspace GC 또는 OOM 조건이 발생하는 것에 대한 monitoring과 tuning이 필요한 것을 보여줍니다. 다음 article에서는 새로운 기능에 연관된 performance 향샹에 대한 비교를 포함할 수 있었으면 좋겠습니다.

Posted by Y2K
,